module DDC.Interface.Input
        ( InputInterface (..)
        , InputState     (..)
        , Input          (..)
        , readInput
        , inputLine)
where
import DDC.Interface.Source
import DDC.Data.ListUtils
import System.Directory
import Data.List
import Data.Char


-- InputState -----------------------------------------------------------------
-- Interpreter input state
data InputState command
        = InputState
        { -- Function to parse commands.
          inputParseCommand :: String -> Maybe (command, String)

          -- Input mode.
        , inputMode         :: Input

          -- Command that we're still receiving input for,
          -- along with the line number it started on.
        , inputCommand      :: Maybe (Maybe command, Int)


          -- The current line number in the command stream.
        , inputLineNumber   :: Int

          -- Accumulation of current input buffer.
        , inputAcc         :: String }


-- InputInterface -------------------------------------------------------------
-- | What interface is being used.
data InputInterface
        -- | Read commands from unix command-line args.
        = InputInterfaceArgs

        -- | Read commands interactively from the console.
        | InputInterfaceConsole

        -- | Read commands from the file with this name.
        | InputInterfaceBatch    FilePath
        deriving (Eq, Show)


-- Input ----------------------------------------------------------------------
-- | How we're reading the current expression.
data Input
        -- | Read input line-by-line, using a backslash at the end of the
        --   line to continue to the next.
        = InputLine

        -- | Read input as a block terminated by a double semicolon (;;)
        | InputBlock

        -- | Read input from a file specified on the prompt
        | InputFile     FilePath
        deriving (Eq, Show)


-- | Read the input mode from the front of a string.
readInput :: String -> (Input, String)
readInput ss
        | isPrefixOf ".." ss
        = (InputBlock, drop 2 ss)

        | isPrefixOf "<" ss
        , filePath      <- dropWhile isSpace (drop 1 ss)
        = (InputFile filePath, drop (length filePath) ss)

        | otherwise
        = (InputLine, ss)

-------------------------------------------------------------------------------
inputLine 
        :: InputInterface
        -> InputState c 
        -> String
        -> IO ( InputState c
              , Maybe (Source, Maybe c, String))
                        -- Just for complete command.
                        --  .. then Maybe for the default command.

inputLine interface inputState chunk
 | InputState readCmd mode mCommand lineNumber acc <- inputState
 = do   
        -- If this is the first line then try to read the command and
        --  input mode from the front so we know how to continue.
        -- If we can't read an explicit command then assume this is 
        --  an expression to evaluate.
        let (cmd, lineStart, (input, rest))
             = case mCommand of
                -- We haven't started a command yet.
                Nothing
                 -> case readCmd chunk of
                     Just (cmd', rest') -> (Just cmd', lineNumber, readInput rest')
                     Nothing            -> (Nothing,   lineNumber, (InputLine, chunk))
                
                -- We've already started a command, and this is more input for it.
                Just (cmd', lineStart')
                 -> (cmd', lineStart', (mode, chunk))

        let source 
                -- We were instructed to read the program from a file.
                -- Report this file as the source location, independent
                -- of how we were instructed to read it.
                | InputFile filePath    <- input
                = SourceFile filePath

                -- The program was embedded in the command stream.
                | otherwise
                = case interface of
                        InputInterfaceArgs        -> SourceArgs
                        InputInterfaceConsole     -> SourceConsole lineStart
                        InputInterfaceBatch file  -> SourceBatch   file lineStart


        case input of
         -- For line-by-line mode, if the line ends with backslash then keep
         -- reading, otherwise run the command.
         -- We also convert the backslash to a newline so the source
         -- position comes out right in parser error messages.
         InputLine
          | not $ null rest
          , last rest == '\\'
          , Just initRest       <- takeInit rest
          -> return     ( inputState
                                { inputCommand    =  (Just (cmd, lineStart))
                                , inputLineNumber = lineNumber + 1
                                , inputAcc        = acc ++ initRest ++ "\n" }
                        , Nothing)

          | otherwise
          -> return     ( inputState
                                { inputMode       = InputLine
                                , inputCommand    = Nothing
                                , inputLineNumber = lineNumber + 1
                                , inputAcc        = [] }
                        , Just (source, cmd, acc ++ rest))


         -- For block mode, if the line ends with ';;' then run the command,
         -- otherwise keep reading.
         InputBlock
          | isSuffixOf ";;" rest
          -> do let rest' = take (length rest - 2) rest
                return  ( inputState
                                { inputMode       = InputLine
                                , inputCommand    = Nothing
                                , inputLineNumber = lineNumber + 1
                                , inputAcc        = [] }
                       , Just (source, cmd, acc ++ rest'))

          | otherwise
          ->    return ( inputState
                                { inputMode       = input
                                , inputCommand    = Just (cmd, lineStart)
                                , inputLineNumber = lineNumber + 1
                                , inputAcc        = acc ++ rest ++ "\n" }
                       , Nothing)

         -- Read input from a file
         InputFile filePath
          -> do exists          <- doesFileExist filePath
                if exists 
                 then do        
                        contents  <- readFile filePath
                        return  ( inputState
                                        { inputMode     = InputLine
                                        , inputCommand  = Nothing
                                        , inputLineNumber = lineNumber + 1
                                        , inputAcc      = [] }
                                , Just (source, cmd, contents))
                 else do
                        putStrLn "No such file."
                        return  ( inputState
                                        { inputMode     = InputLine
                                        , inputCommand  = Nothing
                                        , inputLineNumber = lineNumber + 1
                                        , inputAcc      = [] }
                                , Nothing)