{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE FlexibleInstances #-}
{-|
Module      : Pasta
Description : Assembles SQL statements
-}
module Pasta
    ( target
    , assignments
    , conditions
    , insert
    , columns
    , values
    , update
    , delete
    , returning
    , onConflict
    , doNothing
    , doUpdate
    , (.=)
    , (//)
    , select
    , selectExp
    , selectFrom
    , selectFunction
    , t
    , f
    , relations
    , toSQL
    , NonEmpty (..)
    , fromList
    , Expression(Null)
    , BooleanExpression(Not, In)
    , (.|)
    , (.&)
    , (.!)
    , cmp
    , eq
    , gt
    , lt
    , gte
    , lte
    , fn
    , now
    , age
    ) where

import Protolude hiding ((&))
import Pasta.Types
import Lens.Micro
import Lens.Micro.TH
import Data.List.NonEmpty (NonEmpty(..), fromList)
import qualified Data.List.NonEmpty as NE
import qualified Data.Text as T

makeFields ''Select
makeFields ''Update
makeFields ''Delete
makeFields ''Insert

-- | Builds a SELECT null with neither FROM nor WHERE clauses.
select :: Select
select = Select (Column Null :| []) [] t

-- | Builds a SELECT * FROM table statement.
selectFrom :: Name -> Select
selectFrom table = select & columns .~ ("*" :| []) & relations .~ [FromRelation (NameExp table) table]

-- | Builds a SELECT expression with neither FROM nor WHERE clauses
selectExp :: Expression -> Select
selectExp expr = select & columns .~ (Column expr :| [])

-- | Builds a SELECT fn(parameters) with neither FROM nor WHERE clauses
selectFunction :: Identifier -> [Expression] -> Select
selectFunction fnId parameters = selectExp $ fn fnId parameters

-- | Builds an INSERT statement using a target, a non-empty list of column names and a non-empty list of values
insert :: T.Text -> NonEmpty T.Text -> NonEmpty T.Text -> Insert
insert trg cols vals = Insert (Identifier schema table) colNames valExps Nothing
  where
    (schema, table) = splitTarget trg
    colNames = Name <$> cols
    valExps = (LitExp . Literal) <$> vals

-- | Builds an UPDATE statement using a target, a non-empty list of column names and a non-empty list of values
update :: T.Text -> NonEmpty T.Text -> NonEmpty Expression -> Update
update trg cols vals = Update (Identifier schema table) assigns t []
  where
    (schema, table) = splitTarget trg
    assigns = NE.zipWith Assignment (Name <$> cols) vals

-- | Builds a DELETE statement using a target
delete :: T.Text -> Delete
delete trg = Delete (schema//table) t []
  where
    (schema, table) = splitTarget trg

-- | Builds a BooleanExpression out of an operator and 2 expressions
cmp :: (IsExpression lexp, IsExpression rexp) => Text -> lexp -> rexp -> BooleanExpression
cmp op lexp rexp = Comparison (Operator op) (toExp lexp) (toExp rexp)

-- | Builds a equality comparison out of two expressions
eq :: (IsExpression lexp, IsExpression rexp) => lexp -> rexp -> BooleanExpression
eq = cmp "="

-- | Builds a greater than comparison out of two expressions
gt :: (IsExpression lexp, IsExpression rexp) => lexp -> rexp -> BooleanExpression
gt = cmp ">"

-- | Builds a lesser than comparison out of two expressions
lt :: (IsExpression lexp, IsExpression rexp) => lexp -> rexp -> BooleanExpression
lt = cmp "<"

-- | Builds a greater than or equal comparison out of two expressions
gte :: (IsExpression lexp, IsExpression rexp) => lexp -> rexp -> BooleanExpression
gte = cmp ">="

-- | Builds a lesser than or equal comparison out of two expressions
lte :: (IsExpression lexp, IsExpression rexp) => lexp -> rexp -> BooleanExpression
lte = cmp "<="

-- | Builds a function
fn :: Identifier -> [Expression] -> Expression
fn = FunctionExp

-- | Builds a now() function
now :: Expression
now = fn ("pg_catalog"//"now") []

-- | Builds an age(t) function
age :: IsExpression exp => exp -> Expression
age time = fn ("pg_catalog"//"age") [toExp time]


-- | Just a convenient way to write a BoolLiteral True
t :: BooleanExpression
t = BoolLiteral True

-- | Just a convenient way to write a BoolLiteral False
f :: BooleanExpression
f = BoolLiteral False

-- | Used for conflict resolution when we don't want the conflict to trigger any exception
doNothing :: Maybe Conflict
doNothing = Just $ Conflict Nothing DoNothing

-- | Used for conflict resolution when we want the conflict to update some column
doUpdate :: ConflictTarget -> [Assignment] -> Maybe Conflict
doUpdate _ [] = Nothing
doUpdate trg assigns =
  Just $
  Conflict (Just trg) $
  DoUpdate (fromList assigns) t

-- | Assignment operator creates SQL assignments like in conflict resolution rules
(.=) :: IsExpression exp => Name -> exp -> Assignment
(.=) e ex = Assignment e $ toExp ex

-- | Identifier builder, takes two names and builds a qualified identifier (e.g. "information_schema"."tables")
(//) :: Name -> Name -> Identifier
(//) = Identifier

-- | Boolean OR
(.|) :: BooleanExpression -> BooleanExpression -> BooleanExpression
(.|) = Or

-- | Boolean AND
(.&) :: BooleanExpression -> BooleanExpression -> BooleanExpression
(.&) = And

-- | Boolean NOT
infixr 0 .!
(.!) :: BooleanExpression -> BooleanExpression
(.!) = Not

-- private functions

splitTarget :: T.Text -> (Name, Name)
splitTarget trg = (schema, table)
  where
    qId = Name <$> T.split (=='.') trg
    schema = case qId of
              [s, _] -> s
              _ -> "public"
    table = case qId of
              [_, tbl] -> tbl
              [tbl] -> tbl
              _ -> ""