{-# LANGUAGE UndecidableInstances    #-}
{-# LANGUAGE UndecidableSuperClasses #-}
{-# OPTIONS_GHC -fno-warn-unused-binds #-}

module Servant.Zeppelin.Internal.Types
  ( DependencyList(..)
  , NamedDependency
  , SideLoaded(..)
  , Inflatable(..)
  , HasDependencies(..)
  , AllSatisfy
  , Inflatable'
  , Full'
  ) where

import           Data.Functor.Identity   (Identity)
import           Data.Kind
import           Data.Singletons.Prelude

--------------------------------------------------------------------------------
-- Dependency Lists
--------------------------------------------------------------------------------

-- | 'DependencyList' @m bs fs@ is a type representing a heterogeneous list parameterized
-- by @bs@ , which can be transformed into a hetergeneous list of type @fs@ in the context
-- provided by @m@.
data DependencyList :: (* -> *) -> [*] -> [*] -> * where
  NilDeps :: DependencyList m '[] '[]
  (:&:) :: b -> DependencyList m bs fs -> DependencyList m (b:bs) (f:fs)

infixr 5 :&:

instance AllSatisfy bs Show' => Show (DependencyList m bs fs) where
  show NilDeps    = "NilDeps"
  show (b :&: bs) = show b ++ " :&: " ++ show bs

instance AllSatisfy bs Eq' => Eq (DependencyList m bs fs) where
  NilDeps == NilDeps = True
  (b :&: bs) == (b' :&: bs') = b == b' && bs == bs'

-- | Labels for the objects created in the dependency mapping. Necessary for JSON instances.
--
-- > type instance NamedDependency Person = "person"
-- > type instance NamedDependency [Photo] = "photos"
--
type family NamedDependency a :: Symbol

-- | 'SideLoaded' @a deps@ represents a type @a@ with an hlist of its inflated dependencies.
data SideLoaded a (deps :: [*]) = SideLoaded a (DependencyList Identity deps deps)

deriving instance (Show a, Show (DependencyList Identity deps deps)) => Show (SideLoaded a deps)
deriving instance (Eq a, Eq (DependencyList Identity deps deps)) => Eq (SideLoaded a deps)

-- | Inflatable represents a type 'b' which can be expanded inside of a context 'm'.
--
-- > type PGMonad = ReaderT Connection (ExceptT QueryError IO)
-- >
-- > instance Inflatable PGMonad PersonId where
-- >   type Full PGMonad PersonId = Person
-- >   inflator = getPersonById

class Inflatable m base where
  type Full m base :: *
  inflator :: base -> m (Full m base)

-- | Anything can be expanded into itself in the trivial context.
instance Inflatable Identity base where
  type Full Identity base = base
  inflator = return

--------------------------------------------------------------------------------
-- HasDepedencies
--------------------------------------------------------------------------------

-- | Indicate that a type has dependencies, and supply the uninflated values.
--
-- > data Album =
-- >   Album { albumId     :: AlbumId
-- >         , albumArtist :: PersonId
-- >         , albumPhotos :: [PhotoId]
-- >         , albumTitle  :: Text
-- >         }
-- >
-- > instance HasDependencies PGMonad Album '[Person, [PhotoId]] where
-- >   getDependencies album = albumArtist album :&: albumPhotos album :&: NilDeps

class AllSatisfy bs (Inflatable' m) => HasDependencies m a bs | a -> bs, bs -> m where
  getDependencies :: a -> DependencyList m bs (Map (Full' m) bs)

--------------------------------------------------------------------------------
-- Type Families
--------------------------------------------------------------------------------

-- | All subjects must satisfy the test constraint.
type family AllSatisfy (subjects :: [k]) (test :: (k ~> Constraint)) :: Constraint where
  AllSatisfy '[] test = ()
  AllSatisfy (subj : rest) test = (Apply test subj, AllSatisfy rest test)

-- | Parially applied 'Inflatable' constraint.
data Inflatable' :: m -> (base ~> Constraint) where
  Inflatable' :: Inflatable' m base

type instance Apply (Inflatable' m) base = Inflatable m base

-- | Defunctionalized 'Full' type family to be used with 'Map'.
data Full' :: m -> (base ~> *) where
  Full' :: Full' m base

type instance Apply (Full' m) base = Full m base

data Eq' :: b ~> Constraint where
  Eq' :: Eq' b

type instance Apply Eq' b = Eq b

data Show' :: b ~> Constraint where
  Show' :: Show' b

type instance Apply Show' b = Show b