-- | Proof-of-concept module: use digestive functors for a command line
-- interface prompt
--
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Text.Digestive.Cli
    ( Descriptions (..)
    , Prompt
    , prompt
    , runPrompt
    ) where

import Data.Map (Map)
import qualified Data.Map as M
import Data.Monoid (Monoid, mempty, mappend)
import Control.Applicative ((<$>))

import Text.Digestive.Result
import Text.Digestive.Types
import qualified Text.Digestive.Common as Common

newtype Descriptions = Descriptions
    { unDescriptions :: Map FormId [String]
    } deriving (Show)

instance Monoid Descriptions where
    mempty = Descriptions mempty
    mappend (Descriptions m1) (Descriptions m2) =
        Descriptions $ M.unionWith (++) m1 m2

type Prompt a = Form IO String String Descriptions a

-- | Remove the descriptions for the inputs already in the input map.
--
neededDescriptions :: InputMap -> Descriptions -> Descriptions
neededDescriptions (InputMap inputMap) =
    Descriptions . M.filterWithKey notInInput . unDescriptions
  where
    notInInput k _ = k `notElem` map fst inputMap

-- | Add errors to the descriptions
--
addErrors :: [(FormRange, String)] -> Descriptions -> Descriptions
addErrors errors (Descriptions descr) = Descriptions $ foldl add' descr errors
  where
    add' map' ((FormRange x _, e)) = M.insertWith (++) x [e] map'

newtype InputMap = InputMap
    { unInputMap :: [(FormId, String)]
    } deriving (Show, Monoid)

inputMapEnvironment :: Monad m => InputMap -> Environment m String
inputMapEnvironment map' = Environment $ return . flip lookup (unInputMap map')

promptOnce :: Descriptions -> IO (FormId, String)
promptOnce (Descriptions descr)
    | M.null descr = error "No descriptions!"
    | otherwise = do putStrLn ""
                     mapM_ putStrLn description
                     putStr "> "
                     (,) key <$> getLine
  where
    (key, description) = M.findMin descr

-- | Remove all input for which errors are found
--
removeInvalidInput :: InputMap -> [(FormRange, String)] -> InputMap
removeInvalidInput = foldl removeInvalidInput'
  where
    removeInvalidInput' :: InputMap -> (FormRange, String) -> InputMap
    removeInvalidInput' (InputMap map') (range, _) =
        InputMap $ filter (not . flip isInRange range . fst) map'

prompt :: String -> Prompt String
prompt descr = Common.input (const $ const $ const [])
                            toResult
                            (\x _ -> Descriptions $ M.singleton x [descr])
                            ""
  where
    toResult Nothing _ = Error []
    toResult (Just x) _ = Ok x

runPrompt :: Prompt a -> IO a
runPrompt form = prompt' mempty
  where
    prompt' inputMap = do
        (v, r) <- runForm form "form" $ inputMapEnvironment inputMap
        case r of
            Ok x -> return x
            Error e -> do let inputMap' = removeInvalidInput inputMap e
                              descr = addErrors e
                                    $ neededDescriptions inputMap' (unView v [])
                          input' <- promptOnce descr
                          prompt' $ inputMap' `mappend` InputMap [input']