{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}

module Database.Ribbit.Table (
  Table(..),
  Field,
  (:>)(..),

  Flatten,
  ValidField,
  Validate,
  NotInSchema,
) where


import Data.Type.Bool (type (||), If)
import GHC.TypeLits (Symbol, TypeError, ErrorMessage(ShowType, (:<>:)))
import qualified GHC.TypeLits as Lit


{- |
  Type class for defining your own tables. The primary way for you to
  introduce a new schema is to instantiate this type class for one of
  your types.

  E.g.:

  > data MyTable
  > instance Table MyTable where
  >   type Name MyTable = "my_table"
  >   type DBSchema MyTable =
  >     Field "id" Int
  >     :> Field "my_non_nullable_text_field" Text
  >     :> Field "my_nullable_int_field" (Maybe Int)
    
-}
class Table relation where
  type Name relation :: Symbol
  type DBSchema relation


{- |
  Define a field in a database schema, where:

  - @name@: is the name of the database column, expressed as a type-level
    string literal, and

  - @typ@: is the Haskell type whose values get stored in the column.

  E.g:

  - @'Field' "company_name" 'Text'@
  - @'Field' "expiration_date" ('Maybe' 'Data.Time.Day')@

-}
data Field name typ


{- |
  String two types together. 'Int' ':>' 'Int' ':>' 'Int' is similar in
  principal to the nested tuple ('Int', ('Int', 'Int')), but looks a
  whole lot nicer when the number of elements becomes large.

  This is how you build up a schema from a collection of 'Field' types.

  E.g.:

  > Field "foo" Int
  > :> Field "bar" Text
  > :> Field "baz" (Maybe Text)

  It also the mechanism by which this library builds up the Haskell
  types for query parameters and resulting rows that get returned. So
  if you have a query that accepts three text query parameters, that
  type represented in Haskell is going to be @('Only' 'Text' ':>' 'Only'
  'Text' ':>' 'Only' 'Text')@.

  If that query returns rows that contain a Text, an Int, and a Text,
  then the type of the rows will be @('Only' 'Text' ':>' 'Only' 'Int'
  ':>' 'Only' 'Text')@.

-}
data a :> b = a :> b
  deriving (Eq, Ord, Show)
infixr 5 :>

{- |
  Normalize nested type strings to be right associative. Mainly used to
  help simplify the implementation of other type families.
-}
type family Flatten a where
  Flatten ((a :> b) :> c) = Flatten (a :> b :> c)
  Flatten (a :> b) = a :> Flatten b
  Flatten a = a


{- | Type level check to see if the field is actually contained in the schema -}
type family ValidField field schema where
  ValidField name (Field name typ) = 'True
  ValidField name (Field _ typ) = 'False
  ValidField name (a :> b) = ValidField name a || ValidField name b


type Validate field schema result =
  If (ValidField field schema)
    result
    (NotInSchema field schema)


type family NotInSchema field schema where
  NotInSchema field schema =
    TypeError (
      'Lit.Text "name ("
      ':<>: 'ShowType field
      ':<>: 'Lit.Text ") not found in schema: "
      ':<>: 'ShowType schema
    )