{- This file is part of Vervis.
 -
 - Written in 2016 by fr33domlover <fr33domlover@riseup.net>.
 -
 - ♡ Copying is an act of love. Please copy, reuse and share.
 -
 - The author(s) have dedicated all copyright and related and neighboring
 - rights to this software to the public domain worldwide. This software is
 - distributed without any warranty.
 -
 - You should have received a copy of the CC0 Public Domain Dedication along
 - with this software. If not, see
 - <http://creativecommons.org/publicdomain/zero/1.0/>.
 -}

-- | SQL schema backend specifying SQL statements for manipulating a SQL
-- database's table schema.
module Database.Persist.Schema.Sql
    ( TableName (..)
    , ColumnName (..)
    , ConstraintName (..)
    , Column (..)
    , SchemaBackend (..)
    )
where

import Prelude

import Control.Monad (when)
import Control.Monad.IO.Class (MonadIO)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Reader (ask)
import Data.Char (isUpper, toLower)
import Data.Foldable (traverse_)
import Data.Maybe (isJust)
import Data.Text (Text)
import Database.Persist.Sql (Sql, SqlBackend, rawExecute)
import Database.Persist.Types (SqlType)

import qualified Data.Text as T

import Database.Persist.Schema

newtype TableName = TableName { unTableName :: Text }

newtype ColumnName = ColumnName { unColumnName :: Text }

newtype ConstraintName = ConstraintName { unConstraintName :: Text }

data Column = Column
    { colName :: ColumnName
    , colType :: SqlType
    , colNull :: MaybeNull
    }

exec :: MonadIO m => Sql -> SchemaT SqlBackend m ()
exec t = lift $ rawExecute t []

camelWords :: Text -> [Text]
camelWords ident =
    let low = toLower
        slow = T.singleton . toLower
        go c t l =
            let (x, y) = T.break isUpper t
            in  case (T.null x, T.uncons y) of
                    (True,  Nothing)     -> slow c : l
                    (True,  Just (d, r)) -> go d r $ slow c : l
                    (False, Nothing)     -> (low c `T.cons` x) : l
                    (False, Just (d, r)) -> go d r $ (low c `T.cons` x) : l
        (a, b) = T.break isUpper ident
    in  reverse $ case (T.null a, T.uncons b) of
            (True,  Nothing)     -> []
            (True,  Just (c, r)) -> go c r []
            (False, Nothing)     -> [a]
            (False, Just (c, r)) -> go c r [a]

dbname :: Text -> Text
dbname = T.intercalate (T.singleton '_') . camelWords

entity2table :: EntityName -> TableName
entity2table (EntityName t) = TableName $ dbname t

field2column :: FieldName -> ColumnName
field2column (FieldName t) = ColumnName $ dbname t

unique2constraint :: UniqueName -> ConstraintName
unique2constraint (UniqueName t) = ConstraintName $ dbname t

type2sql :: SchemaBackend SqlBackend -> FieldType -> SqlType
type2sql _   (FTPrim t) = t
type2sql ssb FTRef      = ssbRefType ssb

mkcolumn :: SchemaBackend SqlBackend -> Field -> Column
mkcolumn ssb (Field name typ mnull) = Column
    { colName = field2column name
    , colType = type2sql ssb typ
    , colNull = mnull
    }

instance PersistSchema SqlBackend where
    data SchemaBackend SqlBackend = SqlSchemaBackend
        { ssbRefType        :: SqlType
        , ssbCreateTable    :: TableName -> [Column] -> Sql
        , ssbRenameTable    :: TableName -> TableName -> Sql
        , ssbDropTable      :: TableName -> Sql
        , ssbAddColumn      :: TableName -> Column -> Maybe Text -> Sql
        , ssbRenameColumn   :: TableName -> ColumnName -> ColumnName -> Sql
        , ssbRetypeColumn   :: TableName -> ColumnName -> SqlType -> Sql
        , ssbRenullColumn   :: TableName -> ColumnName -> MaybeNull -> Sql
        , ssbUnnullColumn   :: TableName -> ColumnName -> Text -> Sql
        , ssbDefColumn      :: TableName -> ColumnName -> Text -> Sql
        , ssbUndefColumn    :: TableName -> ColumnName -> Sql
        , ssbDropColumn     :: TableName -> ColumnName -> Sql
        , ssbAddUnique
            :: TableName -> ConstraintName -> [ColumnName] -> Sql
        , ssbAddForeignKey
            :: TableName -> ConstraintName -> ColumnName -> TableName -> Sql
        , ssbRenameConstraint
            :: TableName -> ConstraintName -> ConstraintName -> Sql
        , ssbDropConstraint :: TableName -> ConstraintName -> Sql
        }
    addEntity (Entity name fields uniques) = do
        ssb <- ask
        exec $
            ssbCreateTable ssb (entity2table name) (map (mkcolumn ssb) fields)
        traverse_ (addUnique name) uniques
    removeEntity name = do
        ssb <- ask
        exec $ ssbDropTable ssb $ entity2table name
    addField ent (Field name typ mnull) mdef = do
        ssb <- ask
        exec $
            ssbAddColumn ssb
                (entity2table ent)
                (Column (field2column name) (type2sql ssb typ) mnull)
                mdef
        when (isJust mdef) $
            exec $
            ssbUndefColumn ssb (entity2table ent) (field2column name)
    renameField entity old new = do
        ssb <- ask
        exec $
            ssbRenameColumn ssb
                (entity2table entity)
                (field2column old)
                (field2column new)
    removeField entity field = do
        ssb <- ask
        exec $ ssbDropColumn ssb (entity2table entity) (field2column field)
    addUnique entity (Unique name fields) = do
        ssb <- ask
        exec $
            ssbAddUnique ssb
                (entity2table entity)
                (unique2constraint name)
                (map field2column fields)
    renameUnique entity old new = do
        ssb <- ask
        exec $
            ssbRenameConstraint ssb
                (entity2table entity)
                (unique2constraint old)
                (unique2constraint new)
    removeUnique entity name = do
        ssb <- ask
        exec $
            ssbDropConstraint ssb
                (entity2table entity)
                (unique2constraint name)