module Calamity.Internal.AesonThings
    ( WithSpecialCases
    , WithSpecialCasesInner(..)
    , type IfNoneThen
    , type ExtractField
    , type ExtractFields
    , type InjectID
    , SpecialRule
    , DefaultToEmptyArray
    , DefaultToZero
    , DefaultToFalse
    , CalamityJSON(..)
    , jsonOptions
    , jsonOptionsKeepNothing ) where

import           Control.Lens

import           Data.Aeson
import           Data.Aeson.Lens
import           Data.Aeson.Types      ( Parser )
import           Data.Kind
import           Data.Reflection       ( Reifies(..) )
import           Data.Text             ( Text )
import           Data.Text.Strict.Lens
import           Data.Typeable

import           GHC.Generics
import qualified GHC.TypeLits          as TL
import           GHC.TypeLits          ( KnownSymbol, Symbol, symbolVal )

textSymbolVal :: forall n. KnownSymbol n => Proxy n -> Text
textSymbolVal _ = symbolVal @n Proxy ^. packed

data SpecialCaseList
  = SpecialCaseNil
  | forall label action inner. SpecialCaseElem label action inner

data SpecialRule (label :: Symbol) (action :: SpecialRuleAction)

data SpecialRuleAction
  = forall d. IfNoneThen d
  | forall field. ExtractField field
  | forall field. ExtractFields field
  | forall mn idn. InjectID idn mn

type IfNoneThen label d =
  SpecialRule label ('IfNoneThen d)

type ExtractField label field =
  SpecialRule label ('ExtractField field)

type ExtractFields label field =
  SpecialRule label ('ExtractFields field)

type InjectID label mn idn =
  SpecialRule label ('InjectID mn idn)

class PerformAction (action :: SpecialRuleAction) where
  runAction :: Proxy action -> Value -> Parser Value

instance Reifies d Value => PerformAction ('IfNoneThen d) where
  runAction _ Null = pure $ reflect @d Proxy
  runAction _ x = pure x

instance (KnownSymbol field) => PerformAction ('ExtractField field) where
  runAction _ Null = pure Null
  runAction _ o = withObject (("extracting field " <> textSymbolVal @field Proxy) ^. unpacked)
    (.: textSymbolVal @field Proxy) o

instance (KnownSymbol field) => PerformAction ('ExtractFields field) where
  runAction _ Null = pure Null
  runAction _ o = withArray (("extracting fields " <> textSymbolVal @field Proxy) ^. unpacked)
    ((Array <$>) . traverse (withObject "extracting field" (.: textSymbolVal @field Proxy))) o

instance (KnownSymbol idn, KnownSymbol mn) => PerformAction ('InjectID idn mn) where
  runAction _ = withObject
    (("injecting id from " <> textSymbolVal @idn Proxy <> " into " <> textSymbolVal @mn Proxy) ^. unpacked) $ \o -> do
      id <- o .: "id"

      pure (Object o
            & key (textSymbolVal @mn Proxy) . values . _Object . at (textSymbolVal @idn Proxy) ?~ id)

type family FoldSpecialCases (rules :: [Type]) :: SpecialCaseList where
  FoldSpecialCases '[]                              = 'SpecialCaseNil
  FoldSpecialCases (SpecialRule label action ': xs) = 'SpecialCaseElem label action (FoldSpecialCases xs)
  FoldSpecialCases _ = TL.TypeError ('TL.Text "What did you do?")

newtype WithSpecialCasesInner (rules :: SpecialCaseList) a = WithSpecialCasesInner
  { unwrapWithSpecialCases :: a
  }

type family WithSpecialCases rules a :: Type where
  WithSpecialCases rules a = WithSpecialCasesInner (FoldSpecialCases rules) a

class RunSpecialCase a where
  runSpecialCases :: Proxy a -> Object -> Parser Object

instance RunSpecialCase 'SpecialCaseNil where
  runSpecialCases _ = pure . id

instance (RunSpecialCase inner, KnownSymbol label, PerformAction action)
  => RunSpecialCase ('SpecialCaseElem label action inner) where
  runSpecialCases _ o = do
    o' <- runSpecialCases (Proxy @inner) o
    v <- o' .:? textSymbolVal @label Proxy .!= Null
    v' <- runAction (Proxy @action) v
    pure (o' & at (textSymbolVal @label Proxy) ?~ v')

instance (RunSpecialCase rules, Typeable a, Generic a, GFromJSON Zero (Rep a))
  => FromJSON (WithSpecialCasesInner rules a) where
  parseJSON = withObject (show . typeRep $ Proxy @a) $ \o -> do
    o' <- runSpecialCases (Proxy @rules) o
    WithSpecialCasesInner <$> genericParseJSON jsonOptions (Object o')


data DefaultToEmptyArray

instance Reifies DefaultToEmptyArray Value where
  reflect _ = Array mempty

data DefaultToZero

instance Reifies DefaultToZero Value where
  reflect _ = Number 0

data DefaultToFalse

instance Reifies DefaultToFalse Value where
  reflect _ = Bool False

newtype CalamityJSON a = CalamityJSON
  { unCalamityJSON :: a
  }

instance (Typeable a, Generic a, GToJSON Zero (Rep a), GToEncoding Zero (Rep a)) => ToJSON (CalamityJSON a) where
  toJSON = genericToJSON jsonOptions . unCalamityJSON

  toEncoding = genericToEncoding jsonOptions . unCalamityJSON

instance (Typeable a, Generic a, GFromJSON Zero (Rep a)) => FromJSON (CalamityJSON a) where
  parseJSON = fmap CalamityJSON . genericParseJSON jsonOptions

jsonOptions :: Options
jsonOptions = defaultOptions { sumEncoding        = UntaggedValue
                             , fieldLabelModifier = camelTo2 '_' . filter (/= '_')
                             , omitNothingFields  = True }

jsonOptionsKeepNothing :: Options
jsonOptionsKeepNothing = defaultOptions { sumEncoding        = UntaggedValue
                                        , fieldLabelModifier = camelTo2 '_' . filter (/= '_')
                                        , omitNothingFields  = False }