module Data.Enum.Deriving.Internal (
      
      deriveEnum
    , makeSucc
    , makePred
    , makeToEnum
    , makeFromEnum
    , makeEnumFrom
    , makeEnumFromThen
    ) where
import Data.Deriving.Internal
import Language.Haskell.TH.Datatype
import Language.Haskell.TH.Lib
import Language.Haskell.TH.Syntax
deriveEnum :: Name -> Q [Dec]
deriveEnum name = do
  info <- reifyDatatype name
  case info of
    DatatypeInfo { datatypeContext   = ctxt
                 , datatypeName      = parentName
                 , datatypeInstTypes = instTypes
                 , datatypeVariant   = variant
                 , datatypeCons      = cons
                 } -> do
      (instanceCxt, instanceType)
          <- buildTypeInstance EnumClass parentName ctxt instTypes variant
      (:[]) `fmap` instanceD (return instanceCxt)
                             (return instanceType)
                             (enumFunDecs parentName instanceType cons)
makeSucc :: Name -> Q Exp
makeSucc = makeEnumFun Succ
makePred :: Name -> Q Exp
makePred = makeEnumFun Pred
makeToEnum :: Name -> Q Exp
makeToEnum = makeEnumFun ToEnum
makeFromEnum :: Name -> Q Exp
makeFromEnum = makeEnumFun FromEnum
makeEnumFrom :: Name -> Q Exp
makeEnumFrom = makeEnumFun EnumFrom
makeEnumFromThen :: Name -> Q Exp
makeEnumFromThen = makeEnumFun EnumFromThen
enumFunDecs :: Name -> Type -> [ConstructorInfo] -> [Q Dec]
enumFunDecs tyName ty cons =
    map makeFunD [ Succ
                 , Pred
                 , ToEnum
                 , EnumFrom
                 , EnumFromThen
                 , FromEnum
                 ]
  where
    makeFunD :: EnumFun -> Q Dec
    makeFunD ef =
      funD (enumFunName ef)
           [ clause []
                    (normalB $ makeEnumFunForCons ef tyName ty cons)
                    []
           ]
makeEnumFun :: EnumFun -> Name -> Q Exp
makeEnumFun ef name = do
  info <- reifyDatatype name
  case info of
    DatatypeInfo { datatypeContext   = ctxt
                 , datatypeName      = parentName
                 , datatypeInstTypes = instTypes
                 , datatypeVariant   = variant
                 , datatypeCons      = cons
                 } -> do
      (_, instanceType) <- buildTypeInstance EnumClass parentName ctxt instTypes variant
      makeEnumFunForCons ef parentName instanceType cons
makeEnumFunForCons :: EnumFun -> Name -> Type -> [ConstructorInfo] -> Q Exp
makeEnumFunForCons _  _      _  [] = noConstructorsError
makeEnumFunForCons ef tyName ty cons
    | not $ isEnumerationType cons
    = enumerationError tyNameBase
    | otherwise = case ef of
        Succ -> lamOneHash $ \aHash ->
          condE (varE eqValName `appE` maxTagExpr `appE`
                   (conE iHashDataName `appE` varE aHash))
                (illegalExpr "succ" tyNameBase
                             "tried to take `succ' of last tag in enumeration")
                (tag2Con `appE` (varE plusValName `appE`
                  (conE iHashDataName `appE` varE aHash) `appE` integerE 1))
        Pred -> lamOneHash $ \aHash ->
          condE (varE eqValName `appE` integerE 0 `appE`
                   (conE iHashDataName `appE` varE aHash))
                (illegalExpr "pred" tyNameBase
                             "tried to take `pred' of first tag in enumeration")
                (tag2Con `appE` (varE plusValName `appE`
                  (conE iHashDataName `appE` varE aHash) `appE` integerE (-1)))
        ToEnum -> lamOne $ \a ->
          condE (appsE [ varE andValName
                       , varE geValName `appE` varE a `appE` integerE 0
                       , varE leValName `appE` varE a `appE` maxTagExpr
                       ])
                (tag2Con `appE` varE a)
                (illegalToEnumTag tyNameBase maxTagExpr a)
        EnumFrom -> lamOneHash $ \aHash ->
          appsE [ varE mapValName
                , tag2Con
                , enumFromToExpr (conE iHashDataName `appE` varE aHash) maxTagExpr
                ]
        EnumFromThen -> do
          a     <- newName "a"
          aHash <- newName "a#"
          b     <- newName "b"
          bHash <- newName "b#"
          lamE [varP a, varP b] $ untagExpr [(a, aHash), (b, bHash)] $
              appE (varE mapValName `appE` tag2Con) $
                  enumFromThenToExpr
                    (conE iHashDataName `appE` varE aHash)
                    (conE iHashDataName `appE` varE bHash)
                    (condE (appsE [ varE gtValName
                                  , conE iHashDataName `appE` varE aHash
                                  , conE iHashDataName `appE` varE bHash
                                  ])
                           (integerE 0) maxTagExpr)
        FromEnum -> lamOneHash $ \aHash ->
          conE iHashDataName `appE` varE aHash
  where
    tyNameBase :: String
    tyNameBase = nameBase tyName
    maxTagExpr :: Q Exp
    maxTagExpr = integerE (length cons - 1) `sigE` conT intTypeName
    lamOne :: (Name -> Q Exp) -> Q Exp
    lamOne f = do
        a <- newName "a"
        lam1E (varP a) $ f a
    lamOneHash :: (Name -> Q Exp) -> Q Exp
    lamOneHash f = lamOne $ \a -> do
        aHash <- newName "a#"
        untagExpr [(a, aHash)] $ f aHash
    tag2Con :: Q Exp
    tag2Con = tag2ConExpr $ removeClassApp ty
data EnumClass = EnumClass
instance ClassRep EnumClass where
    arity _ = 0
    allowExQuant _ = True
    fullClassName _ = enumTypeName
    classConstraint _ 0 = Just $ enumTypeName
    classConstraint _ _ = Nothing
data EnumFun = Succ
             | Pred
             | ToEnum
             | FromEnum
             | EnumFrom
             | EnumFromThen
  deriving Show
enumFunName :: EnumFun -> Name
enumFunName Succ           = succValName
enumFunName Pred           = predValName
enumFunName ToEnum         = toEnumValName
enumFunName FromEnum       = fromEnumValName
enumFunName EnumFrom       = enumFromValName
enumFunName EnumFromThen   = enumFromThenValName
enumFromThenToExpr :: Q Exp -> Q Exp -> Q Exp -> Q Exp
enumFromThenToExpr f t1 t2 = varE enumFromThenToValName `appE` f `appE` t1 `appE` t2
illegalExpr :: String -> String -> String -> Q Exp
illegalExpr meth tp msg =
    varE errorValName `appE` stringE (meth ++ '{':tp ++ "}: " ++ msg)
illegalToEnumTag :: String -> Q Exp -> Name -> Q Exp
illegalToEnumTag tp maxtag a =
    appE (varE errorValName)
         (appE (appE (varE appendValName)
                     (stringE ("toEnum{" ++ tp ++ "}: tag(")))
               (appE (appE (appE
                 (varE showsPrecValName)
                 (integerE 0))
                 (varE a))
                 (appE (appE
                   (varE appendValName)
                   (stringE ") is outside of enumeration's range (0,"))
                   (appE (appE (appE
                         (varE showsPrecValName)
                         (integerE 0))
                         maxtag)
                         (stringE ")")))))