{-# LANGUAGE RecordWildCards #-}
module IOManager
  (
    -- * The @Filename@ type
    Filename

    -- * The @Input@ type
  , Input

    -- * The @Output@ type
  , Output

    -- * Exported for use in main pipeline
  , wrapIO

    -- * Exported to be usable by students
  , getStdIn
  , getInputFile
  , writeStdOut
  , writeStdErr
  , writeOutputFile
  ) where

import qualified Data.Map as Map
import System.Environment (getArgs)
import qualified System.IO as System

type Filename = String

-- | Type of values holding inputs to the program, grouped by input source.
data Input = Input
  { stdin :: String
  , fileInput :: Map.Map Filename String
  }

-- | Type of values holding outputs of the program, grouped by output source.
data Output = Output
  { stdout :: String
  , stderr :: String
  , fileOutput :: Map.Map Filename String
  }

-- | Obtains the contents of the standard input as given to the program.
-- Returns a String containing the input without any modification.
getStdIn :: Input -> String
getStdIn = stdin

-- | Obtains the contents of an input file. Returns a String containing the
-- input without any modification.
getInputFile :: Input -> Filename -> String
getInputFile i f = fileInput i Map.! f

-- | Appends text to the standard output. No newline is printed at the end,
-- the caller must handle it. Returns a new @Output@ value, containing the
-- appended text.
writeStdOut :: Output -> String -> Output
writeStdOut o@Output{..} s = o { stdout = stdout ++ s }

-- | Appends text to the standard error. No newline is printed at the end, the
-- caller must handle it. Returns a new @Output@ value, containing the
-- appended text.
--
-- **Note**: When running the program, the standard error text is displayed
-- after the entire text from the standard input is displayed.
writeStdErr :: Output -> String -> Output
writeStdErr o@Output{..} s = o { stderr = stderr ++ s }

-- | Appends to an output file. If the file does not exist in the @Output@
-- value (this program didn't yet write in it), it is created as a new one.
-- Returns a new @Output@ value, containing the appended text.
writeOutputFile :: Output -> Filename -> String -> Output
writeOutputFile o@Output{..} f s
  = o { fileOutput = Map.insertWith (++) f s fileOutput }

-- | Reads the input from all the files given as command line arguments and
-- constructs an @Input@ value.
readInput :: IO (Input, Output)
readInput = do
  args <- getArgs
  imap <- readInputFiles args Map.empty
  input <- getContents
  return $ (Input input imap, Output "" "" Map.empty)

-- | Writes the contents of an @Output@ value to the needed files.
writeOutput :: Output -> IO ()
writeOutput o = do
  putStrLn $ stdout o
  System.hPutStrLn System.stderr $ stderr o
  writeOutputFiles $ Map.toList $ fileOutput o

-- | Wraps a simple function @Input@ -> @Output@ -> @Output@ in
-- order to simplify student's usage.
wrapIO :: (Input -> Output -> Output) -> IO ()
wrapIO f = readInput >>= return . uncurry f >>= writeOutput

-- Reads all of the input files into the map of the Input value.
readInputFiles :: [Filename]
               -> Map.Map Filename String
               -> IO (Map.Map Filename String)
readInputFiles [] m = return m
readInputFiles (f:fs) m = do
  content <- readFile f
  readInputFiles fs $ Map.insert f content m

-- Writes the content of all the output files.
writeOutputFiles :: [(Filename, String)] -> IO ()
writeOutputFiles [] = return ()
writeOutputFiles ((f,s):fs) = writeFile f s >> writeOutputFiles fs
