-- | Types used throughout Multiarg, and associated functions.
-- Ordinarily you should not need this module; "Multiarg" and
-- "Multiarg.Mode" export all the types and constructors you should
-- ordinarily need.  However, if you want more control than those
-- modules afford, you can import this one.
module Multiarg.Types
  ( ArgSpec(..)
  , OptSpec(..)
  , optSpec
  , ShortName
  , shortNameToChar
  , shortName
  , LongName
  , longNameToString
  , longName
  , Word(..)
  , OptName(..)
  , optNameToString
  , OptArg(..)
  , ShortTail(..)
  , isLong
  , isShort
  , wordToOptArg
  , splitShortTail
  ) where

-- GHC 7.10 incorporates 'Data.Word' into the Prelude, which clashes
-- with a binding below.
import Prelude hiding (Word)

-- | Specifies how many /option arguments/ an /option/ takes.
data ArgSpec a
  = ZeroArg a
  -- ^ This /option/ takes no /option arguments/
  | OneArg (String -> a)
  -- ^ This /option/ takes one /option argument/
  | TwoArg (String -> String -> a)
  -- ^ This /option/ takes two /option arguments/
  | ThreeArg (String -> String -> String -> a)
  -- ^ This /option/ takes three /option arguments/

instance Functor ArgSpec where
  fmap f (ZeroArg a) = ZeroArg (f a)
  fmap f (OneArg g) = OneArg $ \a -> f (g a)
  fmap f (TwoArg g) = TwoArg $ \a b -> f (g a b)
  fmap f (ThreeArg g) = ThreeArg $ \a b c -> f (g a b c)

instance Show (ArgSpec a) where
  show (ZeroArg _) = "ZeroArg"
  show (OneArg _) = "OneArg"
  show (TwoArg _) = "TwoArg"
  show (ThreeArg _) = "ThreeArg"

-- | Specifies an /option/.  Typically you will use 'optSpec' to
-- create an 'OptSpec' rather than using the constructor directly.
-- Each 'OptSpec' may contain mulitple /short option names/ and
-- /long option names/; but each 'OptSpec' contains only one 'ArgSpec'.
-- Therefore, all /short option names/ and /long option names/
-- specified in a single 'OptSpec' are synonymous.
data OptSpec a = OptSpec [ShortName] [LongName] (ArgSpec a)
  deriving Show

instance Functor OptSpec where
  fmap f (OptSpec s l p) = OptSpec s l (fmap f p)

-- | Creates an 'OptSpec'.
optSpec
  :: [Char]
  -- ^ There is one character for each desired /short option name/.
  -- Each of these characters may not be a hyphen; otherwise,
  -- 'optSpec' will apply 'error'.

  -> [String]
  -- ^ There is one string for each desired /long option name/.  Each
  -- string:
  --
  -- * cannot be empty;
  --
  -- * must not begin with a hyphen; and
  --
  -- * must not contain an equal sign.
  --
  -- Otherwise, 'optSpec' will apply 'error'.

  -> ArgSpec a
  -- ^ How many /option arguments/ this /option/ takes.  This also
  -- specifies what is returned when the /option/ is parsed on the
  -- command line.

  -> OptSpec a
optSpec ss ls = OptSpec (map mkShort ss) (map mkLong ls)
  where
    mkShort s = case shortName s of
      Nothing -> error $ "invalid short option name: " ++ [s]
      Just n -> n
    mkLong s = case longName s of
      Nothing -> error $ "invalid long option name: " ++ s
      Just n -> n


-- | A /short option name/.
newtype ShortName = ShortName { shortNameToChar ::  Char }
  deriving (Eq, Ord, Show)

-- | A /long option name/.
newtype LongName = LongName { longNameToString :: String }
  deriving (Eq, Ord, Show)

-- | Creates a /short option name/.  Any character other than a single
-- hyphen will succeed.
shortName :: Char -> Maybe ShortName
shortName '-' = Nothing
shortName x = Just $ ShortName x

-- | Creates a /long option name/.  The string may not be empty, and the
-- first character may not be a hyphen.  In addition, no character may
-- be an equal sign.
longName :: String -> Maybe LongName
longName s = case s of
  [] -> Nothing
  '-':_ -> Nothing
  xs | '=' `elem` xs -> Nothing
     | otherwise -> Just $ LongName xs

-- | The /name/ of an /option/ (either a /short option name/
-- or a /long option name/).
newtype OptName = OptName (Either ShortName LongName)
  deriving (Eq, Ord, Show)

optNameToString :: OptName -> String
optNameToString (OptName ei) = case ei of
  Left shrt -> '-' : shortNameToChar shrt : []
  Right lng -> "--" ++ longNameToString lng

-- | A /word/ supplied by the user on the command line.
newtype Word = Word String
  deriving (Eq, Ord, Show)

-- | An /option argument/.
newtype OptArg = OptArg { optArgToString :: String }
  deriving (Eq, Ord, Show)

-- | Is this /word/ an input for a /long option/?
isLong
  :: Word
  -> Maybe (LongName, Maybe OptArg)
  -- ^ Nothing if the option does not begin with a double dash and is
  -- not at least three characters long.  Otherwise, returns the
  -- characters following the double dash to the left of any equal
  -- sign.  The Maybe in the tuple is Nothing if there is no equal
  -- sign, or Just followed by characters following the equal sign if
  -- there is one.
isLong (Word ('-':'-':[])) = Nothing
isLong (Word ('-':'-':xs)) = Just (LongName optName, arg)
  where
    (optName, end) = span (/= '=') xs
    arg = case end of
      [] -> Nothing
      _:rs -> Just . OptArg $ rs
isLong _ = Nothing

-- | Characters after the first /short option name/ in a /flag/ that
-- specifies a /short option/ instance, if the user supplies
-- @-afoobar@, then this will be @foobar@.
newtype ShortTail = ShortTail String
  deriving (Eq, Ord, Show)

-- | Is this an input /word/ for a /short argument/?
isShort
  :: Word
  -> Maybe (ShortName, ShortTail)
isShort (Word ('-':'-':_)) = Nothing
isShort (Word ('-':[])) = Nothing
isShort (Word ('-':x:xs)) = Just (ShortName x, ShortTail xs)
isShort _ = Nothing

wordToOptArg :: Word -> OptArg
wordToOptArg (Word t) = OptArg t

-- | If possible, splits a ShortTail into a /short option name/ and a
-- remaining tail.
splitShortTail :: ShortTail -> Maybe (ShortName, ShortTail)
splitShortTail (ShortTail s) = case s of
  [] -> Nothing
  x:xs -> Just (ShortName x, ShortTail xs)