{-# LANGUAGE UndecidableInstances #-}

module Data.Aeson.Deriving.Internal.RecordSum where

import           Data.Aeson
import           Data.Aeson.Types    (Parser)
import           Data.Bifunctor      (first)
import           Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
import           Data.Kind           (Type)
import           Data.Proxy
import           GHC.Generics


newtype ParserMap a = ParserMap (HashMap String (Value -> Parser a))
  deriving stock Functor
  deriving newtype (Semigroup, Monoid)

unsafeMapKeys :: (String -> String) -> ParserMap a -> ParserMap a
unsafeMapKeys f (ParserMap hm)
  = ParserMap
  . HashMap.fromList
  . fmap (first f)
  $ HashMap.toList hm

-- | Provides a map from the (Haskell) constructor names of the inner contained types,
--   To parsers for the (Rep of the) outer data type that carries them.
class GTagParserMap (repA :: Type -> Type) where
  gParserMap :: Proxy repA -> ParserMap (repA x)

-- | We can create a ParserMap from any reference to a data type that has a
--   FromJSON instance (and at least one available constructor).
instance (GConstructorNames (Rep a), FromJSON a) => GTagParserMap (Rec0 a) where
  gParserMap _ = ParserMap . HashMap.fromList $ do
    constructorName <- gConstructorNames $ Proxy @(Rep a)
    [(constructorName, fmap K1 . parseJSON)]

-- | ParserMaps are trivially extended to the representation of fields under a constructor
instance GTagParserMap repA => GTagParserMap (S1 meta repA) where
  gParserMap _ = M1 <$> gParserMap (Proxy @repA)

-- | ParserMaps are extended to the canonical representation of a constructor.
--   Because there is no instance for :*:, this constraint is only satisfied for types
--   with a single constructor (with an S1 Directly under the C1 in the canonical rep).
instance GTagParserMap repA => GTagParserMap (C1 meta repA) where
  gParserMap _ = M1 <$> gParserMap (Proxy @repA)

-- | ParserMaps corresponding to different cases of a sum type are combined by merging.
instance (GTagParserMap repA, GTagParserMap repB) => GTagParserMap (repA :+: repB) where
  gParserMap _ =
       (L1 <$> gParserMap (Proxy @repA))
    <> (R1 <$> gParserMap (Proxy @repB))

-- | The ParserMap for the whole data type is now the one we get from under this D1 constructor.
instance GTagParserMap repA => GTagParserMap (D1 meta repA) where
  gParserMap _ = M1 <$> gParserMap (Proxy @repA)


-- | Provides constructor names
class GConstructorNames (repA :: Type -> Type) where
  gConstructorNames :: Proxy repA -> [String]

instance Constructor constructorMeta => GConstructorNames (C1 constructorMeta r) where
  gConstructorNames _ = [conName @constructorMeta undefined]

instance (GConstructorNames x, GConstructorNames y) => GConstructorNames (x :+: y) where
  gConstructorNames _ =
       gConstructorNames (Proxy @x)
    <> gConstructorNames (Proxy @y)

instance GConstructorNames r => GConstructorNames (D1 datatypeMeta r) where
  gConstructorNames _ = gConstructorNames $ Proxy @r