{-# LANGUAGE KindSignatures, DataKinds, FlexibleInstances, FlexibleContexts,
             FunctionalDependencies, TypeFamilies, TypeOperators,
             PatternSynonyms, UndecidableInstances, ConstraintKinds,
             TypeApplications, ScopedTypeVariables, CPP,
             AllowAmbiguousTypes #-}

module Named.Internal where

import Prelude (id, Maybe(..))
import Data.Maybe (fromMaybe)
import Data.Functor.Identity (Identity(..))
import Data.Kind (Type)
import GHC.TypeLits (Symbol, TypeError, ErrorMessage(..))
import GHC.OverloadedLabels (IsLabel(..))

{- |

Assign a name to a value of type @a@ wrapped in @f@.

@
#verbose True :: NamedF Identity Bool "verbose"
@

-}
newtype NamedF f (a :: Type) (name :: Symbol) =
  ArgF (f a) -- ^ Match on an F-argument without specifying its name.
             -- See also: 'argF'.

-- | Match on an argument without specifying its name. See also: 'arg'.
pattern Arg :: a -> name :! a
pattern Arg a = ArgF (Identity a)

#if MIN_VERSION_base(4,10,0)
{-# COMPLETE Arg #-}
#endif

-- | Infix notation for the type of a named parameter.
type name :! a = NamedF Identity a name

-- | Infix notation for the type of an optional named parameter.
type name :? a = NamedF Maybe a name

class InjValue f where
  injValue :: a -> f a

instance InjValue Identity where
  injValue = Identity

instance InjValue Maybe where
  injValue = Just

instance (name ~ name', a ~ a', InjValue f) => IsLabel name (a -> NamedF f a' name') where
#if MIN_VERSION_base(4,10,0)
  fromLabel a = ArgF (injValue a)
#else
  fromLabel _ a = ArgF (injValue a)
#endif
  {-# INLINE fromLabel #-}

newtype Param p = Param p

instance (p ~ NamedF f a name, InjValue f) => IsLabel name (a -> Param p) where
#if MIN_VERSION_base(4,10,0)
  fromLabel a = Param (fromLabel @name a)
#else
  fromLabel pName a = Param (fromLabel pName a)
#endif
  {-# INLINE fromLabel #-}

{- | Explicitly build a function parameter:

@
fn '!' 'param' \#param_name value
@

This is equivalent to the implicit notation:

@
fn '!' \#param_name value
@

-}
param :: Name name -> a -> Param (name :! a)
param _ a = Param (Arg a)

{- | Explicitly build a function parameter inside an arity wrapper:

@
fn '!' 'paramF' \#param_name ('Identity' value)
fn '!' 'paramF' \#param_name ('Just' value)
fn '!' 'paramF' \#param_name 'Nothing'
@

This has no equivalent implicit notation.

-}
paramF :: Name name -> f a -> Param (NamedF f a name)
paramF _ fa = Param (ArgF fa)

{- | Supply a parameter to a function:

@
function '!' \#param_name value
@

@
function '!' \#x 7 '!' #y 42 '!' 'defaults'
@

This is an infix version of 'with'.

-}
(!) :: forall p fn fn'. WithParam p fn fn' => fn -> Param p -> fn'
fn ! p = with p fn
{-# INLINE (!) #-}

infixl 9 !

{- |
Supply a parameter @p@ to a function @fn@, resulting in @fn'@.

For example, when we pass a single named parameter, we get a function without
this parameter:

@
WithParam
                 ("x" :! Char)       -- p
  ("b" :! Bool -> "x" :! Char -> r)  -- fn
  ("b" :! Bool                -> r)  -- fn'
@

In case the parameter cannot be supplied, this constraint will become a type
error.

-}
class WithParam p fn fn' | p fn -> fn' where
  {- | Supply a parameter to a function:

  @
  'with' (\#param_name value) function
  @

  @
  'with' 'defaults' function
  @

  This is a prefix version of the ('!') operator.

  -}
  with :: Param p -> fn -> fn'

instance WithParam' (Decide p fn) p fn fn' => WithParam p fn fn' where
  with (Param p) fn = withParam @(Decide p fn) p fn
  {-# INLINE with #-}

data Defaults = Defaults

{- |
Passing 'defaults' to a function fills all unspecified optional parameters
with 'Nothing':

@
fn            :: "b" ':!' Bool -> "x" ':?' Char -> Int -> IO ()
fn '!' 'defaults' :: "b" ':!' Bool ->                Int -> IO ()
@

-}
defaults :: Param Defaults
defaults = Param Defaults

{- |

A proxy for a name, intended for use with @-XOverloadedLabels@:

@
#verbose :: Name "verbose"
@

-}

data Name (name :: Symbol) = Name

instance name ~ name' => IsLabel name' (Name name) where
#if MIN_VERSION_base(4,10,0)
  fromLabel = Name
#else
  fromLabel _ = Name
#endif
  {-# INLINE fromLabel #-}

{- |

'arg' unwraps a named parameter with the specified name. One way to use it is
to match on arguments with @-XViewPatterns@:

@
fn (arg \#t -> t) (arg \#f -> f) = ...
@

This way, the names of parameters can be inferred from the patterns: no type
signature for @fn@ is required. In case a type signature for @fn@ is
provided, the parameters must come in the same order:

@
fn :: "t" :! Integer -> "f" :! Integer -> ...
fn (arg \#t -> t) (arg \#f -> f) = ... -- ok
fn (arg \#f -> f) (arg \#t -> t) = ... -- does not typecheck
@

-}
arg :: Name name -> name :! a -> a
arg _ (ArgF (Identity a)) = a
{-# INLINE arg #-}

{- |

'argF' is similar to 'arg': it unwraps a named parameter with the specified name.
The difference is that the result of 'argF' is inside an arity wrapper,
which is 'Identity' for normal parameters and 'Maybe' for optional parameters.

-}
argF :: Name name -> NamedF f a name -> f a
argF _ (ArgF fa) = fa
{-# INLINE argF #-}

{- |

A variation of 'arg' for optional arguments. Requires a default value to handle
the case when the optional argument was omitted:

@
fn (argDef \#answer 42 -> ans) = ...
@

In case you want to get a value wrapped in 'Maybe' instead, use 'argF' or
'ArgF'.

-}
argDef :: Name name -> a -> name :? a -> a
argDef _ d (ArgF fa) = fromMaybe d fa

--------------------------------------------------------------------------------
-- The working horses of the library: Decide and WithParam'
--------------------------------------------------------------------------------

data DApply
data DFill
data DPass

type family Decide (p :: Type) (fn :: Type) :: [Type] where
  Decide (NamedF f' a' name) (NamedF f a name -> r) = '[DApply]
  Decide Defaults (NamedF Maybe a name -> r) = DFill : Decide Defaults r
  Decide p (x -> r) = DPass : Decide p r
  Decide (NamedF f' a' name) t =
    TypeError (Text "Named parameter '" :<>: Text name :<>:
               Text "' was supplied, but not expected")
  Decide Defaults t = '[]

class WithParam' (ds :: [Type]) p fn fn' | ds p fn -> fn' where
  withParam :: p -> fn -> fn'

instance fn ~ fn' => WithParam' '[] p fn fn' where
  withParam _ = id
  {-# INLINE withParam #-}

instance
    ( WithParam' ds p r r',
      fn ~ (p -> r),
      fn' ~ r'
    ) => WithParam' (DApply : ds) p fn fn'
  where
    withParam p fn = withParam @ds p (fn p)
    {-# INLINE withParam #-}

instance
    ( WithParam' ds p r r',
      fn ~ (x -> r),
      fn' ~ (x -> r')
    ) => WithParam' (DPass : ds) p fn fn'
  where
    withParam a fn = \x -> withParam @ds a (fn x)
    {-# INLINE withParam #-}

instance
    ( WithParam' ds p r r',
      fn ~ (NamedF f x name -> r),
      fn' ~ r',
      f ~ Maybe
    ) => WithParam' (DFill : ds) p fn fn'
  where
    withParam p fn = withParam @ds p (fn (ArgF Nothing))
    {-# INLINE withParam #-}