-- | Maddash is a Mealy finite state machine that processes /options/.
-- Ordinarily you will not need this module; instead, see "Multiarg"
-- for most uses or "Multiarg.Mode" for commands that have more than
-- one mode.
--
-- The machine consists of the following parts:
--
-- * The set of states, in 'State'
--
-- * The start state, which is 'Ready'
--
-- * The input alphabet, which is all 'Word's.  A 'Word' is an
-- input /word/ from the command line.
--
-- * The output alphabet, which is 'Pallet'.  A 'Pallet' indicates
-- whether its input is not an option at all with 'NotAnOption'.  This
-- indicates that the input 'Word' was not a short option and was not
-- a long option; that is, it was not a single dash followed by a
-- non-dash character and it was not a double dash followed by another
-- character.  (Neither a single dash alone nor a double dash alone is
-- an option.)  Anything else is an option and will return 'Full',
-- which is a list of 'Output'.  Each 'Output' indicates either an
-- error or a good result.
--
-- * The transition function and the output function are combined into
-- a single function, 'processWord'.

module Multiarg.Maddash
  ( -- * /Options/ and /option arguments/
    OptName(..)
  , optSpec
  , ArgSpec(..)
  , ShortName
  , LongName
  , shortName
  , longName
  , shortNameToChar
  , longNameToString

  -- * Machine components
  , Output(..)
  , Pallet(..)
  , State(..)
  , isReady
  , isPending
  , processWord

  -- * Multi-word processor
  , processWords

  -- * Errors
  , OptArg(..)
  , OptionError(..)
  ) where

import Control.Applicative
import Multiarg.Types

-- * Machine components

data Output a
  = Good a
  | OptionError OptionError
  deriving (Eq, Ord, Show)

instance Functor Output where
  fmap f (Good a) = Good (f a)
  fmap _ (OptionError e) = OptionError e

data Pallet a
  = NotAnOption
  | Full [Output a]
  deriving (Eq, Ord, Show)

instance Functor Pallet where
  fmap _ NotAnOption = NotAnOption
  fmap f (Full os) = Full (map (fmap f) os)

data State a
  = Ready
  -- ^ Accepting new words

  | Pending OptName (Word -> ([Output a], State a))
  -- ^ In the middle of processing an /option/; this function will be
  -- applied to the next word to get a result


instance Functor State where
  fmap _ Ready = Ready
  fmap f (Pending o g)
    = Pending o (\t -> let (os, st') = g t
                       in (map (fmap f) os, fmap f st'))

instance Show (State a) where
  show Ready = "Ready"
  show (Pending o _) = "Pending - " ++ show o

isReady :: State a -> Bool
isReady Ready = True
isReady _ = False

isPending :: State a -> Bool
isPending (Pending _ _) = True
isPending _ = False

-- | Process a single word in the machine.
processWord
  :: [(ShortName, ArgSpec a)]
  -> [(LongName, ArgSpec a)]
  -> State a
  -> Word
  -> (Pallet a, State a)
processWord shorts longs st inp = case st of
  Pending _ f -> (Full os, st')
    where
      (os, st') = f inp
  Ready -> case procOpt of
    Just (os, st') -> (Full os, st')
    Nothing -> (NotAnOption, Ready)
    where
      procOpt = procShort shorts inp <|> procLong longs inp

-- * Multi-word processor

-- | Processes multiple /words/ in the machine.  Processing ends with
-- the first /word/ that is 'NotAnOption'.  This first /word/ that is
-- 'NotAnOption', and all remaining /words/, are returned in the
-- result.  A list of all lists of 'Output' are also returned, with
-- one list for each input 'Word' that was processed.  Each of these
-- lists may be of any length.  For instance, if the input /word/ is
-- the /flag/ for a /long option/ that takes two /option arguments/,
-- the corresponding list will be empty.  If the input /word/ is a
-- /flag/ for a /short option/, this list may have more than one
-- element.
processWords
  :: [(ShortName, ArgSpec a)]
  -> [(LongName, ArgSpec a)]
  -> [Word]
  -> ([[Output a]], Either (OptName, Word -> ([Output a], State a)) [Word])
processWords shorts longs = go Ready
  where
    go Ready [] = ([], Right [])
    go (Pending opt f) [] = ([], Left (opt, f))
    go st (t:ts) = (rs, eiToksPend)
      where
        (pallet, st') = processWord shorts longs st t
        (rs, eiToksPend) = case pallet of
          NotAnOption -> ([], Right (t:ts))
          Full out -> (out : outRest, ei)
            where
              (outRest, ei) = go st' ts

-- * Errors

data OptionError
  = BadOption OptName
  | LongArgumentForZeroArgumentOption LongName OptArg
  -- ^ The user gave an argument for a long option that does not take
  -- an argument.
  deriving (Eq, Ord, Show)

-- * All exported types and functions above this line

-- * Internal functions - not exported

-- | Examines a word to determine if it is a short option.  If so,
-- processes it; otherwise, returns Nothing.
procShort
  :: [(ShortName, ArgSpec a)]
  -> Word
  -> Maybe ([Output a], State a)
procShort shorts inp = fmap (getShortOpt shorts) (isShort inp)

getShortOpt
  :: [(ShortName, ArgSpec a)]
  -> (ShortName, ShortTail)
  -> ([Output a], State a)
getShortOpt shorts (short, rest) = case lookup short shorts of
  Nothing -> ( [OptionError (BadOption (OptName (Left short))) ], Ready)
  Just arg -> procShortOpt shorts short arg rest

procShortOpt
  :: [(ShortName, ArgSpec a)]
  -> ShortName
  -> ArgSpec a
  -> ShortTail
  -> ([Output a], State a)
procShortOpt opts _ (ZeroArg a) inp = (this : rest, st)
  where
    this = Good a
    (rest, st) = case splitShortTail inp of
      Nothing -> ([], Ready)
      Just (opt,arg) -> getShortOpt opts (opt, arg)

procShortOpt _ shrt (OneArg f) (ShortTail inp) = case inp of
  [] -> ([], Pending opt g)
    where
      g tok = ([res], Ready)
        where
          res = Good . f . optArgToString $ arg
          arg = wordToOptArg tok
  xs -> ([res], Ready)
    where
      res = Good . f . optArgToString $ optArg
      optArg = OptArg xs
  where
    opt = OptName (Left shrt)

procShortOpt _ shrt (TwoArg f) (ShortTail inp) = ([], Pending opt g)
  where
    g tok1 = case inp of
      [] -> ([], Pending opt h)
        where
          h tok2 = ([res], Ready)
            where
              oa2 = wordToOptArg tok2
              res = Good $ f (optArgToString oa1) (optArgToString oa2)

      xs -> ([res], Ready)
        where
          res = Good $ f (optArgToString tokArg) (optArgToString oa1)
          tokArg = OptArg xs
      where
        oa1 = wordToOptArg tok1
    opt = OptName (Left shrt)

procShortOpt _ shrt (ThreeArg f) (ShortTail inp) = ([], Pending opt g)
  where
    opt = OptName (Left shrt)
    g tok1 = ([], Pending opt h)
      where
        oa1 = wordToOptArg tok1
        h tok2 = case inp of
          [] -> ([], Pending opt i)
            where
              i tok3 = ([res], Ready)
                where
                  oa3 = wordToOptArg tok3
                  res = Good $ f (optArgToString oa1) (optArgToString oa2)
                                 (optArgToString oa3)
          tokInp -> ([res], Ready)
            where
              tokArg = wordToOptArg (Word tokInp)
              res = Good $ f (optArgToString tokArg) (optArgToString oa1)
                             (optArgToString oa2)
          where
            oa2 = wordToOptArg tok2

procLong
  :: [(LongName, ArgSpec a)]
  -> Word
  -> Maybe ([Output a], State a)
procLong longs inp = fmap (procLongOpt longs) (isLong inp)

procLongOpt
  :: [(LongName, ArgSpec a)]
  -> (LongName, Maybe OptArg)
  -> ([Output a], State a)
procLongOpt longs (inp, mayArg) = case lookup inp longs of
  Nothing -> ( [OptionError (BadOption . OptName . Right $ inp)], Ready)
  Just (ZeroArg r) -> ([result], Ready)
    where
      result = case mayArg of
        Nothing -> Good r
        Just arg -> OptionError (LongArgumentForZeroArgumentOption inp arg)

  Just (OneArg f) -> case mayArg of
    Nothing -> ([], Pending opt run)
      where
        run tok = ([out], Ready)
          where
            out = Good $ f (optArgToString arg1)
            arg1 = wordToOptArg tok
    Just arg -> ([out], Ready)
      where
        out = Good $ f (optArgToString arg)

  Just (TwoArg f) -> ([], Pending opt g)
    where
      g gTok = case mayArg of
        Just arg1 -> ([out], Ready)
          where
            out = Good $ f (optArgToString arg1) (optArgToString gArg)
        Nothing -> ([], Pending opt h)
          where
            h hTok = ([out], Ready)
              where
                out = Good $ f (optArgToString gArg)
                               (optArgToString hArg)
                hArg = wordToOptArg hTok
        where
          gArg = wordToOptArg gTok

  Just (ThreeArg f) -> ([], Pending opt g)
    where
      g gTok = ([], Pending opt h)
        where
          gArg = wordToOptArg gTok
          h hTok = case mayArg of
            Just arg1 -> ([out], Ready)
              where
                out = Good $ f (optArgToString arg1) (optArgToString gArg)
                               (optArgToString hArg)
            Nothing -> ([], Pending opt i)
              where
                i iTok = ([out], Ready)
                  where
                    iArg = wordToOptArg iTok
                    out = Good $ f (optArgToString gArg)
                                   (optArgToString hArg)
                                   (optArgToString iArg)
            where
              hArg = wordToOptArg hTok
  where
    opt = OptName (Right inp)

-- * end