{-# OPTIONS_GHC -Wno-redundant-constraints #-}
{-# OPTIONS_GHC -Wno-orphans #-}

module Lorentz.UParam
  ( UParam (..)
  , EntryPointKind
  , type (?:)

    -- * Construction
  , mkUParam

    -- * Deconstruction
  , ConstrainedSome (..)
  , UnpackUParam
  , unpackUParam

    -- * Casting to homomorphic value
  , SomeInterface
  , UParam_

    -- * Pattern-matching
  , EntryPointsImpl
  , UParamFallback
  , EntryPointLookupError (..)
  , CaseUParam
  , caseUParam
  , caseUParamT
  , uparamFallbackFail

    -- * Constraints
  , LookupEntryPoint
  , RequireUniqueEntryPoints

    -- * Conversion from ADT
  , uparamFromAdt
  , UParamLinearize
  , UParamLinearized

    -- * Documentation
  , pbsUParam

    -- * Internals used for entrypoint-wise migrations
  , unwrapUParam
  ) where

import Data.Constraint ((\\))
import qualified Data.Kind as Kind
import Data.Vinyl.Core (Rec(..))
import Data.Vinyl.Derived (Label)
import Data.Vinyl.TypeLevel (type (++))
import qualified Fcf
import Fmt (Buildable(..))
import GHC.Generics ((:*:)(..), (:+:)(..))
import qualified GHC.Generics as G
import GHC.TypeLits (KnownSymbol, Symbol)
import qualified Text.Show

import Lorentz.ADT
import Lorentz.Base
import Lorentz.Coercions
import Lorentz.EntryPoints.Doc
import Lorentz.Constraints
import Lorentz.Errors
import Lorentz.Instr as L
import Lorentz.Macro
import Lorentz.Pack
import Michelson.Text
import Michelson.Typed
import Util.Type
import Util.TypeLits
import Util.TypeTuple
import Util.Markdown

-- | An entry point is described by two types: its name and type of argument.
type EntryPointKind = (Symbol, Kind.Type)

-- | A convenient alias for type-level name-something pair.
type (n :: Symbol) ?: (a :: k) = '(n, a)

-- | Encapsulates parameter for one of entry points.
-- It keeps entry point name and corresponding argument serialized.
--
-- In Haskell world, we keep an invariant of that contained value relates
-- to one of entry points from @entries@ list.
newtype UParam (entries :: [EntryPointKind]) = UParamUnsafe (MText, ByteString)
  deriving stock (Generic, Eq, Show)
  deriving anyclass (IsoValue)

instance Wrapped (UParam entries)

-- Casting to homomorphic value
----------------------------------------------------------------------------

-- | Pseudo value for 'UParam' type variable.
type SomeInterface = '[ '("SomeEntrypoints", Void) ]

-- | Homomorphic version of 'UParam', forgets the exact interface.
type UParam_ = UParam SomeInterface

-- | Allows casts only between 'UParam_' and 'UParam'.
instance SameEntries entries1 entries2 =>
         UParam entries1 `CanCastTo` UParam entries2

type family SameEntries (es1 :: [EntryPointKind]) (es :: [EntryPointKind])
              :: Constraint where
  SameEntries e e = ()
  SameEntries SomeInterface _ = ()
  SameEntries _ SomeInterface = ()
  SameEntries e1 e2 = (e1 ~ e2)

----------------------------------------------------------------------------
-- Common type-level stuff
----------------------------------------------------------------------------

-- | Get type of entry point argument by its name.
type family LookupEntryPoint (name :: Symbol) (entries :: [EntryPointKind])
             :: Kind.Type where
  LookupEntryPoint name ('(name, a) ': _) = a
  LookupEntryPoint name (_ ': entries) = LookupEntryPoint name entries
  LookupEntryPoint name '[] =
    TypeError ('Text "Entry point " ':<>: 'ShowType name ':<>:
               'Text " in not in the entry points list")

-- | Ensure that given entry points do no contain duplicated names.
type family RequireUniqueEntryPoints (entries :: [EntryPointKind])
             :: Constraint where
  RequireUniqueEntryPoints entries =
    RequireAllUnique "entry point" (Fcf.Eval (Fcf.Map Fcf.Fst entries))

----------------------------------------------------------------------------
-- Construction
----------------------------------------------------------------------------

-- | Construct a 'UParam' safely.
mkUParam
  :: ( KnownSymbol name, NicePackedValue a
     , LookupEntryPoint name entries ~ a
     , RequireUniqueEntryPoints entries
     )
  => Label name -> a -> UParam entries
mkUParam label (a :: a) =
  UParamUnsafe (labelToMText label, lPackValue a)
    \\ nicePackedValueEvi @a

-- Example
----------------------------------------------------------------------------

type MyEntryPoints =
  [ "add" ?: Integer
  , "reset" ?: ()
  ]

_mkParamSample1 :: UParam MyEntryPoints
_mkParamSample1 = mkUParam #add 5

----------------------------------------------------------------------------
-- Deconstruction
----------------------------------------------------------------------------

-- | This type can store any value that satisfies a certain constraint.
data ConstrainedSome (c :: Kind.Type -> Constraint) where
  ConstrainedSome :: c a => a -> ConstrainedSome c

instance Show (ConstrainedSome Show) where
  show (ConstrainedSome a) = show a

instance Buildable (ConstrainedSome Buildable) where
  build (ConstrainedSome a) = build a

-- | This class is needed to implement `unpackUParam`.
class UnpackUParam (c :: Kind.Type -> Constraint) entries where
  -- | Turn 'UParam' into a Haskell value.
  -- Since we don't know its type in compile time, we have to erase it using
  -- 'ConstrainedSome'. The user of this function can require arbitrary
  -- constraint to hold (depending on how they want to use the result).
  unpackUParam ::
    UParam entries -> Either EntryPointLookupError (MText, ConstrainedSome c)

instance UnpackUParam c '[] where
  unpackUParam (UParamUnsafe (name, _)) = Left (NoSuchEntryPoint name)

instance
  ( KnownSymbol name
  , UnpackUParam c entries
  , NiceUnpackedValue arg
  , c arg
  ) => UnpackUParam c ((name ?: arg) ': entries) where
  unpackUParam (UParamUnsafe (name, bytes))
    | name == symbolToMText @name =
      fmap (name,) .
      first (const ArgumentUnpackFailed) .
      fmap ConstrainedSome .
      lUnpackValue @arg $
      bytes
    | otherwise = unpackUParam @c @entries (UParamUnsafe (name, bytes))

----------------------------------------------------------------------------
-- Pattern-matching
----------------------------------------------------------------------------

-- | Helper instruction which extracts content of 'UParam'.
unwrapUParam :: UParam entries : s :-> (MText, ByteString) : s
unwrapUParam = coerceUnwrap

-- | Wrapper for a single "case" branch.
data CaseClauseU inp out (entry :: EntryPointKind) where
  CaseClauseU :: (arg : inp) :-> out -> CaseClauseU inp out '(name, arg)

instance (name ~ name', body ~ ((arg : inp) :-> out)) =>
         CaseArrow name' body (CaseClauseU inp out '(name, arg)) where
  (/->) _ = CaseClauseU

data EntryPointLookupError
  = NoSuchEntryPoint MText
  | ArgumentUnpackFailed
  deriving stock (Generic, Eq, Show)

instance Buildable EntryPointLookupError where
  build =
    \case
      NoSuchEntryPoint name -> "No such entrypoint: " <> build name
      ArgumentUnpackFailed -> "UNPACK failed"

type instance ErrorArg "uparamNoSuchEntryPoint" = MText
type instance ErrorArg "uparamArgumentUnpackFailed" = ()

instance Buildable (CustomError "uparamNoSuchEntryPoint") where
  build (CustomError _ name) = "No such entrypoint: " <> build name

instance Buildable (CustomError "uparamArgumentUnpackFailed") where
  build (CustomError _ ()) = "UNPACK failed"

instance CustomErrorHasDoc "uparamNoSuchEntryPoint" where
  customErrClass = ErrClassBadArgument
  customErrDocMdCause = "Entrypoint with given name does not exist."

instance CustomErrorHasDoc "uparamArgumentUnpackFailed" where
  customErrClass = ErrClassBadArgument
  customErrDocMdCause = "Argument of wrong type is provided to the entrypoint."

-- | Implementations of some entry points.
--
-- Note that this thing inherits properties of 'Rec', e.g. you can
-- @Data.Vinyl.Core.rappend@ implementations for two entry point sets
-- when assembling scattered parts of a contract.
type EntryPointsImpl inp out entries =
  Rec (CaseClauseU inp out) entries

-- | An action invoked when user-provided entry point is not found.
type UParamFallback inp out = ((MText, ByteString) : inp) :-> out

-- | Default implementation for 'UParamFallback', simply reports an error.
uparamFallbackFail :: UParamFallback inp out
uparamFallbackFail =
  car # failCustom #uparamNoSuchEntryPoint

-- | Make up a "case" over entry points.
class CaseUParam (entries :: [EntryPointKind]) where
  -- | Pattern-match on given @UParam entries@.
  --
  -- You have to provide all case branches and a fallback action on case
  -- when entry point is not found.
  --
  -- This function is unsafe because it does not make sure at type-level
  -- that entry points' names do not repeat.
  caseUParamUnsafe
    :: Rec (CaseClauseU inp out) entries
    -> UParamFallback inp out
    -> (UParam entries : inp) :-> out

instance CaseUParam '[] where
  caseUParamUnsafe RNil fallback = unwrapUParam # fallback

instance ( KnownSymbol name
         , CaseUParam entries
         , NiceUnpackedValue arg
         ) =>
         CaseUParam ((name ?: arg) ': entries) where
  caseUParamUnsafe (CaseClauseU clause :& clauses) fallback =
    dup # unwrapUParam # car #
    push (mkMTextUnsafe $ symbolValT' @name) # eq #
    if_ (unwrapUParam # cdr # unpack #
         ifSome nop (failCustom_ #uparamArgumentUnpackFailed) # clause)
        (cutUParamEntry # caseUParamUnsafe clauses fallback)
    where
      cutUParamEntry :: UParam (e : es) : s :-> UParam es : s
      cutUParamEntry = forcedCoerce_

-- | Pattern-match on given @UParam entries@.
--
-- You have to provide all case branches and a fallback action on case
-- when entry point is not found.
caseUParam
  :: (CaseUParam entries, RequireUniqueEntryPoints entries)
  => Rec (CaseClauseU inp out) entries
  -> UParamFallback inp out
  -> (UParam entries : inp) :-> out
caseUParam = caseUParamUnsafe

-- | Like 'caseUParam', but accepts a tuple of clauses, not a 'Rec'.
caseUParamT
  :: forall entries inp out clauses.
     ( clauses ~ Rec (CaseClauseU inp out) entries
     , RecFromTuple clauses
     , CaseUParam entries
     )
  => IsoRecTuple clauses
  -> UParamFallback inp out
  -> (UParam entries : inp) :-> out
caseUParamT clauses fallback = caseUParamUnsafe (recFromTuple clauses) fallback

-- Example
----------------------------------------------------------------------------

_caseSample :: UParam MyEntryPoints : s :-> Integer : s
_caseSample = caseUParamT
  ( #add /-> nop
  , #reset /-> L.drop @() # push 0
  ) uparamFallbackFail

----------------------------------------------------------------------------
-- ADT conversion
----------------------------------------------------------------------------

{- @martoon: I actually hope no one will use this capability,
   it's here primarily because in other places we also use Generic stuff.

   Representation with type-level list and a datatype made polymorhpic over
   it seems more powerful than Generics.
   1. It's simpler to implement features for it. No extra boilerplate.
   2. 'Data.Vinyl' provides many useful utilities to work with such things.
   3. You are not such constrained in selecting names of entry points as when
      they come from constructor names.
-}

-- | Make up 'UParam' from ADT sum.
--
-- Entry points template will consist of
-- @(constructorName, constructorFieldType)@ pairs.
-- Each constructor is expected to have exactly one field.
uparamFromAdt
  :: UParamLinearize up
  => up -> UParam (UParamLinearized up)
uparamFromAdt = adtToRec . G.from

-- | Constraint required by 'uparamFromAdt'.
type UParamLinearize p = (Generic p, GUParamLinearize (G.Rep p))

-- | Entry points template derived from given ADT sum.
type UParamLinearized p = GUParamLinearized (G.Rep p)

-- | Generic traversal for conversion between ADT sum and 'UParam'.
class GUParamLinearize (x :: Kind.Type -> Kind.Type) where
  type GUParamLinearized x :: [(Symbol, Kind.Type)]
  adtToRec :: x p -> UParam (GUParamLinearized x)

instance GUParamLinearize x => GUParamLinearize (G.D1 i x) where
  type GUParamLinearized (G.D1 i x) = GUParamLinearized x
  adtToRec = adtToRec . G.unM1

instance (GUParamLinearize x, GUParamLinearize y) => GUParamLinearize (x :+: y) where
  type GUParamLinearized (x :+: y) = GUParamLinearized x ++ GUParamLinearized y
  adtToRec = \case
    G.L1 x -> let UParamUnsafe up = adtToRec x in UParamUnsafe up
    G.R1 y -> let UParamUnsafe up = adtToRec y in UParamUnsafe up

instance (KnownSymbol name, NicePackedValue a) =>
         GUParamLinearize (G.C1 ('G.MetaCons name _1 _2) (G.S1 si (G.Rec0 a))) where
  type GUParamLinearized (G.C1 ('G.MetaCons name _1 _2) (G.S1 si (G.Rec0 a))) =
    '[ '(name, a) ]

  adtToRec (G.M1 (G.M1 (G.K1 a))) = UParamUnsafe
    ( symbolToMText @name
    , lPackValue a
    )

instance
    TypeError ('Text "UParam linearization requires exactly one field \
                    \in each constructor") =>
    GUParamLinearize (G.C1 i G.U1) where
  type GUParamLinearized (G.C1 i G.U1) =
    TypeError ('Text "Bad linearized ADT")
  adtToRec = error "impossible"

instance
    TypeError ('Text "UParam linearization requires exactly one field \
                    \in each constructor") =>
    GUParamLinearize (G.C1 i (x :*: y)) where
  type GUParamLinearized (G.C1 i (x :*: y)) =
    TypeError ('Text "Bad linearized ADT")
  adtToRec = error "impossible"

----------------------------------------------------------------------------
-- Documentation
----------------------------------------------------------------------------

instance Typeable interface => TypeHasDoc (UParam interface) where
  typeDocName _ = "Upgradable parameter"
  typeDocMdReference p = customTypeDocMdReference ("UParam", DType p) []
  typeDocMdDescription = "<UParam description>"
  typeDocHaskellRep = homomorphicTypeDocHaskellRep
  typeDocMichelsonRep = homomorphicTypeDocMichelsonRep

-- | Note that calling given entrypoints involves constructing 'UParam'.
pbsUParam :: forall ctorName. KnownSymbol ctorName => ParamBuildingStep
pbsUParam =
  let ctor = build $ symbolValT' @ctorName
  in ParamBuildingStep
      { pbsEnglish =
          "Wrap into *UParam* as " <> mdTicked ctor <> " entry point."
      , pbsHaskell =
          \a -> "mkUParam #" <> ctor <> " (" <> a <> ")"
      , pbsMichelson =
          \a -> "Pair \"" <> ctor <> "\" (pack (" <> a <> "))"
      }