packages feed

ohhecs-0.0.1: src/Games/ECS/Component/TH/Internal.hs

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE Trustworthy #-}

-- |
-- Module      :  Games.ECS.Component.TH.Internal
-- Description : Template Haskell derivation of Component classes
-- Copyright   :  (C) 2020 Sophie Taylor
-- License     :  AGPL-3.0-or-later
-- Maintainer  :  Sophie Taylor <sophie@spacekitteh.moe>
-- Stability   :  experimental
-- Portability: GHC
--
-- Implements helper classes and constraints on components.
module Games.ECS.Component.TH.Internal
  ( makeHasComponentClass,
    makeComponentAccessor,
    makeConstructorPatternAndValuePair,
  )
where

import Control.Lens
import Data.Char
import Data.List
import Data.Maybe (fromMaybe)
import Games.ECS.Component
import Games.ECS.Entity
import Games.ECS.Slot
import Games.ECS.World
import Language.Haskell.TH
import Language.Haskell.TH.Datatype qualified as D
import Language.Haskell.TH.Syntax

-- | Forms the accessor optic based on the properties of the component.
accessorBuilder :: (Quote m) => m Type -> m Type -> m Type -> m Type -> m Exp
accessorBuilder hkd name s a = [|runIndexedTraversal $ accessor @($name) @($hkd) @($s) @(Prop $a) @($a)|]

normalName :: Name
normalName = 'Normal

uniqueName :: Name
uniqueName = 'Unique

requiredName :: Name
requiredName = 'Required

-- | Computes the name for a \"classy optics\" style class.
makeHasComponentClassName :: String -> Name
makeHasComponentClassName name = mkName $ "Has" ++ [toUpper (head name)] ++ tail name

-- | Computes the name for the  \"Using\" type synonyms.
makeUsingComponentClassName :: String -> Name
makeUsingComponentClassName name = mkName $ "Using" ++ [toUpper (head name)] ++ tail name

-- | Creates the \"classy optics\" class, and the \"Using\" type synonym, matching a 'Component'.
makeHasComponentClass :: (MonadFail m, Quote m, Quasi m) => Name -> m [Dec]
makeHasComponentClass componentName = do
  componentType <- conT componentName
  -- Look up the "CanonicalName" associated type to get the standard name used to reference the component
  componentInstance <- runQ $ reifyInstances ''Games.ECS.Component.CanonicalName [componentType]
  case componentInstance of
    [] -> fail ("No Component.CanonicalName instance found for " ++ (show componentName) ++ "!")
    [_canonicalNameDec@(TySynInstD (TySynEqn _ _ canonicalNameType@(LitT (StrTyLit canonicalName))))] -> do
      let hasComponentClassName = makeHasComponentClassName canonicalName
      let worldTypeName = mkName "worldType"
      -- We need this type var for the HKD optics lookup fuckery
      let s = varT $ mkName "s"
      worldType <- varT worldTypeName
      let worldType' = pure worldType
          componentType' = pure componentType
          canonicalNameType' = pure canonicalNameType

      classSignatures <- makeRawComponentAccessorSignatures (pure worldType) canonicalNameType (pure componentType)
      -- Not sure we really need /all/ of this, but it works.
      classContext <-
        [t|
          ( World $worldType',
            Component $componentType',
            EntityProperty $canonicalNameType' $worldType' Individual (Prop $componentType') $componentType',
            OpticsFor $canonicalNameType' $worldType' Storing (Prop $componentType') $componentType'
              ~ ReifiedIndexedTraversal' Entity ($worldType' Storing) $componentType'
          )
          |]
      -- Declare the actual "Has<xyz>" class, with the adder and remover functions, and the accessing optic
      classDec <- classD (pure [classContext]) hasComponentClassName [PlainTV worldTypeName BndrReq] [] (fmap pure classSignatures)

      -- Create the "Using<xyz>" type synonym.
      useComponentDecType <-
        [t|
          ( $(conT hasComponentClassName) $worldType',
            EntityProperty $canonicalNameType' $worldType' $s (Prop $componentType') $componentType',
            OpticsFor $canonicalNameType' $worldType' $s (Prop $componentType') $componentType'
              ~ ReifiedIndexedTraversal' Entity ($worldType' $s) $componentType'
          )
          |]
      -- The "Using<xyz>" type synonym has two type parameters: The world type, and the storage variable. The
      -- storage variable depends on whether you're accessing individual entity components or the storage of
      -- components.
      useComponentDec <- tySynD (makeUsingComponentClassName canonicalName) [PlainTV worldTypeName BndrReq, PlainTV (mkName "s") BndrReq] (pure useComponentDecType)
      pure [classDec, useComponentDec]
    _ -> fail ("Error while processing Component.CanonicalName instance for " ++ show componentName ++ "!")

-- | Create the accessing functions/optic signatures for a given component.
makeRawComponentAccessorSignatures :: (MonadFail m, Quote m) => m Type -> Type -> m Type -> m [Dec]
makeRawComponentAccessorSignatures worldType canonicalNameType'@(LitT (StrTyLit name')) componentType = do
  let nameType = pure canonicalNameType'
      -- The component type
      s' = mkName "s"
      s = varT s'
      namedOptic = mkName name'

  entitiesWithSig <-
    sigD
      (makeWithName name')
      [t|
        Control.Lens.Fold ($worldType Storing) IntersectionOfEntities
        |]
  adderSig <-
    sigD
      (makeAdderName name')
      [t|
        Control.Lens.IndexedSetter' Entity ($worldType Individual) $componentType
        |]

  removerSig <-
    sigD
      (makeRemoverName name')
      [t|
        $worldType Individual -> $worldType Individual
        |]
  context <-
    [t|
      ( EntityProperty $nameType $worldType $s (Prop $componentType) $componentType,
        OpticsFor $nameType $worldType $s (Prop $componentType) $componentType
          ~ ReifiedIndexedTraversal' Entity ($worldType $s) $componentType
      )
      |]
  theSignature <-
    sigD
      namedOptic
      ( forallT
          [PlainTV s' inferredSpec]
          (pure [context])
          [t|
            IndexedTraversal' Entity ($worldType $s) $componentType
            |]
      )

  pure [theSignature, adderSig, removerSig, entitiesWithSig]
makeRawComponentAccessorSignatures _ _ _ = fail "Mischief afoot with makeRawComponentAccessorSignatures"

-- | Computes  the name for a component remover function, with a given postfix.
makeRemoverName' :: String -> String -> Name
makeRemoverName' postfix name =
  mkName $ "remove" ++ [toUpper (head name)] ++ tail name ++ postfix

-- | Computes the name for a component remover function.
makeRemoverName :: String -> Name
makeRemoverName = makeRemoverName' ""

-- | Computes the name for a component addition function, with a given postfix.
makeAdderName' :: String -> String -> Name
makeAdderName' postfix name = mkName $ "add" ++ [toUpper (head name)] ++ tail name ++ postfix

-- | Computes the name for a component addition function.
makeAdderName :: String -> Name
makeAdderName = makeAdderName' ""

-- | Computes the name for a "withComponent" function, with a given postfix.
makeWithName' :: String -> String -> Name
makeWithName' postfix name = mkName $ "with" ++ [toUpper (head name)] ++ tail name ++ postfix

makeWithName :: String -> Name
makeWithName = makeWithName' ""

-- | Make the instance for the \"classy optics\" for a given world and canonically named component. We make
-- the bodies for the optics for accessing components, as well as (law-breaking?) setters that create them if
-- they're missing.
makeComponentAccessor :: (Quasi m, Quote m) => D.ConstructorInfo -> m Type -> (Type, Type, Type) -> m [Dec]
makeComponentAccessor constructorInfo worldType tys@(nameType'@(LitT (StrTyLit name')), a', _) = do
  let nameType = pure nameType'
      -- name = pure @Q name'
      a = pure a' -- The component type
      s' = mkName "s"
      s = varT s'
      namedOptic = mkName name'
      built = accessorBuilder worldType nameType s a
  -- fieldType <- [t|Field $nameType $s (Prop $a) $a|]
  sigs <- makeRawComponentAccessorSignatures worldType nameType' a
  theBody <- funD namedOptic [clause [] (normalB built) []]

  let adderPat = varP (makeAdderName name')
      removerPat = varP (makeRemoverName name')
      withPat = varP (makeWithName name')

  let setterPat =
        [|
          setTyped @(Field $nameType Individual (Prop $a) $a) @($worldType Individual) . injectToField @($nameType) @($worldType) @Individual @(Prop $a)
          |]
  let removerLambda =
        [|
          setTyped @(Field $nameType Individual (Prop $a) $a) @($worldType Individual) (defaultField @($nameType) @($worldType) @Individual @(Prop $a))
          |]

  adderInlinePragma <- pragInlD (makeAdderName name') Inline FunLike AllPhases
  removerInlinePragma <- pragInlD (makeRemoverName name') Inline FunLike AllPhases
  withComonentInlinePragma <- pragInlD (makeWithName name') Inline FunLike AllPhases
  opticInlinePragma <- pragInlD namedOptic Inline ConLike AllPhases

  {-  clset <- [|set|]
    clview <- [|view |]
    let   worldRuleBinderType = typedRuleVar (mkName "w") (appT worldType s)
          sRuleType = typedRuleVar s' s
          opticAccessorForRule = varE namedOptic
          wName = mkName "w"
          lhsExpr = AppE (AppE (AppE clset (VarE namedOptic)) (AppE (AppE clview (VarE namedOptic)) (VarE wName))) (VarE wName)
                          --(appE (appE [|set|] ( [|($opticAccessorForRule)|])) [|(view ($opticAccessorForRule) w) w|])
    opticRule <- pragRuleD (name' ++ "/set . get") [worldRuleBinderType] (pure lhsExpr) [|w|] AllPhases -}
  --
  --              [d| {-# RULES "component/set . get" forall w . set $(opticAccessorForRule) (view $(opticAccessorForRule) w) w = w #-} |] -- TODO get proper component name --
  withComponent <-
    [d|$withPat = storage @($nameType) @($worldType) @Storing @(Prop $a) @($a) . entityKeys . from asIntersection|]

  componentAdder <-
    [d|
      $adderPat = conjoined (sets (\f ent -> $setterPat (error ("Entity " ++ show ent ++ " never had its " ++ name' ++ " set!")) ent & $(varE namedOptic) %~ f)) (isets (\f ent -> $setterPat (error ("Entity " ++ show ent ++ " never had its " ++ name' ++ " set!")) ent & $(varE namedOptic) %@~ f))
      |]
  componentRemover <-
    [d|
      $removerPat = $removerLambda
      |]

  let className = makeHasComponentClassName name'
      classInstanceContext = pure []
      classInstanceType = (conT className) `appT` worldType
  classInstance <- instanceD classInstanceContext classInstanceType (fmap pure (componentAdder <> componentRemover <> sigs <> withComponent <> [theBody, withComonentInlinePragma, adderInlinePragma, removerInlinePragma, opticInlinePragma]))
  hasTypeInstance <- makeHasTypeInstance constructorInfo worldType tys
  pure ([classInstance {-,  opticRule-}] ++ hasTypeInstance)
makeComponentAccessor _ _ _ = fail "Mischief afoot with makeComponentAccessor"

-- | This is so we do not have to use the Generics-based functions which use indexing, and so are hella slow. At this point, we do not need the generic-lens package at all.
makeHasTypeInstance :: (Quasi m, Quote m) => D.ConstructorInfo -> m Type -> (Type, Type, Type) -> m [Dec]
makeHasTypeInstance constructorInfo worldType (nameType'@(LitT (StrTyLit name')), a', s') = do
  let nameType = pure nameType'
      a = pure a' -- The component type
  fieldType <- [t|Games.ECS.Component.AComponent $nameType $(pure s') $a|]
  let fieldPosition' = elemIndex ((show . ppr) fieldType) (fmap (show . ppr) $ D.constructorFields constructorInfo)
      fieldPosition = fromMaybe (error ((show fieldType) ++ (show constructorInfo))) fieldPosition'
      (getterBindingVar, consPat, valP, _, setterPattern) = makeConstructorPatternAndValuePair constructorInfo fieldPosition

  componentInstances <- runQ $ reifyInstances ''Prop [a']
  case componentInstances of
    [] -> fail ("No Component instance found for " ++ name' ++ ". Don't forget to also call makeHasComponentClass!")
    [TySynInstD (TySynEqn _bndrs _lhs (PromotedT prop))] -> do
      theInstance <- theInstanceFunc prop nameType consPat getterBindingVar valP setterPattern a
      pure theInstance
    _ -> fail ("Weird Component instance found for " ++ name' ++ ". Don't forget to also call makeHasComponentClass!")
  where
    theInstanceFunc p nameType consPat getterBindingVar valP setterPattern a
      | p == requiredName =
          [d|
            instance {-# OVERLAPS #-} (Prop $a ~ Required) => Games.ECS.Slot.HasType (TaggedComponent $nameType $a) ($worldType Individual) where
              {-# INLINE CONLIKE typed #-}
              -- typed :: (Prop $a ~ Required) => Lens' ($worldType Individual) (Tagged $nameType $a)
              typed = Control.Lens.lens getTyped (flip setTyped)

              -- getTyped :: (Prop $a ~ Required) => $worldType Individual -> (Tagged $nameType $a)
              {-# INLINE CONLIKE getTyped #-}
              getTyped $consPat = $getterBindingVar
              {-# INLINE CONLIKE setTyped #-}
              setTyped $valP $consPat = $setterPattern
            |]
    theInstanceFunc p nameType consPat getterBindingVar valP setterPattern a
      | p == normalName =
          [d|
            instance {-# OVERLAPS #-} (Prop $a ~ Normal) => Games.ECS.Slot.HasType (TaggedComponent $nameType (Maybe $a)) ($worldType Individual) where
              {-# INLINE CONLIKE typed #-}
              -- typed :: (Prop $a ~ Normal) => Lens' ($worldType Individual) (Tagged $nameType (Maybe $a))
              typed = Control.Lens.lens getTyped (flip setTyped)

              -- getTyped :: (Prop $a ~ Normal) => $worldType Individual -> (Tagged $nameType (Maybe $a))
              {-# INLINE CONLIKE getTyped #-}
              getTyped $consPat = $getterBindingVar
              {-# INLINE CONLIKE setTyped #-}
              setTyped $valP $consPat = $setterPattern

            instance {-# OVERLAPS #-} (Prop $a ~ Normal, Storage $a ~ saa) => Games.ECS.Slot.HasType (TaggedComponent $nameType (saa $a)) ($worldType Storing) where
              {-# INLINE CONLIKE typed #-}
              --                         typed :: (Prop $a ~ Normal) => Lens' ($worldType Storing) (Tagged $nameType (Storage $a $a))
              typed = Control.Lens.lens getTyped (flip setTyped)

              --                         getTyped :: (Prop $a ~ Normal) => $worldType Storing -> (Tagged $nameType (Storage $a $a))
              {-# INLINE CONLIKE getTyped #-}
              getTyped $consPat = $getterBindingVar
              {-# INLINE CONLIKE setTyped #-}
              setTyped $valP $consPat = $setterPattern
            |]
    theInstanceFunc p nameType consPat getterBindingVar valP setterPattern a
      | p == uniqueName =
          [d|
            instance {-# OVERLAPS #-} (Prop $a ~ Games.ECS.World.Unique) => Games.ECS.Slot.HasType (TaggedComponent $nameType (Maybe $a)) ($worldType Individual) where
              {-# INLINE CONLIKE typed #-}
              -- typed :: (Prop $a ~ Games.ECS.World.Unique) => Lens' ($worldType Individual) (Tagged $nameType (Maybe $a))
              typed = Control.Lens.lens getTyped (flip setTyped)

              -- getTyped :: (Prop $a ~ Games.ECS.World.Unique) => $worldType Individual -> (Tagged $nameType (Maybe $a))
              {-# INLINE CONLIKE getTyped #-}
              getTyped $consPat = $getterBindingVar
              {-# INLINE CONLIKE setTyped #-}
              setTyped $valP $consPat = $setterPattern

            instance {-# OVERLAPS #-} (Prop $a ~ Games.ECS.World.Unique) => Games.ECS.Slot.HasType (TaggedComponent $nameType (UniqueStore $a)) ($worldType Storing) where
              {-# INLINE CONLIKE typed #-}
              -- typed :: (Prop $a ~ Games.ECS.World.Unique) => Lens' ($worldType Storing) (Tagged $nameType (UniqueStore $a))
              typed = Control.Lens.lens getTyped (flip setTyped)

              -- getTyped :: (Prop $a ~ Games.ECS.World.Unique) => $worldType Storing -> (Tagged $nameType (UniqueStore $a))
              {-# INLINE CONLIKE getTyped #-}
              getTyped $consPat = $getterBindingVar
              {-# INLINE CONLIKE setTyped #-}
              setTyped $valP $consPat = $setterPattern
            |]
    theInstanceFunc _ nameType _ _ _ _ _ = do
      theName <- nameType
      fail ("Weird stuff is going on when creating HasType instance for " ++ (show theName) ++ "!")
makeHasTypeInstance _ worldType _ = do
  worldTy <- worldType
  fail ("Error while creating HasType instance for " ++ show worldTy ++ "!")

makeConstructorPatternAndValuePair :: (Quote m) => D.ConstructorInfo -> Int -> (m Exp, m Pat, m Pat, m Exp, m Exp)
makeConstructorPatternAndValuePair constructorInfo fieldPosition = (getterBindingVar, consPat, valP, valE, setterPattern)
  where
    valName = mkName "val"
    valE = varE valName
    valP = (tildeP . varP) valName
    numFields = length (D.constructorFields constructorInfo)
    fieldNames = fmap (\i -> mkName ("_theField" ++ show i)) [0 .. (numFields - 1)]
    fieldPatterns = fmap (tildeP . varP) fieldNames
    consPat = conP (D.constructorName constructorInfo) fieldPatterns
    getterBindingVar = varE (fieldNames !! fieldPosition)
    prefix = fmap varE (take fieldPosition fieldNames)
    mid = prefix ++ [valE]
    postfix = mid ++ fmap varE (drop (fieldPosition + 1) fieldNames)
    setterPattern = foldl appE (conE (D.constructorName constructorInfo)) postfix