-- This file is part of HamSql
--
-- Copyright 2014-2016 by it's authors.
-- Some rights reserved. See COPYING, AUTHORS.
{-# LANGUAGE FlexibleInstances #-}

module Database.HamSql.Internal.Stmt.Table
  ( stmtsDropTableConstr
  , stmtsDropTableColumn
  ) where

import qualified Data.Text as T

import Database.HamSql.Internal.Stmt.Basic
import Database.HamSql.Internal.Stmt.Sequence()

-- | Assuming that CASCADE will only cause other constraints to be deleted.
-- | Required since foreign keys may depend on other keys.
stmtsDropTableConstr :: SqlObj SQL_TABLE_CONSTRAINT (SqlName, SqlName, SqlName)
                     -> [Maybe SqlStmt]
stmtsDropTableConstr x@(SqlObj _ (s, t, c)) =
  [ newSqlStmt SqlDropTableConstr x $
    "ALTER TABLE" <-> toSqlCode (s <.> t) <-> "DROP CONSTRAINT IF EXISTS" <->
    toSqlCode c <->
    "CASCADE"
  ]

stmtsDropTableColumn :: SqlObj SQL_COLUMN (SqlName, SqlName) -> [Maybe SqlStmt]
stmtsDropTableColumn x@(SqlObj _ (t, c)) =
  [ newSqlStmt SqlDropTableColumn x $
    "ALTER TABLE" <-> toSqlCode t <-> "DROP COLUMN" <-> toSqlCode c
  ]

constrId
  :: Schema
  -> Table
  -> SqlName
  -> SqlObj SQL_TABLE_CONSTRAINT (SqlName, SqlName, SqlName)
constrId s t c = SqlObj SQL_TABLE_CONSTRAINT (schemaName s, tableName t, c)

-- TODO: prefix with table name
stmtCheck
  :: ToSqlId a
  => a -> Check -> Maybe SqlStmt
stmtCheck obj c =
  newSqlStmt SqlCreateCheckConstr obj $
  "ALTER TABLE " <> sqlIdCode obj <> " ADD CONSTRAINT " <>
  toSqlCode (checkName c) <>
  " CHECK (" <>
  checkCheck c <>
  ")"

instance ToSqlStmts (SqlContext (Schema, Table, Column)) where
  toSqlStmts context obj@(SqlContext (schema, table, rawColumn)) =
    [ stmtAddColumn
    , stmtAlterColumnType
    , stmtDropDefault
    , stmtAddColumnDefault
    , stmtAlterColumnNull
    , stmtCommentOn obj (columnDescription c)
    , stmtAddForeignKey
    , stmtColumnUnique
    ] ++
    stmtsSerialSequence ++ stmtsAddColumnCheck
  -- ADD COLUMN
    where
      stmtAddColumn =
        newSqlStmt SqlAddColumn obj $
        "ALTER TABLE" <-> tblId <-> "ADD COLUMN" <-> toSqlCode (columnName c) <->
        toSqlCode (columnType c)
      -- UNIQUE
      stmtColumnUnique
        | columnUnique c == Just True =
          let constr = tableName table <> columnName c <> SqlName "key"
          in newSqlStmt SqlCreateUniqueConstr (constrId schema table constr) $
             "ALTER TABLE " <> tblId <> " ADD CONSTRAINT " <> toSqlCode constr <>
             " UNIQUE (" <>
             toSqlCode (columnName c) <>
             ")"
        | otherwise = Nothing
      -- NOT NULL
      stmtAlterColumnNull =
        stmtAlterColumn SqlAlterColumn $
        if columnNull c == Just True
          then "DROP NOT NULL"
          else "SET NOT NULL"
      -- SET DATA TYPE
      stmtAlterColumnType =
        stmtAlterColumn SqlAlterColumn $
        "SET DATA TYPE " <> toSqlCode (columnType c)
      -- DROP DEFAULT
      stmtDropDefault = stmtAlterColumn SqlDropColumnDefault "DROP DEFAULT"
      -- SET DEFAULT
      stmtAddColumnDefault = columnDefault c >>= sqlDefault
        where
          sqlDefault d = stmtAlterColumn SqlAddDefault $ "SET DEFAULT " <> d
      -- [CHECK]
      stmtsAddColumnCheck = maybeMap (stmtCheck obj) (columnChecks c)
      -- FOREIGN KEY
      stmtAddForeignKey =
        case columnReferences c of
          Nothing -> Nothing
          (Just ref) ->
            let constr = tableName table <> columnName c <> SqlName "fkey"
            in newSqlStmt
                 SqlCreateForeignKeyConstr
                 (constrId schema table constr) $
               "ALTER TABLE" <-> sqlIdCode obj <-> "ADD CONSTRAINT" <->
               toSqlCode constr <->
               "FOREIGN KEY (" <>
               toSqlCode (columnName c) <>
               ")" <->
               "REFERENCES" <->
               toSqlCode' (init $ expSqlName ref) <->
               "(" <>
               toSqlCode (last $ expSqlName ref) <>
               ")" <>
               maybePrefix " ON UPDATE " (columnOnRefUpdate c) <>
               maybePrefix " ON DELETE " (columnOnRefDelete c)
      -- CREATE SEQUENCE (for type SERIAL)
      stmtsSerialSequence
        | columnIsSerial = toSqlStmts context serialSequenceContext
        | otherwise = [Nothing]
      -- Helpers
      stmtAlterColumn t x =
        newSqlStmt t obj $
        "ALTER TABLE " <> tblId <> " ALTER COLUMN " <> toSqlCode (columnName c) <>
        " " <>
        x
      columnIsSerial = toSqlCode (columnType rawColumn) == "SERIAL"
      c
        | columnIsSerial =
          rawColumn
          { columnType = SqlType "integer"
          , columnDefault =
            Just $
            "nextval('" <> toSqlCode (sqlId serialSequenceContext) <> "')"
          }
        | otherwise = rawColumn
      tblId = toSqlCode $ schemaName schema <.> tableName table
      serialSequenceContext =
        SqlContext
          ( schema
          , Sequence
            -- sequenceName follows PostgreSQL internal convention
            { sequenceName = tableName table <> columnName c <> SqlName "_seq"
            , sequenceIncrement = Nothing
            , sequenceMinValue = Nothing
            , sequenceMaxValue = Nothing
            , sequenceStartValue = Nothing
            , sequenceCache = Nothing
            , sequenceCycle = Nothing
            , sequenceOwnedByColumn = Just $ SqlName $ sqlIdCode obj
            })

instance ToSqlStmts (SqlContext (Schema, Table)) where
  toSqlStmts SetupContext {setupContextSetup = setup} obj@(SqlContext (s, t)) =
    [ stmtCreateTable
      -- table comment
    , stmtCommentOn obj (tableDescription t)
    ] ++
    maybeMap (stmtCheck obj) (tableChecks t) ++
    -- grant rights to roles
    maybeMap (sqlGrant "SELECT") (tablePrivSelect t) ++
    maybeMap (sqlGrant "UPDATE") (tablePrivUpdate t) ++
    maybeMap (sqlGrant "INSERT") (tablePrivInsert t) ++
    maybeMap (sqlGrant "DELETE") (tablePrivDelete t) ++
    -- primary key
    [sqlAddPrimaryKey (tablePrimaryKey t)] ++
    -- mult column unique
    maybeMap sqlUniqueConstr (tableUnique t) ++
    -- inheritance
    maybeMap sqlAddInheritance (tableInherits t) ++
    -- multi column FKs
    maybeMap sqlAddForeignKey' (tableForeignKeys t)
    where
      stmtCreateTable =
        newSqlStmt SqlCreateTable obj $
        "CREATE TABLE IF NOT EXISTS" <-> sqlIdCode obj <> " ()"
      -- PRIMARY KEY
      sqlAddPrimaryKey :: [SqlName] -> Maybe SqlStmt
      sqlAddPrimaryKey [] = Nothing
      sqlAddPrimaryKey ks =
        let constr = tableName t <> SqlName "pkey"
        in newSqlStmt SqlCreatePrimaryKeyConstr (constrId s t constr) $
           "ALTER TABLE " <> sqlIdCode obj <> " ADD CONSTRAINT " <> toSqlCode constr <>
           " PRIMARY KEY (" <>
           T.intercalate ", " (map toSqlCode ks) <>
           ")"
      -- TODO: allow empty name with "mconcat (uniquekeyColumns ks)"
      sqlUniqueConstr :: UniqueKey -> Maybe SqlStmt
      sqlUniqueConstr ks =
        let constr = tableName t <> uniquekeyName ks
        in newSqlStmt SqlCreateUniqueConstr (constrId s t constr) $
           "ALTER TABLE " <> sqlIdCode obj <> " ADD CONSTRAINT " <> toSqlCode constr <>
           " UNIQUE (" <>
           T.intercalate ", " (map toSqlCode (uniquekeyColumns ks)) <>
           ")"
      sqlAddForeignKey' :: ForeignKey -> Maybe SqlStmt
      sqlAddForeignKey' fk =
        let constr = tableName t <> foreignkeyName fk
        in newSqlStmt SqlCreateForeignKeyConstr (constrId s t constr) $
           "ALTER TABLE " <> sqlIdCode obj <> " ADD CONSTRAINT " <> toSqlCode constr <>
           " FOREIGN KEY (" <>
           T.intercalate ", " (map toSqlCode (foreignkeyColumns fk)) <>
           ")" <>
           " REFERENCES " <>
           toSqlCode (foreignkeyRefTable fk) <>
           " (" <>
           T.intercalate ", " (map toSqlCode $ foreignkeyRefColumns fk) <>
           ")" <>
           maybePrefix " ON UPDATE " (foreignkeyOnUpdate fk) <>
           maybePrefix " ON DELETE " (foreignkeyOnDelete fk)
      sqlGrant right role =
        newSqlStmt
          SqlPriv
          obj
          ("GRANT " <> right <> " ON TABLE " <> toSqlCode (tableName t) <>
           " TO " <>
           prefixedRole setup role)
      sqlAddInheritance :: SqlName -> Maybe SqlStmt
      sqlAddInheritance n =
        newSqlStmt SqlAlterTable obj $
        "ALTER TABLE " <> sqlIdCode obj <> " INHERIT " <> toSqlCode n