content: {-# LANGUAGE TemplateHaskell #-}
  
  module Database.Relational.Schema.SQLite3 (
    module Database.Relational.Schema.SQLite3.Config,
  
    getType, normalizeColumn, normalizeType, notNull,
    tableInfoQuerySQL, indexListQuerySQL, indexInfoQuerySQL
    ) where
  
  import qualified Data.Map as Map
  import qualified Database.Relational.Schema.SQLite3.TableInfo as TableInfo
  
  import Language.Haskell.TH (TypeQ)
  import Control.Arrow (first)
  import Control.Applicative ((<|>))
  import Data.ByteString (ByteString)
  import Data.Char (toLower, toUpper)
  import Data.Int (Int8, Int16, Int32, Int64)
  import Data.Map (Map)
  import Data.Time (Day, LocalTime)
  import Database.Relational (Query, unsafeTypedQuery)
  
  import Database.Relational.Schema.SQLite3.Config
  import Database.Relational.Schema.SQLite3.IndexInfo
  import Database.Relational.Schema.SQLite3.IndexList
  import Database.Relational.Schema.SQLite3.TableInfo
  
  --{-# ANN module "HLint: ignore Redundant $" #-}
  
  -- <https://www.sqlite.org/datatype3.html>
  -- SQLite3 is dynamic typing,
  -- so assign narrower constraints in this default mapping.
  -- Using upper case typenames along with SQLite3 document.
  mapFromSqlDefault :: Map String TypeQ
  mapFromSqlDefault =
      Map.fromList [ ("INT",        [t|Int32|])
                   , ("INTEGER",    [t|Int32|])
                   , ("TINYINT",    [t|Int8|])
                   , ("SMALLINT",   [t|Int16|])
                   , ("MEDIUMINT",  [t|Int32|])
                   , ("BIGINT",     [t|Int64|])
                   , ("INT2",       [t|Int16|])
                   , ("INT8",       [t|Int64|])
  
                   , ("CHARACTER",  [t|String|])
                   , ("VARCHAR",    [t|String|])
                   , ("TEXT",       [t|String|])
  
                   , ("BLOB",       [t|ByteString|])
  
                   , ("REAL",       [t|Double|])
                   , ("DOUBLE",     [t|Double|])
                   , ("FLOAT",      [t|Float|])
  
                   , ("DATE",       [t|Day|])
                   , ("DATETIME",   [t|LocalTime|])
                   ]
  
  normalizeColumn :: String -> String
  normalizeColumn = map toLower
  
  normalizeType :: String -> String
  normalizeType = map toUpper . takeWhile (not . flip elem " (")
  
  notNull :: TableInfo -> Bool
  notNull info = isTrue . TableInfo.notnull $ info
    where
      isTrue 0 = False
      isTrue _ = True
  
  -- for backward compatibility
  normalizeMap :: Map String TypeQ -> Map String TypeQ
  normalizeMap = Map.fromList . map (first $ map toUpper) . Map.toList
  
  getType :: Map String TypeQ -> TableInfo -> Maybe (String, TypeQ)
  getType mapFromSql info = do
      typ <- Map.lookup key (normalizeMap {- for backward compatibility -} mapFromSql)
             <|>
             Map.lookup key mapFromSqlDefault
      return (normalizeColumn (TableInfo.name info), mayNull typ)
    where
      key = normalizeType . TableInfo.ctype $ info
      mayNull typ = if notNull info
                      then typ
                      else [t|Maybe $(typ)|]
  
  tableInfoQuerySQL :: String -> String -> Query () TableInfo
  tableInfoQuerySQL db tbl = unsafeTypedQuery $ "pragma " ++ db ++ ".table_info(" ++ tbl ++ ");"
  
  indexListQuerySQL :: String -> String -> Query () IndexList
  indexListQuerySQL db tbl = unsafeTypedQuery $ "pragma " ++ db ++ ".index_list(" ++ tbl ++ ");"
  
  indexInfoQuerySQL :: String -> String -> Query () IndexInfo
  indexInfoQuerySQL db idx = unsafeTypedQuery $ "pragma " ++ db ++ ".index_info(" ++ idx ++ ");"
  