-- SPDX-FileCopyrightText: 2021 Oxhead Alpha
-- SPDX-License-Identifier: LicenseRef-MIT-OA

-- | Type-level field description validators
module Morley.Michelson.Typed.Haskell.ValidateDescription
  ( FieldDescriptions
  , FieldDescriptionsV

  , FieldDescriptionsValid
  ) where

import Data.Singletons (Demote)
import Data.Type.Bool (type (||))
import Fcf qualified
import GHC.Generics qualified as G
import GHC.TypeLits (ErrorMessage(..), Symbol)
import Type.Errors (DelayError, IfStuck)

import Morley.Util.Type (FailUnless, FailUnlessElse)

-- | Description of constructors and fields of some datatype.
--
-- This type is just two nested maps represented as associative lists. It is supposed to
-- be interpreted like this:
--
-- > [(Constructor name, (Maybe constructor description, [(Field name, Field description)]))]
--
-- Example with a concrete data type:
--
-- > data Foo
-- >   = Foo
-- >       { fFoo :: Int
-- >       }
-- >   | Bar
-- >       { fBar :: Text
-- >       }
-- >   deriving (Generic)
-- >
-- > type FooDescriptions =
-- >   '[ '( "Foo", '( 'Just "foo constructor",
-- >       , '[ '("fFoo", "some number")
-- >          ])
-- >       )
-- >    , '( "Bar", '( 'Nothing,
-- >       , '[ '("fBar", "some string")
-- >          ])
-- >       )
-- >    ]
type FieldDescriptions = [(Symbol, (Maybe Symbol, [(Symbol, Symbol)]))]

-- | Value-level counterpart to 'FieldDescriptions'.
type FieldDescriptionsV = Demote FieldDescriptions

-- | This type family checks that field descriptions mention only existing constructors and fields.
--
-- When @descr@ is empty this family does nothing, to avoid breaking for built-in, non-ADT types.
--
-- When @descr@ is not empty, this family will demand 'G.Generic' instance for @typ@ and fail with a
-- @TypeError@ if there none.
type FieldDescriptionsValid :: FieldDescriptions -> Type -> Constraint
type family FieldDescriptionsValid descr typ where
  FieldDescriptionsValid '[] _ = ()
  FieldDescriptionsValid descr typ =
    IfStuck (G.Rep typ)
      (DelayError
        ('Text "No Generic instance for " ':<>: 'ShowType typ ':<>: 'Text "." ':$$:
         'Text "Generic is needed to validate TypeDocFieldDescriptions.")
      )
      (Fcf.Pure (FieldDescriptionsValidImpl descr typ))

-- | Actual recursive implementation for 'FieldDescriptionsValid'.
type FieldDescriptionsValidImpl :: FieldDescriptions -> Type -> Constraint
type family FieldDescriptionsValidImpl descr typ where
  FieldDescriptionsValidImpl '[] _ = ()
  FieldDescriptionsValidImpl ( '(conName, '(_, fields)) ': rest ) typ =
    ( HasAllFields conName fields typ
    , FieldDescriptionsValidImpl rest typ
    )

-- | Check whether given constructor name @conName@ is present in 'G.Rep' of a datatype.
type HasConstructor :: Symbol -> (Type -> Type) -> Bool
type family HasConstructor conName g where
  HasConstructor conName (G.D1 _ cs) = HasConstructor conName cs
  HasConstructor conName (G.C1 ('G.MetaCons conName _ _) _) = 'True
  HasConstructor conName (l G.:+: r) = HasConstructor conName l || HasConstructor conName r
  HasConstructor _ _ = 'False

-- | Check whether all fields described for given constructor @conName@ are present in the datatype.
--
-- This family also checks whether the constructor is present and fails with a @TypeError@
-- otherwise.
type HasAllFields :: Symbol -> [(Symbol, Symbol)] -> Type -> Constraint
type family HasAllFields conName fields typ where
  HasAllFields conName fields typ =
    FailUnlessElse
      (HasConstructor conName (G.Rep typ))
      ('Text "No constructor " ':<>: 'ShowType conName ':<>:
      'Text " in type " ':<>: 'ShowType typ ':<>: 'Text "."
      )
      (HasAllFieldsImpl conName fields typ)

-- | Actual recursive implementation of 'HasAllFields'.
type HasAllFieldsImpl :: Symbol -> [(Symbol, Symbol)] -> Type -> Constraint
type family HasAllFieldsImpl conName fields typ where
  HasAllFieldsImpl _ '[] _ = ()
  HasAllFieldsImpl conName ( '(fieldName, _) ': rest ) typ =
    ( FailUnless
        (HasField conName fieldName (G.Rep typ))
        ('Text "No field " ':<>: 'ShowType fieldName ':<>: 'Text " in constructor " ':<>:
        'ShowType conName ':<>: 'Text " of type " ':<>: 'ShowType typ ':<>: 'Text "."
        )
    , HasAllFieldsImpl conName rest typ
    )

-- | Check that Generic 'G.Rep' has given field @fieldName@ in a given constructor @conName@.
type HasField :: Symbol -> Symbol -> (Type -> Type) -> Bool
type family HasField conName fieldName g where
  HasField conName fieldName (G.D1 _ cs) = HasField conName fieldName cs
  HasField conName fieldName (G.C1 ('G.MetaCons conName _ _) c) = ConHasField fieldName c
  HasField conName fieldName (l G.:+: r) = HasField conName fieldName l || HasField conName fieldName r
  HasField _ _ _ = 'False

-- | Check that Generic representation for a constructor has given field @fieldName@.
type ConHasField :: Symbol -> (Type -> Type) -> Bool
type family ConHasField fieldName g where
  ConHasField fieldName (G.S1 ('G.MetaSel ('Just fieldName) _ _ _) _) = 'True
  ConHasField fieldName (l G.:*: r) = ConHasField fieldName l || ConHasField fieldName r
  ConHasField _ _ = 'False