{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts          #-}
{-# LANGUAGE FlexibleInstances         #-}
{-# LANGUAGE GADTs                     #-}
{-# LANGUAGE LambdaCase                #-}
{-# LANGUAGE MultiParamTypeClasses     #-}
{-# LANGUAGE RankNTypes                #-}
{-# LANGUAGE ScopedTypeVariables       #-}
{-# LANGUAGE TypeFamilies              #-}
{-# LANGUAGE TypeOperators             #-}
{-# LANGUAGE TypeSynonymInstances      #-}

module Data.Schema.Internal.Types where

import           Control.Applicative.Free
import           Control.Functor.HigherOrder
import           Control.Lens                hiding (iso)
import qualified Control.Lens                as Lens
import           Control.Natural
import           Data.Functor.Invariant
import           Data.List.NonEmpty          (NonEmpty)
import           Data.Profunctor
import           Data.Text                   (Text)
import qualified Data.Text                   as T
import           Data.Vector                 (Vector)
import           Prelude                     hiding (const, seq)

-- | Metadata for a field of type `a`, belonging to the data type `o` and based on schema `s`
data FieldDef o s a where
  RequiredField :: Text -> s a -> Getter o a -> FieldDef o s a
  OptionalField :: Text -> s a -> Getter o (Maybe a) -> FieldDef o s (Maybe a)

fieldName :: FieldDef o s a -> Text
fieldName (RequiredField name _ _) = name
fieldName (OptionalField name _ _) = name

instance HFunctor (FieldDef o) where
  hfmap nt = \case
    RequiredField name s acc -> RequiredField name (nt s) acc
    OptionalField name s acc -> OptionalField name (nt s) acc

-- | The type of a field of type `a`, belonging to the data type `o` and based on schema `s`
newtype Field s o a = Field { unwrapField :: Ap (FieldDef o s) a }

hoistField :: (m ~> n) -> Field m o a -> Field n o a
hoistField nt (Field ap) = Field $ hoistAp (hfmap nt) ap

-- | The set of fields for the data type `o` based on schema `s`
type Fields s o = Field s o o

instance Functor (Field s o) where
  fmap f (Field x) = Field $ fmap f x

instance Applicative (Field s o) where
  pure x = Field $ Pure x
  (Field x) <*> (Field y) = Field (x <*> y)

instance Profunctor (Field s) where
  lmap f (Field ap) = Field $ hoistAp (contraNT f) ap
    where contraNT :: (n -> o) -> FieldDef o s ~> FieldDef n s
          contraNT f = \case
            RequiredField n s g -> RequiredField n s ((to f) . g)
            OptionalField n s g -> OptionalField n s ((to f) . g)
  rmap = fmap

-- | Define a field
field :: Text -> s a -> Getter o a -> Field s o a
field name schema getter = Field $ liftAp (RequiredField name schema getter)

optional :: Text -> s a -> Getter o (Maybe a) -> Field s o (Maybe a)
optional name schema getter = Field $ liftAp (OptionalField name schema getter)

-- | Metadata for an alternative of type `a` based on schema `s`
data AltDef s a = forall b. AltDef
  { altName   :: Text
  , altSchema :: s b
  , altPrism  :: Prism' a b
  }

instance HFunctor AltDef where
  hfmap nt = \(AltDef name schema pr) -> AltDef name (nt schema) pr

-- | Metadata for a schema `s` based on primitives `p` and representing type `a`
data SchemaF p s a where
  PrimitiveSchema :: p a -> SchemaF p s a
  RecordSchema    :: Fields s a -> SchemaF p s a
  UnionSchema     :: NonEmpty (AltDef s a) -> SchemaF p s a
  AliasSchema     :: s a -> Iso' a b -> SchemaF p s b

instance HFunctor (SchemaF p) where
  hfmap nt = \case
    PrimitiveSchema p         -> PrimitiveSchema p
    RecordSchema (Field flds) -> RecordSchema . Field $ hoistAp (hfmap nt) flds
    UnionSchema alts          -> UnionSchema $ fmap (hfmap nt) alts
    AliasSchema base iso      -> AliasSchema (nt base) iso

-- | The Schema type itself for a set of primitives `p`
newtype Schema p a = Schema { unwrapSchema :: HFix (SchemaF p) a }

instance Invariant (Schema p) where
  invmap f g sch = case (unfix . unwrapSchema $ sch) of
    AliasSchema base iso -> Schema . HFix $ AliasSchema base (iso . (Lens.iso f g))
    otherwise            -> Schema . HFix $ AliasSchema (unwrapSchema sch) (Lens.iso f g)

-- | An Schema has a HFunctor that performs a natural transformation of the primitive algebra of the Schema
instance HFunctor Schema where
  hfmap nt (Schema fsch) = Schema $ cataNT pfmapAlg fsch
    where pfmapAlg = wrapNT $ \sch -> HFix $ pfmap nt sch

          pfmap :: (p ~> q) -> SchemaF p s ~> SchemaF q s
          pfmap nt = \case
            PrimitiveSchema p    -> PrimitiveSchema (nt p)
            RecordSchema fields  -> RecordSchema fields
            UnionSchema alts     -> UnionSchema alts
            AliasSchema base iso -> AliasSchema base iso

class HasSchema a where
  type PrimitivesOf a :: * -> *

  getSchema :: Schema (PrimitivesOf a) a