module Composite.Aeson.Enum where

import Control.Monad.Error.Class (throwError)
import Composite.Aeson.Base (JsonFormat(JsonFormat), JsonProfunctor(JsonProfunctor))
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.BetterErrors as ABE
import qualified Data.HashMap.Strict as HM
import Data.List (intercalate, stripPrefix)
import qualified Data.Map.Strict as M
import Data.Text (Text, pack, unpack)
import GHC.Generics (Generic(type Rep))
import Generics.Deriving.ConNames (ConNames, conNameOf)
import Generics.Deriving.Enum (Enum', genumDefault)

-- |For some type @a@ which represents an enumeration (i.e. all nullary constructors) generate a 'JsonFormat' which maps that type to strings in JSON.
--
-- Each constructor will be mapped to a string with the same value as its name with some prefix removed.
--
-- For example, given:
--
-- > data MyEnum = MyEnumFoo | MyEnumBar
-- > myEnumFormat :: JsonFormat e MyEnum
-- > myEnumFormat = enumJsonFormat "MyEnum"
--
-- Then:
--
-- > toJsonWithFormat myEnumFormat MyEnumFoo == Aeson.String "Foo"
enumJsonFormat :: forall e a. (Show a, Ord a, Generic a, ConNames (Rep a), Enum' (Rep a)) => String -> JsonFormat e a
enumJsonFormat prefix =
  let removePrefix s
        | Just suffix <- stripPrefix prefix s = suffix
        | otherwise                           = s
      values = genumDefault
      names = map (pack . removePrefix . conNameOf) values
      lookupText  = flip HM.lookup . HM.fromList $ zip names values
      lookupValue = flip  M.lookup .  M.fromList $ zip values names
      expectedValues = "one of " ++ (intercalate ", " . map unpack $ names)
  in enumMapJsonFormat lookupText lookupValue expectedValues

-- |For some type @a@ which bidirectional mapping functions can be provided, produce a 'JsonFormat' which maps to JSON strings.
enumMapJsonFormat :: Show a => (Text -> Maybe a) -> (a -> Maybe Text) -> String -> JsonFormat e a
enumMapJsonFormat lookupText lookupValue expectedText = JsonFormat $ JsonProfunctor toJson fromJson
  where
    toJson a =
      case lookupValue a of
        Nothing -> error $ "unrecognized enum value " ++ show a -- eugh
        Just t  -> Aeson.String t

    fromJson = do
      t <- ABE.asText
      case lookupText t of
        Nothing -> throwError $ ABE.BadSchema [] $ ABE.FromAeson $
                     "expected " ++ expectedText ++ ", not " ++ unpack t
        Just v  -> pure v