-- | Utilities for declaring and documenting entry points.
module Lorentz.EntryPoints.Doc
  ( DEntryPoint (..)
  , PlainEntryPointsKind
  , diEntryPointToMarkdown
  , DEntryPointArg (..)
  , DeriveCtorFieldDoc (..)
  , ParamBuildingStep (..)
  , clarifyParamBuildingSteps
  , mkDEntryPointArgSimple
  , DocumentEntryPoints
  , entryCase
  , entryCase_
  ) where

import qualified Data.Kind as Kind
import Data.Vinyl.Core (RMap, Rec(..), rappend)
import Fmt (build)
import GHC.Generics ((:+:))
import qualified GHC.Generics as G
import GHC.TypeLits (KnownSymbol, symbolVal)

import Lorentz.ADT
import Lorentz.Base
import Lorentz.Doc
import Michelson.Typed.Doc
import Michelson.Typed.Haskell.Doc
import Michelson.Typed.Haskell.Instr
import Util.Markdown
import Util.Type
import Util.TypeTuple

-- | Gathers information about single entry point.
--
-- We assume that entry points might be of different kinds,
-- which is designated by phantom type parameter.
-- For instance, you may want to have several groups of entry points
-- corresponding to various parts of a contract - specifying different @kind@
-- type argument for each of those groups will allow you defining different
-- 'DocItem' instances with appropriate custom descriptions for them.
data DEntryPoint (kind :: Kind.Type) = DEntryPoint
  { depName :: Text
  , depSub :: SubDoc
  }

-- | Default implementation of 'docItemToMarkdown' for entry points.
diEntryPointToMarkdown :: HeaderLevel -> DEntryPoint level -> Markdown
diEntryPointToMarkdown lvl (DEntryPoint name block) =
  mdSeparator <>
  mdHeader lvl (mdTicked $ build name) <>
  subDocToMarkdown (nextHeaderLevel lvl) block

-- | Default value for 'DEntryPoint' type argument.
data PlainEntryPointsKind

instance DocItem (DEntryPoint PlainEntryPointsKind) where
  type DocItemPosition (DEntryPoint PlainEntryPointsKind) = 1000
  docItemSectionName = Just "Entry points"
  docItemToMarkdown = diEntryPointToMarkdown

-- | During incremental assembly of parameter building steps -
-- current representation of parameter.
type CurrentParam = Markdown

-- | Describes a parameter building step.
--
-- This can be wrapping into (Haskell) constructor, or a more complex
-- transformation.
data ParamBuildingStep = ParamBuildingStep
  { pbsEnglish :: Markdown
    -- ^ Plain english description of this step.
  , pbsHaskell :: CurrentParam -> Markdown
    -- ^ How to construct parameter in Haskell code.
  , pbsMichelson :: CurrentParam -> Markdown
    -- ^ How to construct parameter working on raw Michelson.
  }

-- | Describes argument of an entry point.
data DEntryPointArg = DEntryPointArg
  { epaArg :: Maybe DType
    -- ^ Argument of the entry point. Pass 'Nothing' if no argument is required.
  , epaBuilding :: [ParamBuildingStep]
    -- ^ Describes a way to lift an entry point argument into full parameter
    -- which can be passed to the contract.
    --
    -- Steps are supposed to be applied in the order in which they are given.
    -- E.g. suppose that an entry point is called as @Run (Service1 arg)@;
    -- then the first step should describe wrapping into @Service1@ constructor,
    -- and the second step should be about wrapping into @Run@ constructor.
  }

mkDEntryPointArgSimple :: forall t. TypeHasDoc t => DEntryPointArg
mkDEntryPointArgSimple = DEntryPointArg
  { epaArg = Just $ DType (Proxy @t)
  , epaBuilding = []
  }

-- | Go over contract code and update every occurrence of 'DEntryPointArg'
-- documentation item, adding the given step to its "how to build parameter"
-- description.
clarifyParamBuildingSteps :: ParamBuildingStep -> (inp :-> out) -> (inp :-> out)
clarifyParamBuildingSteps pbs =
  iMapAnyCode $
  modifyInstrDoc (\di -> di{ epaBuilding = epaBuilding di ++ [pbs] })

instance DocItem DEntryPointArg where
  type DocItemPosition DEntryPointArg = 20
  docItemSectionName = Nothing
  docItemDependencies (DEntryPointArg mdty _) =
    [SomeDocDefinitionItem dty | Just dty <- pure mdty]
  docItemToMarkdown _ (DEntryPointArg mdty psteps) =
    mconcat . Prelude.map (<> "\n\n") $
      [ mdSubsection "Parameter" $
          case mdty of
            Nothing -> "none (pass unit)"
            Just (DType dty) -> typeDocMdReference dty (WithinParens False)

      , mdSpoiler "How to call this entry point" . mconcat $
          [ "\n0. Construct parameter for the entry point.\n"
          , mconcat . Prelude.intersperse "\n" $
              psteps <&> \ParamBuildingStep{..} ->
                mconcat . Prelude.intersperse "\n" $
                [ -- Markdown re-enumerates enumerated lists automatically
                  "1. " <> pbsEnglish
                , "    + " <>
                  mdSubsection "In Haskell" (mdTicked $ pbsHaskell "·")
                , "    + " <>
                  mdSubsection "In Michelson" (mdTicked $ pbsMichelson "·")
                ]
          , "\n\nPass resulting value as parameter to the contract.\n"
          ]
      ]

-- | Pick a type documentation from 'CtorField'.
class DeriveCtorFieldDoc (cf :: CtorField) where
  deriveCtorFieldDoc :: Maybe DType
instance DeriveCtorFieldDoc 'NoFields where
  deriveCtorFieldDoc = Nothing
instance TypeHasDoc ty => DeriveCtorFieldDoc ('OneField ty) where
  deriveCtorFieldDoc = Just $ DType (Proxy @ty)

-- | Add necessary documentation to entry points.
documentEntryPoints
  :: forall a kind inp out.
     DocumentEntryPoints kind a
  => Rec (CaseClauseL inp out) (CaseClauses a)
  -> Rec (CaseClauseL inp out) (CaseClauses a)
documentEntryPoints = gDocumentEntryPoints @kind @(G.Rep a) id

-- | Constraint for 'documentEntryPoints'.
type DocumentEntryPoints kind a =
  (Generic a, GDocumentEntryPoints kind (G.Rep a))

-- | Traverse entry points and add parameter building step (which describes
-- necessity to wrap parameter into some constructor of the given datatype)
-- to all parameters described within given code.
class GDocumentEntryPoints (kind :: Kind.Type) (x :: Kind.Type -> Kind.Type) where
  -- | Add corresponding parameter building step.
  --
  -- First argument is accumulator for Michelson description of the building step.
  gDocumentEntryPoints
    :: (Markdown -> Markdown)
    -> Rec (CaseClauseL inp out) (GCaseClauses x)
    -> Rec (CaseClauseL inp out) (GCaseClauses x)

instance GDocumentEntryPoints kind x => GDocumentEntryPoints kind (G.D1 i x) where
  gDocumentEntryPoints = gDocumentEntryPoints @kind @x

instance ( GDocumentEntryPoints kind x, GDocumentEntryPoints kind y
         , RSplit (GCaseClauses x) (GCaseClauses y)
         ) =>
         GDocumentEntryPoints kind (x :+: y) where
  gDocumentEntryPoints michDesc clauses =
    let (lclauses, rclauses) = rsplit @CaseClauseParam @(GCaseClauses x) clauses
    in gDocumentEntryPoints @kind @x
         (\a -> michDesc $ "Left (" <> a <> ")")
         lclauses
       `rappend`
       gDocumentEntryPoints @kind @y
         (\a -> michDesc $ "Right (" <> a <> ")")
         rclauses

instance ( 'CaseClauseParam ctor cf ~ GCaseBranchInput ctor x
         , KnownSymbol ctor
         , DocItem (DEntryPoint kind)
         , DeriveCtorFieldDoc cf
         ) =>
         GDocumentEntryPoints kind (G.C1 ('G.MetaCons ctor _1 _2) x) where
  gDocumentEntryPoints michDesc (CaseClauseL clause :& RNil) =
    let entryPointName = toText $ symbolVal (Proxy @ctor)
        psteps = ParamBuildingStep
          { pbsEnglish = "Wrap into " <> mdTicked (build entryPointName) <> " constructor."
          , pbsHaskell = \p -> build entryPointName <> " (" <> p <> ")"
          , pbsMichelson = michDesc
          }
        addDoc instr =
          clarifyParamBuildingSteps psteps $
          docGroup (SomeDocItem . DEntryPoint @kind entryPointName) $
          doc (DEntryPointArg (deriveCtorFieldDoc @cf) []) # instr
    in CaseClauseL (addDoc clause) :& RNil

-- | Like 'case_', to be used for pattern-matching on parameter.
--
-- Modifies documentation accordingly. Including description of
-- entrypoints' arguments, thus for them you will need to supply
-- 'TypeHasDoc' instance.
entryCase_
  :: forall dt entryPointKind out inp.
     ( InstrCaseC dt inp out
     , RMap (CaseClauses dt)
     , DocumentEntryPoints entryPointKind dt
     )
  => Proxy entryPointKind
  -> Rec (CaseClauseL inp out) (CaseClauses dt)
  -> dt & inp :-> out
entryCase_ _ = case_ . documentEntryPoints @dt @entryPointKind

-- | Version of 'entryCase_' for tuples.
entryCase
  :: forall dt entryPointKind out inp clauses.
     ( CaseTC dt out inp clauses
     , DocumentEntryPoints entryPointKind dt
     )
  => Proxy entryPointKind -> IsoRecTuple clauses -> dt & inp :-> out
entryCase p = entryCase_ p . recFromTuple