{-# LANGUAGE TemplateHaskell #-}

{- |

Generate enumeration data types with prefixed constructors but unprefixed
`Show` and `Read` instances.

> enumGenerate $ EnumDesc "Animal" ["Cat", "Dog", "Gopher"]

produces

> data Animal = AnimalCat | AnimalDog | AnimalGopher

yet `Read` and `Show` parse and print regular values:

> show AnimalDog == "Dog"
> (read "Cat" :: Animal) == AnimalCat

-}

module Data.THGen.Enum
  ( Exhaustiveness(..)
  , EnumDesc(..)
  , enumGenerate
  ) where

import           Control.Applicative
import           Control.DeepSeq
import           Control.Lens (over, _head, (<&>))
import           Control.Monad
import qualified Data.Char as C
import           Data.THGen.Compat
import           GHC.Generics (Generic)
import qualified Language.Haskell.TH as TH
import qualified Test.QuickCheck as QC
import qualified Text.Read as R


data Exhaustiveness = Exhaustive | NonExhaustive
  deriving (Eq, Ord, Show)

data EnumDesc = EnumDesc Exhaustiveness String [String]

funSimple :: TH.Name -> TH.ExpQ -> TH.DecQ
funSimple name body = TH.funD name [ TH.clause [] (TH.normalB body) [] ]

getC :: Char -> R.ReadPrec Char
getC c = mfilter (==c) R.get

skipSpaces :: R.ReadPrec String
skipSpaces = do
  n <- length . takeWhile C.isSpace <$> R.look
  replicateM n R.get

readRemaining :: R.ReadPrec String
readRemaining = many R.get

done :: R.ReadPrec String
done = mfilter null R.look

mangleEnumConName :: String -> String
mangleEnumConName
  = filter C.isAlphaNum
  . unwords
  . map (over _head C.toUpper)
  . words

enumGenerate :: EnumDesc -> TH.DecsQ
enumGenerate (EnumDesc exh strName strVals) = do
  let
    name       = TH.mkName strName
    vals       = strVals <&> \strVal ->
      TH.mkName (strName ++ mangleEnumConName strVal)
    unknownVal = TH.mkName ("Unknown" ++ strName)
  dataDecl <- do
    let
      constrs       = map (\val -> TH.normalC val []) vals
      unknownConstr = case exh of
        Exhaustive    -> []
        NonExhaustive ->
          [TH.normalC unknownVal [strictType [t|String|]]]
    dataD
      name
      (constrs ++ unknownConstr)
      ([''Eq, ''Ord, ''Generic] ++ if (exh == Exhaustive) then [''Enum, ''Bounded] else [])
  showInstDecl <- do
    unknownMatch <- case exh of
      Exhaustive    -> return []
      NonExhaustive -> do
        v <- TH.newName "str"
        return
          [ TH.match
              (TH.conP unknownVal [TH.varP v])
              (TH.normalB (TH.varE v))
              []
          ]
    let
      matches  = do
        (strVal, val) <- zip strVals vals
        return $ TH.match
          (TH.conP val [])
          (TH.normalB (TH.litE (TH.stringL strVal)))
          []
      showExpr = TH.lamCaseE (matches ++ unknownMatch)
    TH.instanceD
      (return [])
      [t|Show $(TH.conT name)|]
      [funSimple 'show showExpr]
  readInstDecl <- do
    let
      matches      = do
        (strVal, val) <- zip strVals vals
        let
          valE    = TH.conE val
          strValE = TH.litE (TH.stringL strVal)
        return $ [e|$valE <$ traverse getC ($strValE :: String) <* done|]
      unknownMatch = case exh of
        Exhaustive    -> [e|R.pfail|]
        NonExhaustive -> [e|$(TH.conE unknownVal) <$> readRemaining|]
      readPrecExpr =
        [e|skipSpaces >> (R.choice $(TH.listE matches) R.<++ $unknownMatch)|]
    TH.instanceD
      (return [])
      [t|Read $(TH.conT name)|]
      [funSimple 'R.readPrec readPrecExpr]
  arbInstance <- do
    let arbExpr = [e|QC.elements $(TH.listE (map TH.conE vals))|]
    TH.instanceD
      (return [])
      [t|QC.Arbitrary $(TH.conT name)|]
      [funSimple 'QC.arbitrary arbExpr]
  nfDataInst <- do
    TH.instanceD
      (return [])
      [t|NFData $(TH.conT name)|]
      []
  return [dataDecl, readInstDecl, showInstDecl, arbInstance, nfDataInst]