{- Copyright © 2012, Vincent Elisha Lee Frey.  All rights reserved.
 - This is open source software distributed under a MIT license.
 - See the file 'LICENSE' for further information.
 -}
module System.Console.CmdTheLine.Common where

import Data.Function    ( on )
import Text.PrettyPrint ( Doc, text )
import Control.Applicative ( Applicative(..) )

import qualified Data.Map as M

import Control.Monad.Trans.Error

data Absence = Absent
             | Present String
               deriving ( Eq )

data OptKind = FlagKind
             | OptKind
             | OptVal String
               deriving ( Eq )

data PosKind = PosAny
             | PosN Bool Int
             | PosL Bool Int
             | PosR Bool Int
               deriving ( Eq, Ord )

data ArgInfo = ArgInfo
  { absence    :: Absence
  , argDoc     :: String
  , argName    :: String
  , argSec     :: String
  , posKind    :: PosKind
  , optKind    :: OptKind
  , optNames   :: [String]
  , repeatable :: Bool
  }

instance Eq ArgInfo where
  ai == ai'
    | isPos ai && isPos ai' = ((==) `on` posKind) ai ai'
    | isOpt ai && isOpt ai' = ((==) `on` optNames) ai ai'
    | otherwise             = False

-- This Ord instance works for placing in 'Data.Map's, but not much else.
instance Ord ArgInfo where
  compare ai ai'
    | isPos ai && isPos ai' = (compare `on` posKind) ai ai'
    | isOpt ai && isOpt ai' = (compare `on` optNames) ai ai'
    | isOpt ai && isPos ai' = LT
    | otherwise             = GT


data Arg = Opt [( Int          -- The position were the argument was found.
                , String       -- The name by which the argument was supplied.
                , Maybe String -- If present, a value assigned to the argument.
                )]
         | Pos [String]        -- A list of positional arguments

type CmdLine = M.Map ArgInfo Arg

isOpt, isPos :: ArgInfo -> Bool
isOpt ai = optNames ai /= []
isPos ai = optNames ai == []

{- |
  Any 'String' argument to a 'ManBlock' constructor may contain the
  following significant forms for a limited kind of meta-programing.

  * $(i,text): italicizes @text@.

  * $(b,text): bolds @text@.

  * $(mname): evaluates to the name of the default term if there are choices
    of commands, or the only term otherwise.

  * $(tname): evaluates to the name of the currently evaluating term.

  Additionally, text inside the content portion of an 'I' constructor may
  contain one of the following significant forms.

  * $(argName): evaluates to the name of the argument being documented.

-}
data ManBlock = S String        -- ^ A section title.
              | P String        -- ^ A paragraph.
              | I String String -- ^ A label-content pair. As in an argument
                                --   definition and its accompanying
                                --   documentation.
              | NoBlank         -- ^ Suppress the normal blank line following
                                --   a 'P' or an 'I'.
                deriving ( Eq )

type Title = ( String, Int, String, String, String )

type Page = ( Title, [ManBlock] )

-- | Information about a 'Term'.  It is recommended that 'TermInfo's be
-- created by customizing 'defTI', as in
--
-- > termInfo = defTI
-- >   { termName = "caroline-no"
-- >   , termDoc  = "carry a line off"
-- >   }
data TermInfo = TermInfo
  {
  -- | The name of the command or program represented by the term. Defaults to
  -- @\"\"@.
    termName  :: String

  -- | Documentation for the term. Defaults to @\"\"@.
  , termDoc   :: String

  -- | The section under which to place the terms documentation.
  -- Defaults to @\"COMMANDS\"@.
  , termSec   :: String

  -- | The section under which to place a term's argument's
  -- documentation by default. Defaults to @\"OPTIONS\"@.
  , stdOptSec :: String

  -- | A version string.  Must be left blank for commands. Defaults to @\"\"@.
  , version   :: String

  -- | A list of 'ManBlock's to append to the default @[ManBlock]@. Defaults
  -- to @[]@.
  , man       :: [ManBlock]
  } deriving ( Eq )

-- | A default 'TermInfo'.
defTI :: TermInfo
defTI = TermInfo
  { termName  = ""
  , version   = ""
  , termDoc   = ""
  , termSec   = "COMMANDS"
  , stdOptSec = "OPTIONS"
  , man       = []
  }

type Command = ( TermInfo, [ArgInfo] )

data EvalInfo = EvalInfo
  { term    :: Command   -- The chosen term for this run.
  , main    :: Command   -- The default term.
  , choices :: [Command] -- A list of command-terms.
  }

-- | The format to print help in.
data HelpFormat = Pager | Plain | Groff deriving ( Eq )

data Fail = MsgFail   Doc
          | UsageFail Doc
          | HelpFail  HelpFormat (Maybe String)

instance Error Fail where
  strMsg = MsgFail . text

-- | A monad for values in the context of possibly failing with a helpful
-- message.
type Err = ErrorT Fail IO


type Yield a = EvalInfo -> CmdLine -> Err a

-- | The underlying Applicative of the library.  A @Term@ represents a value
-- in the context of being computed from the command line arguments.
data Term a = Term [ArgInfo] (Yield a)

instance Functor Term where
  fmap = yield . result . result . fmap
    where
    yield f (Term ais y) = Term ais (f y)
    result = (.)

instance Applicative Term where
  pure v = Term [] (\ _ _ -> return v)

  (Term args f) <*> (Term args' v) = Term (args ++ args') wrapped
    where
    wrapped ei cl = f ei cl <*> v ei cl


data EvalKind = Simple   -- The program has no commands.
              | Main     -- The default program is running.
              | Choice   -- A command has been chosen.

evalKind :: EvalInfo -> EvalKind
evalKind ei
  | choices ei == []               = Simple
  | fst (term ei) == fst (main ei) = Main
  | otherwise                      = Choice

descCompare :: Ord a => a -> a -> Ordering
descCompare = flip compare

splitOn :: Eq a => a -> [a] -> ( [a], [a] )
splitOn sep xs = ( left, rest' )
  where
  rest' = if rest == [] then rest else tail rest -- Skip the 'sep'.
  ( left, rest ) = span (/= sep) xs

select :: a -> [( Bool, a )] -> a
select baseCase = foldr (uncurry (?)) baseCase
  where
  (?) True = const
  (?) False = flip const