{-# OPTIONS -fglasgow-exts #-}
{-| Module      :  Database.HSQL.ODBC
    Copyright   :  (c) Krasimir Angelov 2003
    License     :  BSD-style

    Maintainer  :  kr.angelov@gmail.com
    Stability   :  provisional
    Portability :  portable

    The module provides interface to ODBC
-}
module Database.HSQL.ODBC(connect, driverConnect
                         ,module Database.HSQL) where

import Database.HSQL
import Database.HSQL.Types
import Foreign(toBool,Ptr,allocaBytes,alloca,nullPtr,peek
              ,newForeignPtr,withForeignPtr)
import Foreign.C(withCString,withCStringLen)
import Control.Concurrent.MVar(newMVar)
import System.IO.Unsafe(unsafePerformIO)

import DB.HSQL.ODBC.Type(HDBC,SQLRETURN,HENV,HENVRef,mkSqlType)
import DB.HSQL.ODBC.Functions
import DB.HSQL.ODBC.Core(handleSqlResult,withStatement)

------------------------------------------------------------------------------
-- Connect/Disconnect
------------------------------------------------------------------------------
-- | Makes a new connection to the ODBC data source
connect :: String               -- ^ Data source name
        -> String               -- ^ User identifier
        -> String               -- ^ Authentication string (password)
        -> IO Connection        -- ^ the returned value represents
                                -- the new connection
connect server user authentication = connectHelper $ \hDBC ->
        withCString server $ \pServer ->
	withCString user $ \pUser ->
	withCString authentication $ \pAuthentication ->
	sqlConnect hDBC 
                   pServer (fromIntegral sqlNts)
                   pUser (fromIntegral sqlNts)
                   pAuthentication (fromIntegral sqlNts)

-- | 'driverConnect' is an alternative to 'connect'. It supports data sources
-- that require more connection information than the three arguments in
-- 'connect' and data sources that are not defined in the system information.
driverConnect :: String               -- ^ Connection string
              -> IO Connection        -- ^ the returned value represents
                                      -- the new connection
driverConnect connString = connectHelper $ \hDBC -> 
    withCString connString $ \pConnString ->
	allocaBytes 1024 $ \pOutConnString ->
	    alloca $ \pLen ->
	        sqlDriverConnect hDBC 
                                 nullPtr 
                                 pConnString 
                                 sqlNts 
                                 pOutConnString 1024 
                                 pLen 
                                 sqlDriverNoPrompt

-- |
connectHelper :: (HDBC -> IO SQLRETURN) -> IO Connection
connectHelper connectFunction = withForeignPtr myEnvironment $ \hEnv -> do
  hDBC <- alloca $ \ (phDBC :: Ptr HDBC) -> do
    res <- sqlAllocConnect hEnv phDBC
    handleSqlResult sqlHandleEnv hEnv res
    peek phDBC
  res <- connectFunction hDBC
  handleSqlResult sqlHandleDbc hDBC res
  refFalse <- newMVar False
  let connection 
        = Connection { connDisconnect = odbcDisconnect hDBC
		     , connExecute    = odbcExecute hDBC
		     , connQuery      = odbcQuery connection hDBC
		     , connTables     = odbcTables connection hDBC
		     , connDescribe   = odbcDescribe connection hDBC
		     , connBeginTransaction = 
                         beginTransaction myEnvironment hDBC
		     , connCommitTransaction = 
                         commitTransaction myEnvironment hDBC
		     , connRollbackTransaction = 
                         rollbackTransaction myEnvironment hDBC
		     , connClosed     = refFalse }
  return connection

{-|

-}
odbcDisconnect :: HDBC -- ^ ODBC handle
               -> IO ()
odbcDisconnect hDBC = do
  sqlDisconnect hDBC >>= handleSqlResult sqlHandleDbc hDBC
  sqlFreeConnect hDBC >>= handleSqlResult sqlHandleDbc hDBC

{-|

-}
odbcExecute :: HDBC -- ^ ODBC handle
            -> SQL -- ^ SQL Query
            -> IO ()
odbcExecute hDBC query = allocaBytes sizeOfHStmt $ \pStmt -> do
  res <- sqlAllocStmt hDBC pStmt
  handleSqlResult sqlHandleDbc hDBC res
  hSTMT <- peek pStmt
  withCStringLen query $ \(pQuery,len) -> do
    res <- sqlExecDirect hSTMT pQuery len
    handleSqlResult sqlHandleStmt hSTMT res
  res <- sqlFreeStmt hSTMT sqlDrop
  handleSqlResult sqlHandleStmt hSTMT res

{-|

-}
odbcQuery :: Connection 
          -> HDBC -- ^ ODBC handle
          -> String -- ^ SQL Query
          -> IO Statement
odbcQuery connection hDBC q = 
    withStatement connection hDBC doQuery
    where doQuery hSTMT = 
              withCStringLen q (uncurry (sqlExecDirect hSTMT))

{-|

-}
odbcTables :: Connection 
           -> HDBC -- ^ ODBC handle
           -> IO [String]
odbcTables connection hDBC = do
  stmt <- withStatement connection hDBC sqlTables'
  -- SQLTables returns (column names may vary):
  -- Column name     #   Type
  -- TABLE_NAME      3   VARCHAR
  collectRows (\s -> getFieldValue s "TABLE_NAME") stmt
  where sqlTables' hSTMT = sqlTables hSTMT nullPtr 0 
                                           nullPtr 0 
                                           nullPtr 0 
                                           nullPtr 0

{-|
-}
odbcDescribe :: Connection 
             -> HDBC -- ^ ODBC handle
             -> String -- ^ table name
             -> IO [ColDef]
odbcDescribe connection hDBC table = do
  stmt <- withStatement connection hDBC (odbcSqlColumns table)
  collectRows getColumnInfo stmt

-- |
odbcSqlColumns table hSTMT =
    withCStringLen table (\(pTable,len) ->
	sqlColumns hSTMT nullPtr 0 
                         nullPtr 0 
                         pTable (fromIntegral len) 
                         nullPtr 0)

-- | SQLColumns returns (column names may vary):
-- Column name     #   Type
-- COLUMN_NAME     4   Varchar not NULL
-- DATA_TYPE       5   Smallint not NULL
-- COLUMN_SIZE     7   Integer
-- DECIMAL_DIGITS  9   Smallint
-- NULLABLE       11   Smallint not NULL
getColumnInfo stmt = do
  column_name <- getFieldValue stmt "COLUMN_NAME"
  (data_type::Int) <- getFieldValue stmt "DATA_TYPE"
  (column_size::Int) <- getFieldValue' stmt "COLUMN_SIZE" 0
  (decimal_digits::Int) <- getFieldValue' stmt "DECIMAL_DIGITS" 0
  let sqlType = mkSqlType (fromIntegral data_type) 
                          (fromIntegral column_size) 
                          (fromIntegral decimal_digits)
  (nullable::Int) <- getFieldValue stmt "NULLABLE"
  return (column_name, sqlType, toBool nullable)

------------------------------------------------------------------------------
-- transaction management
------------------------------------------------------------------------------
{-|
-}
beginTransaction:: HENVRef-> HDBC-> IO ()
beginTransaction myEnvironment hDBC = do
  sqlSetConnectOption hDBC sqlAutoCommit sqlAutoCommitOff
  return ()

{-|
-}
commitTransaction:: HENVRef-> HDBC-> IO ()
commitTransaction myEnvironment hDBC = 
    withForeignPtr myEnvironment $ \hEnv -> do
      sqlTransact hEnv hDBC sqlCommit
      sqlSetConnectOption hDBC sqlAutoCommit sqlAutoCommitOn
      return ()

{-|
-}
rollbackTransaction:: HENVRef-> HDBC-> IO ()
rollbackTransaction myEnvironment hDBC = 
    withForeignPtr myEnvironment $ \hEnv -> do
      sqlTransact hEnv hDBC sqlRollback
      sqlSetConnectOption hDBC sqlAutoCommit sqlAutoCommitOn
      return ()

------------------------------------------------------------------------------
-- keeper of HENV
------------------------------------------------------------------------------
{-|
-}
{-# NOINLINE myEnvironment #-}
myEnvironment :: HENVRef
myEnvironment = unsafePerformIO $ alloca $ \ (phEnv :: Ptr HENV) -> do
  res <- sqlAllocEnv phEnv
  hEnv <- peek phEnv
  handleSqlResult 0 nullPtr res
  newForeignPtr sqlFreeEnv_p hEnv

