-- SPDX-FileCopyrightText: 2020 Tocqueville Group
--
-- SPDX-License-Identifier: LicenseRef-MIT-TQ

{-# LANGUAGE InstanceSigs #-}

{- |
This module contains the core of Indigo language:
the 'IndigoState' monad, a datatype that represents its state.
It also includes some convenient functions to work with the state in IndigoM,
to provide rebindable syntax.

The 'IndigoState' monad implements the functionality of a symbolic interpreter.
During its execution Lorentz code is being generated.
-}

module Indigo.Internal.State
  ( -- * Indigo State
    IndigoState (..)
  , usingIndigoState
  , (>>=)
  , (=<<)
  , (>>)
  , (<$>)
  , return
  , iget
  , iput

  , RefId
  , StkEl (..)
  , StackVars
  , GenCode (..)
  , MetaData (..)
  , emptyMetadata
  , cleanGenCode
  , DefaultStack
  ) where

import qualified Data.Kind as Kind
import Data.Type.Equality (TestEquality(..))
import Data.Typeable (eqT)

import Indigo.Backend.Prelude
import Indigo.Lorentz
import qualified Lorentz.Instr as L

{-# ANN module ("HLint: ignore Reduce duplication" :: Text) #-}

----------------------------------------------------------------------------
-- Indigo State
----------------------------------------------------------------------------

-- | IndigoState monad. It's basically
-- [Control.Monad.Indexed.State](https://hackage.haskell.org/package/category-extras-0.53.5/docs/Control-Monad-Indexed-State.html)
-- , however this package is not in the used lts and it doesn't compile.
--
-- It takes as input a 'MetaData' (for the initial state) and returns a
-- 'GenCode' (for the resulting state and the generated Lorentz code).
--
-- IndigoState has to be used to write backend typed Lorentz code
-- from the corresponding frontend constructions.
newtype IndigoState inp out a =
  IndigoState {runIndigoState :: MetaData inp -> GenCode inp out a}
  deriving stock Functor

usingIndigoState :: MetaData inp -> IndigoState inp out a -> GenCode inp out a
usingIndigoState = flip runIndigoState

-- | Return for rebindable syntax.
return :: a -> IndigoState inp inp a
return a = IndigoState $ \md -> GenCode a md L.nop L.nop

-- | Bind for rebindable syntax.
--
-- It's basically like the bind for the 'State' monad, but it also composes the
-- generated code from @m a@ and @a -> m b@.
(>>=) :: forall inp out out1 a b . IndigoState inp out a -> (a -> IndigoState out out1 b) -> IndigoState inp out1 b
(>>=) m f = IndigoState $ \md ->
  let GenCode a md1 cd1 cl1 = runIndigoState m md in
  let GenCode b md2 cd2 cl2 = runIndigoState (f a) md1 in
  GenCode b md2 (cd1 ## cd2) (cl2 ## cl1)

(=<<) :: (a -> IndigoState out out1 b) -> IndigoState inp out a -> IndigoState inp out1 b
(=<<) = flip (>>=)

-- | Then for rebindable syntax.
(>>) :: IndigoState inp out a -> IndigoState out out1 b -> IndigoState inp out1 b
(>>) a b = a >>= const b

-- | Get current 'MetaData'.
iget :: IndigoState inp inp (MetaData inp)
iget = IndigoState $ \md -> GenCode md md L.nop L.nop

-- | Put new 'GenCode'.
iput :: GenCode inp out a -> IndigoState inp out a
iput gc = IndigoState $ \_ -> gc

----------------------------------------------------------------------------
-- Indigo stack and code gen primitives
----------------------------------------------------------------------------

-- | Reference id to a stack cell
newtype RefId = RefId Word
  deriving stock (Show, Generic)
  deriving newtype (Eq, Ord, Real, Num)

-- | Stack element of the symbolic interpreter.
--
-- It holds either a reference index that refers to this element
-- or just 'NoRef', indicating that there are no references
-- to this element.
data StkEl a where
  NoRef :: KnownValue a => StkEl a
  Ref :: KnownValue a => RefId -> StkEl a

instance TestEquality StkEl where
  testEquality NoRef NoRef = eqT
  testEquality (Ref _) (Ref _) = eqT
  testEquality (Ref _) NoRef = eqT
  testEquality NoRef (Ref _) = eqT

-- | Stack of the symbolic interpreter.
type StackVars (stk :: [Kind.Type]) = Rec StkEl stk

-- | Initial state of 'IndigoState'.
data MetaData stk = MetaData
  { mdStack :: StackVars stk
  -- ^ Stack of the symbolic interpreter.
  , mdRefCount :: RefId
  -- ^ Number of allocated variables.
  }

emptyMetadata :: MetaData '[]
emptyMetadata = MetaData RNil 0

type DefaultStack stk = Default (MetaData stk)

instance Default (MetaData '[]) where
  def = emptyMetadata

instance (KnownValue x, Default (MetaData xs)) => Default (MetaData (x ': xs)) where
  def = MetaData (NoRef :& mdStack def) 0

-- | Resulting state of IndigoM.
data GenCode inp out a = GenCode
  { gcOut :: ~a
  -- ^ Interpreter output value
  , gcMeta :: ~(MetaData out)
  -- ^ Interpreter meta data.
  , gcCode  :: inp :-> out
  -- ^ Generated Lorentz code.
  , gcClear :: out :-> inp
  -- ^ Clearing Lorentz code.
  } deriving stock Functor

-- | Produces the generated Lorentz code that cleans after itself, leaving the
-- same stack as the input one
cleanGenCode :: GenCode inp out a -> inp :-> inp
cleanGenCode GenCode {..} = gcCode ## gcClear
