{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}

-- |
-- The data types creator of exceptions in the compile type.
--
-- It has the record of "cause" and "clue".
module Control.Exception.Throwable.TH
  ( declareException
  ) where

import Data.Char (toLower, isUpper, isPunctuation, isNumber)
import Language.Haskell.TH ( Bang(..), DecsQ, newName, mkName, Dec(..), TyVarBndr(..), Con(..), Type(..), SourceUnpackedness(..), SourceStrictness(..), Q, Name, Exp(..), Lit(..), Pat(..), Clause(..), Body(..))

-- |
-- Mean names of a data type and its value constructors
--
-- @
-- data IOException' a =
--   IOException'
--     { ioExceptionCause :: String
--     , ioExceptionClue  :: a
--     } |
--   FileNotFoundException
--     { fileNotFoundExceptionCause :: String
--     , fileNotFoundExceptionClue  :: a
--     }
-- ==>
--   DatatypeNames
--     { typeName     = mkName "IOException'"
--     , constructors =
--       [ ValueConstructor
--         { constrName      = "IOException'"
--         , causeRecordName = mkName "ioExceptionCause"
--         , clueRecordName  = mkName "ioExceptionClue"
--         }
--       , ValueConstructor
--         { constrName      = "FileNotFoundException"
--         , causeRecordName = mkName "fileNotFoundExceptionCause"
--         , clueRecordName  = mkName "fileNotFoundExceptionClue"
--         }
--       ]
--     }
-- @
data DatatypeNames = DatatypeNames
  { datatypeName :: DatatypeName
  , constructors :: [ValueConstructor]
  } deriving (Show)

-- | Mean a name of a value constructor and its records
data ValueConstructor = ValueConstructor
  { constructorName :: ValueConstructorName
  , causeRecordName :: String
  , clueRecordName  :: String
  } deriving (Show)

type DatatypeName = String
type ValueConstructorName = String


-- |
-- Create the value of @DatatypeNames@ by the type name.
-- That type name will be executed the instantiation for @Exception@.
--
-- @typeName@ and @constrNames@ elements must be non empty string.
-- If it is given, create Nothing.
getDatatypeNames :: DatatypeName -> [ValueConstructorName] -> Maybe DatatypeNames
getDatatypeNames "" _                 = Nothing
getDatatypeNames typeName constrNames = fmap (DatatypeNames typeName) $ mapM toValueConstructorName constrNames
  where
    toValueConstructorName :: ValueConstructorName -> Maybe ValueConstructor
    toValueConstructorName [] = Nothing
    toValueConstructorName constrName@(c:onstrName) =
      let constrName' = (toLower c) : onstrName -- camelCase
      in Just ValueConstructor { constructorName = constrName
                               , causeRecordName = constrName' ++ "Cause"
                               , clueRecordName  = constrName' ++ "Clue"
                               }


-- | A natural strategy of the evaluation
noBang :: Bang
noBang = Bang NoSourceUnpackedness NoSourceStrictness


-- |
-- Declare simple concrete exception data type in the compile time.
--
-- If the empty list is given to @constrNames@, create empty data type.
--
-- @typeName@ and @constrNames@ must be PascalCase
-- (e.g> "IOException'", "IndexOutOfBoundsException". NG> "ioException'", "indexOutOfBoundsException") .
declareException :: DatatypeName -> [ValueConstructorName] -> DecsQ
declareException typeName constrNames = do
  case getDatatypeNames typeName constrNames of
    Nothing        -> fail "Data.Exception.Throwable.TH.declareException requires non empty string for `typeName`"
    Just typeNames -> do
      typeParam <- newName "a" -- type `a` of `data FooException a`
      let dataDec = defineDatatype typeNames typeParam
      showInstanceDec     <- defineShowInstanceFor typeNames
      exceptionInstancDec <- defineExceptionInstanceFor typeNames
      fakeConstructorDecs <- mapM defineFakeConstructor $ constructors typeNames
      return $ [ dataDec
               , showInstanceDec
               , exceptionInstancDec
               ] ++ fakeConstructorDecs
  where
    -- Define a data of an exception.
    -- the data is defined by @DatatypeNames@.
    defineDatatype :: DatatypeNames -> Name -> Dec
    defineDatatype (DatatypeNames {..}) a =
      let exception   = mkName datatypeName
          constrsCons = map (flip makeValueConstructorsCon a) constructors
      in DataD []
        exception [PlainTV a] Nothing
        constrsCons []

    -- Make @Con@ from @ValueConstructor@ for @defineDatatype@
    -- @a@ is @defineDatatype@'s datatype's type parameter.
    makeValueConstructorsCon :: ValueConstructor -> Name -> Con
    makeValueConstructorsCon (ValueConstructor {..}) a =
      let constructor = mkName constructorName
          causeRecord = mkName causeRecordName
          clueRecord  = mkName clueRecordName
      in RecC constructor [ (causeRecord, noBang, ConT $ mkName "String")
                          , (clueRecord, noBang, VarT a)
                          ]

    -- Define an instance of a data of @DatatypeNames@ for @Show@.
    defineShowInstanceFor :: DatatypeNames -> Q Dec
    defineShowInstanceFor dataTypeNames@(DatatypeNames {..}) = do
      let showClass = mkName "Show"
          exception = mkName typeName
      a <- newName "a"
      showFuncDec <- declareShowFunc dataTypeNames
      return $ InstanceD Nothing
        [AppT (ConT showClass) (VarT a)]
        (AppT (ConT showClass) (AppT (ConT exception) (VarT a)))
        [showFuncDec]

    -- Make a @show@ function definition.
    declareShowFunc :: DatatypeNames -> Q Dec
    declareShowFunc DatatypeNames {..} = do
      let showFunc = mkName "show"
      showFuncClauses <- mapM (flip makeShowFuncClause showFunc) constructors
      return $ FunD showFunc showFuncClauses
      where
        -- Make patterns of @show@ function (@showFunc@) implementation,
        -- for @constructors@ of @DatatypeNames@.
        makeShowFuncClause :: ValueConstructor -> Name -> Q Clause
        makeShowFuncClause (ValueConstructor {..}) showFunc = do
          let constructor = mkName constructorName
          cause <- newName "cause"
          return $ Clause [ConP constructor [VarP cause, WildP]] -- (FooException cause _) =
            (NormalB
                (InfixE (Just . LitE . StringL $ typeName ++ ": ") (VarE $ mkName "++") (Just $ AppE (VarE showFunc) (VarE cause))) -- show ("FooException: " ++ show cause)
            ) []

    -- Define an instance of a data of @DatatypeNames@ for @Exception@.
    defineExceptionInstanceFor :: DatatypeNames -> Q Dec
    defineExceptionInstanceFor DatatypeNames {..} = do
      let typeableClass  = mkName "Typeable"
          showClass      = mkName "Show"
          exceptionClass = mkName "Exception"
          exception      = mkName typeName
      a <- newName "a"
      return $ InstanceD Nothing
        [ AppT (ConT typeableClass) (VarT a)
        , AppT (ConT showClass) (VarT a)
        ]
        (AppT (ConT exceptionClass) (AppT (ConT exception) (VarT a)))
        []

    -- Define the casual data constructor.
    -- Like @Control.Exception.Throwable.generalException@ without name field.
    defineFakeConstructor :: ValueConstructor -> Q Dec
    defineFakeConstructor ValueConstructor {..} = do
      let fConstructor = mkName $ pascalToCamelCase constructorName
          constructor  = mkName constructorName
      a <- newName "a"
      return $ FunD fConstructor [
          Clause [VarP a] (NormalB $ AppE (AppE (ConE constructor) (VarE a)) (TupE []))
          []
        ]


-- |
-- Convert string of 'PascalCase' to 'camelCase'.
-- If no 'PascalCase' string or multi byte string is given, undefined behavior is happended.
--
-- >>> pascalToCamelCase "IOException"
-- "ioException"
-- >>> pascalToCamelCase "IOException'"
-- "ioException'"
-- >>> pascalToCamelCase "FOO2BarException"
-- "foO2BarException"
-- >>> pascalToCamelCase "I2SException"
-- "i2SException"
-- >>> pascalToCamelCase "Foo2BarException"
-- "foo2BarException"
-- >>> pascalToCamelCase "IndexOutOfBoundsException"
-- "indexOutOfBoundsException"
-- >>> pascalToCamelCase "IllegalArgumentException'"
-- "illegalArgumentException'"
-- >>> pascalToCamelCase "Exception1"
-- "exception1"
pascalToCamelCase :: String -> String
pascalToCamelCase pascalCase
  | upperIsCont pascalCase  = forUpperContCase pascalCase  -- e.g. "IOException'", "FOOBarException", "FOO2BarException"
  | signInMiddle pascalCase = forSignMiddleCase pascalCase -- e.g. "I2SException", "Foo2BarException" ("Exception1" is not)
  | otherwise               = forCasualCase pascalCase     -- e.g. "IndexOutOfBoundsException", "Exception1",
  where
    -- Are the upper characters continuing ?
    upperIsCont :: String -> Bool
    upperIsCont ""      = False
    upperIsCont (_:a:_) = isUpper a
    upperIsCont (_:_)   = False

    -- Is the sign put in the middle ?
    signInMiddle :: String -> Bool
    signInMiddle = any isNumber . init . tail

    forUpperContCase :: String -> String
    forUpperContCase pascalCase =                                      -- IOException'
      let (pascalCase', signs) = span (not . isPunctuation) pascalCase -- ("IOException", "'")
          (p:ascalCase)        = pascalCase'                           -- ('I':"OException)
          p'                   = toLower p                             -- 'i'
          (ascal, case_)       = span isUpper ascalCase                -- ("OE", "xception")
          (l:acsa)             = reverse ascal                         -- ('E':"O")
          asca                 = map toLower $ reverse acsa            -- "o"
      in p':asca ++ l:case_ ++ signs                                   -- ('i':"o") ++ ('E':xception) ++ "'" == "ioException'" ;P

    forSignMiddleCase :: String -> String
    forSignMiddleCase pascal0Case =
      let (pascal, _0Case) = span (not . isNumber) pascal0Case
          pascal'          = map toLower pascal
      in pascal' ++ _0Case

    forCasualCase :: String -> String
    forCasualCase pascalCase =
      let (p:ascalCase) = pascalCase
          p'            = toLower p
      in p' : ascalCase