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

module Indigo.Internal.Object
  ( IndigoObjectF (..)
  , NamedFieldVar (..)
  , TypedFieldVar (..)
  , FieldTypes
  , Var
  , namedToTypedRec
  , typedToNamedRec
  , namedToTypedFieldVar
  , typedToNamedFieldVar

  , IsObject
  , complexObjectDict
  , ComplexObjectC
  , castFieldConstructors

  -- * Stack operations
  , withVarAt
  , makeTopVar
  , pushRefMd
  , pushNoRefMd
  , popNoRefMd

  -- * Operations/Storage variables
  , Ops
  , HasSideEffects
  , operationsVar
  , HasStorage
  , storageVar
  ) where

import Data.Vinyl (RMap)
import Data.Vinyl.TypeLevel (AllConstrained)
import Data.Reflection (Given (..))
import Data.Constraint (Dict(..))
import Data.Singletons (Sing)
import qualified GHC.Generics as G

import Indigo.Backend.Prelude
import Indigo.Lorentz
import Indigo.Internal.State
import Michelson.Typed.Haskell.Instr.Product
  ( GetFieldType, ConstructorFieldNames, GetFieldType
  , InstrDeconstructC, FieldConstructor (..), CastFieldConstructors (..))
import Michelson.Typed (IsPrimitiveValue)
import qualified Lorentz.Instr as L
import Util.Peano
import Util.Type (KnownList (..), KList (..))

----------------------------------------------------------------------------
-- IndigoObjectF and Variable
----------------------------------------------------------------------------

-- | A object that can be either
-- stored in the single stack cell or split into fields.
-- Fields are identified by their names.
--
-- @f@ is a functor to be applied to each of field names.
data IndigoObjectF f a where
  -- | Value stored on the stack, it might be
  -- either complex product type, like @(a, b)@, Storage, etc,
  -- or sum type like 'Either', or primitive like 'Int', 'Operation', etc.
  --
  -- Laziness of 'RefId' is needed here to make possible to put
  -- 'error' in a variable.
  -- This is used as a workaround in "Indigo.Compilation.Lambda".
  Cell :: KnownValue a => ~RefId -> IndigoObjectF f a
  -- | Decomposed product type, which is NOT stored
  -- as one cell on the stack.
  Decomposed :: ComplexObjectC a => Rec f (ConstructorFieldNames a) -> IndigoObjectF f a

-- | Auxiliary type family to convert list of field names
-- to list of field types
type family MapGFT a rs where
  MapGFT _ '[] = '[]
  MapGFT a (name ': xs) = GetFieldType a name ': MapGFT a xs

-- | Convert a list of fields from name-based list to type-based one
namedToTypedRec
  :: forall a f g .
    (forall name . f name -> g (GetFieldType a name))
  -> Rec f (ConstructorFieldNames a)
  -> Rec g (FieldTypes a)
namedToTypedRec fun = namedToTypedRecImpl
  where
    namedToTypedRecImpl :: Rec f rs -> Rec g (MapGFT a rs)
    namedToTypedRecImpl RNil = RNil
    namedToTypedRecImpl (v :& xs) = fun v :& namedToTypedRecImpl xs

-- | Convert a list of fields from type-based list to named-based one
typedToNamedRec :: forall a f g . KnownList (ConstructorFieldNames a)
  => (forall name . f (GetFieldType a name) -> g name)
  -> Rec f (FieldTypes a)
  -> Rec g (ConstructorFieldNames a)
typedToNamedRec fun = typedToNamedRecImpl
  where
    typedToNamedRecImpl :: forall rs . KnownList rs => Rec f (MapGFT a rs) -> Rec g rs
    typedToNamedRecImpl re = case (klist @rs, re) of
      (KNil, RNil)  -> RNil
      (KCons (_ :: Proxy nm) (_ :: Proxy rs'), v :& vs) -> fun v :& typedToNamedRecImpl vs

castFieldConstructors
  :: forall a st . CastFieldConstructors (FieldTypes a) (ConstructorFieldTypes a)
  => Rec (FieldConstructor st) (FieldTypes a) -> Rec (FieldConstructor st) (ConstructorFieldTypes a)
castFieldConstructors = castFieldConstructorsImpl

-- | Auxiliary datatype to define a variable.
-- Keeps field name as type param
data NamedFieldVar a name where
  NamedFieldVar
    :: IsObject (GetFieldType a name)
    => { unFieldVar :: Var (GetFieldType a name)
       }
    -> NamedFieldVar a name

-- | Variable exposed to a user.
--
-- 'Var' represents the tree of fields.
-- Each field is 'Var' itself:
-- either a value on the stack or 'Rec' of its direct fields.
type Var a = IndigoObjectF (NamedFieldVar a) a

-- | Like 'NamedFieldVar', but this one doesn't keep name of a field
data TypedFieldVar a where
  TypedFieldVar :: IsObject a => Var a -> TypedFieldVar a

namedToTypedFieldVar :: forall a name . NamedFieldVar a name -> TypedFieldVar (GetFieldType a name)
namedToTypedFieldVar (NamedFieldVar f) = TypedFieldVar f

typedToNamedFieldVar :: forall a name . TypedFieldVar (GetFieldType a name) -> NamedFieldVar a name
typedToNamedFieldVar (TypedFieldVar f) = NamedFieldVar f

----------------------------------------------------------------------------
-- IsObject type class
----------------------------------------------------------------------------

class IsObject' (TypeDecision a) a => IsObject a
instance IsObject' (TypeDecision a) a => IsObject a

type FieldTypes a = MapGFT a (ConstructorFieldNames a)

type ToDeconstructC a =
  ( InstrDeconstructC a
  , KnownList (FieldTypes a)
  , AllConstrained KnownValue (FieldTypes a)
  )

type ToConstructC a =
  ( KnownValue a
  , InstrConstructC a
  , RMap (ConstructorFieldNames a)
  , RMap (ConstructorFieldTypes a)
  , RMap (FieldTypes a)
  , KnownList (ConstructorFieldNames a)
  , CastFieldConstructors (FieldTypes a) (ConstructorFieldTypes a)
  )

type ComplexObjectC a =
  ( ToDeconstructC a
  , ToConstructC a
  , AllConstrained IsObject (FieldTypes a)
  )

-- | Type class instantiated for all possible Indigo types
class KnownValue a => IsObject' (decision :: Decision) a where
  complexObjectDict' :: Maybe (Dict (ComplexObjectC a))

instance KnownValue a => IsObject' 'PrimitiveD a where
  complexObjectDict' = Nothing

instance KnownValue a => IsObject' 'SumTypeD a where
  complexObjectDict' = Nothing

instance ComplexObjectC a => IsObject' 'ProductTypeD a where
  complexObjectDict' = Just Dict

complexObjectDict :: forall a . IsObject a => Maybe (Dict (ComplexObjectC a))
complexObjectDict = complexObjectDict' @(TypeDecision a) @a

-- | Decide whether type is either primitive or ADT
type TypeDecision a = Decide (IsPrimitiveValue a) (IsSumType (G.Rep a))

data Decision
  = PrimitiveD
  | SumTypeD
  | ProductTypeD

type family Decide flagPrimitive flagSumType where
  Decide 'True _ = 'PrimitiveD
  Decide 'False 'True = 'SumTypeD
  Decide 'False 'False = 'ProductTypeD

type family IsSumType x where
  IsSumType (G.D1 _ x) = IsSumType x
  IsSumType (G.C1 _ x) = IsSumType x
  IsSumType (G.M1 _ _ x) = IsSumType x
  IsSumType (_ G.:*: _) = 'False
  IsSumType (G.Rec0 _) = 'False
  IsSumType G.V1 = 'False
  IsSumType G.U1 = 'False
  IsSumType (_ G.:+: _) = 'True

----------------------------------------------------------------------------
-- Stack operations
----------------------------------------------------------------------------

-- | Given a 'MetaData' and a @Peano@ singleton for a depth, it puts a new 'Var'
-- at that depth (0-indexed) and returns it with the updated 'MetaData'.
--
-- If there is a 'Var' there already it is used and the 'MetaData' not changed.
withVarAt
  :: (KnownValue a, a ~ At n inp, RequireLongerThan inp n)
  => MetaData inp
  -> Sing n
  -> (MetaData inp, Var a)
withVarAt md@(MetaData (top :& xs) ref) = \case
  SS n -> first (appendToStack top) $ withVarAt (MetaData xs ref) n
  SZ -> case top of
    Ref matRef -> (md, Cell matRef)
    NoRef -> (MetaData (Ref ref :& xs) (ref + 1), Cell ref)
  where
    appendToStack :: StkEl x -> MetaData inp -> MetaData (x ': inp)
    appendToStack v (MetaData st r) = MetaData (v :& st) r

-- | Create a variable referencing the element on top of the stack.
makeTopVar :: KnownValue x => IndigoState (x & inp) (x & inp) (Var x)
makeTopVar = iget >>= \md ->
  let (newMd, var) = withVarAt md SZ
  in iput $ GenCode var newMd L.nop L.nop

-- | Push a new stack element with a reference to it.
-- Return the variable referencing this element.
pushRefMd :: KnownValue x => MetaData stk -> (Var x, MetaData (x & stk))
pushRefMd (MetaData stk cnt) = (Cell cnt, MetaData (Ref cnt :& stk) (cnt + 1))

-- | Push a new stack element without a reference to it.
pushNoRefMd :: KnownValue a => MetaData inp -> MetaData (a & inp)
pushNoRefMd (MetaData xs ref) = MetaData (NoRef :& xs) ref

-- | Remove the top element of the stack.
-- It's supposed that no variable refers to this element.
popNoRefMd :: MetaData (a & inp) -> MetaData inp
popNoRefMd (MetaData (NoRef :& xs) ref) = MetaData xs ref
popNoRefMd (MetaData (Ref refId :& _) _) =
  error $ "You try to pop stack element, which is referenced by some variable #" <> show refId

----------------------------------------------------------------------------
-- Operations/Storage variables
----------------------------------------------------------------------------

type Ops = [Operation]

-- | Allows to get a variable with operations
type HasSideEffects = Given (Var Ops)

-- | Return a variable which refers to a stack cell with operations
operationsVar :: HasSideEffects => Var Ops
operationsVar = given

-- This storage machinery is here to avoid cyclic deps

-- | Allows to get a variable with storage
type HasStorage st = Given (Var st)

-- | Return a variable which refers to a stack cell with storage
storageVar :: HasStorage st => Var st
storageVar = given
