{-|
Module: Squeal.PostgreSQL.Expression.Parameter
Description: out-of-line parameters
Copyright: (c) Eitan Chatav, 2019
Maintainer: eitan@morphism.tech
Stability: experimental

out-of-line parameters
-}

{-# LANGUAGE
    AllowAmbiguousTypes
  , DataKinds
  , FlexibleContexts
  , FlexibleInstances
  , FunctionalDependencies
  , GADTs
  , KindSignatures
  , MultiParamTypeClasses
  , OverloadedStrings
  , RankNTypes
  , ScopedTypeVariables
  , TypeApplications
  , TypeFamilies
  , TypeOperators
  , UndecidableInstances
#-}

module Squeal.PostgreSQL.Expression.Parameter
  ( -- * Parameter
    HasParameter (parameter)
  , param
    -- * Parameter Internals
  , HasParameter'
  , ParamOutOfBoundsError
  , ParamTypeMismatchError
  ) where

import Data.Kind (Constraint)
import GHC.Exts (Any)
import GHC.TypeLits

import Squeal.PostgreSQL.Expression
import Squeal.PostgreSQL.Expression.Type
import Squeal.PostgreSQL.Render
import Squeal.PostgreSQL.Type.Schema

-- $setup
-- >>> import Squeal.PostgreSQL

{- | A `HasParameter` constraint is used to indicate a value that is
supplied externally to a SQL statement.
`Squeal.PostgreSQL.Session.manipulateParams`,
`Squeal.PostgreSQL.Session.queryParams` and
`Squeal.PostgreSQL.Session.traversePrepared` support specifying data values
separately from the SQL command string, in which case `param`s are used to
refer to the out-of-line data values.
-}
class KnownNat ix => HasParameter
  (ix :: Nat)
  (params :: [NullType])
  (ty :: NullType)
  | ix params -> ty where
    -- | `parameter` takes a `Nat` using type application and a `TypeExpression`.
    --
    -- >>> printSQL (parameter @1 int4)
    -- ($1 :: int4)
    parameter
      :: TypeExpression db ty
      -> Expression grp lat with db params from ty
    parameter TypeExpression db ty
ty = forall (grp :: Grouping) (lat :: FromType) (with :: FromType)
       (db :: SchemasType) (params :: [NullType]) (from :: FromType)
       (ty :: NullType).
ByteString -> Expression grp lat with db params from ty
UnsafeExpression forall a b. (a -> b) -> a -> b
$ ByteString -> ByteString
parenthesized forall a b. (a -> b) -> a -> b
$
      ByteString
"$" forall a. Semigroup a => a -> a -> a
<> forall (n :: Nat). KnownNat n => ByteString
renderNat @ix ByteString -> ByteString -> ByteString
<+> ByteString
"::"
        ByteString -> ByteString -> ByteString
<+> forall sql. RenderSQL sql => sql -> ByteString
renderSQL TypeExpression db ty
ty

-- we could do the check for 0 in @HasParameter'@, but this way forces checking 'ix' before delegating,
-- which has the nice effect of ambiguous 'ix' errors mentioning 'HasParameter' instead of @HasParameter'@
instance {-# OVERLAPS #-} (TypeError ('Text "Tried to get the param at index 0, but params are 1-indexed"), x ~ Any) => HasParameter 0 params x
instance {-# OVERLAPS #-} (KnownNat ix, HasParameter' ix params ix params x) => HasParameter ix params x

-- | @HasParameter'@ is an implementation detail of 'HasParameter' allowing us to
-- include the full parameter list in our errors.
class KnownNat ix => HasParameter'
  (originalIx :: Nat)
  (allParams :: [NullType])
  (ix :: Nat)
  (params :: [NullType])
  (ty :: NullType)
  | ix params -> ty where
instance {-# OVERLAPS #-}
  ( params ~ (y ': xs)
  , y ~ x -- having a separate 'y' type variable is required for 'ParamTypeMismatchError'
  , ParamOutOfBoundsError originalIx allParams params
  , ParamTypeMismatchError originalIx allParams x y
  ) => HasParameter' originalIx allParams 1 params x
instance {-# OVERLAPS #-}
  ( KnownNat ix
  , HasParameter' originalIx allParams (ix-1) xs x
  , params ~ (y ': xs)
  , ParamOutOfBoundsError originalIx allParams params
  )
  => HasParameter' originalIx allParams ix params x

-- | @ParamOutOfBoundsError@ reports a nicer error with more context when we try to do an out-of-bounds lookup successfully do a lookup but
-- find a different field than we expected, or when we find ourself out of bounds
type family ParamOutOfBoundsError (originalIx :: Nat) (allParams :: [NullType]) (params :: [NullType]) :: Constraint where
  ParamOutOfBoundsError originalIx allParams '[] = TypeError
    ('Text "Index " ':<>: 'ShowType originalIx ':<>: 'Text " is out of bounds in 1-indexed parameter list:" ':$$: 'ShowType allParams)
  ParamOutOfBoundsError _ _ _ = ()

-- | @ParamTypeMismatchError@ reports a nicer error with more context when we successfully do a lookup but
-- find a different field than we expected, or when we find ourself out of bounds
type family ParamTypeMismatchError  (originalIx :: Nat) (allParams :: [NullType]) (found :: NullType) (expected :: NullType) :: Constraint where
  ParamTypeMismatchError _ _ found found = ()
  ParamTypeMismatchError originalIx allParams found expected = TypeError
    (     'Text "Type mismatch when looking up param at index " ':<>: 'ShowType originalIx
    ':$$: 'Text "in 1-indexed parameter list:"
    ':$$: 'Text "  " ':<>: 'ShowType allParams
    ':$$: 'Text ""
    ':$$: 'Text "Expected: " ':<>: 'ShowType expected
    ':$$: 'Text "But found: " ':<>: 'ShowType found
    ':$$: 'Text ""
    )

-- | `param` takes a `Nat` using type application and for basic types,
-- infers a `TypeExpression`.
--
-- >>> printSQL (param @1 @('Null 'PGint4))
-- ($1 :: int4)
param
  :: forall n ty lat with db params from grp
   . (NullTyped db ty, HasParameter n params ty)
  => Expression grp lat with db params from ty -- ^ param
param :: forall (n :: Nat) (ty :: NullType) (lat :: FromType)
       (with :: FromType) (db :: SchemasType) (params :: [NullType])
       (from :: FromType) (grp :: Grouping).
(NullTyped db ty, HasParameter n params ty) =>
Expression grp lat with db params from ty
param = forall (ix :: Nat) (params :: [NullType]) (ty :: NullType)
       (db :: SchemasType) (grp :: Grouping) (lat :: FromType)
       (with :: FromType) (from :: FromType).
HasParameter ix params ty =>
TypeExpression db ty -> Expression grp lat with db params from ty
parameter @n (forall (db :: SchemasType) (ty :: NullType).
NullTyped db ty =>
TypeExpression db ty
nulltype @db)