{-- |
Module      : Commons
Description : Common definitions used by other modules
Copyright   : (c) Mihai Giurgeanu, 2017
License     : GPL-3
Maintainer  : mihai.giurgeanu@gmail.com
Stability   : experimental
Portability : Portable
--}

module Database.TransferDB.Commons where

import Prelude hiding (fail, log)

import Control.Monad.Trans.Maybe (MaybeT(MaybeT), runMaybeT)
import Control.Monad.Trans.Reader (ReaderT, runReaderT, ask, asks)
import Control.Monad.Trans.Class (lift)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Fail (MonadFail, fail)
import Control.Logging(loggingLogger, LogLevel(LevelError), log)

import Data.Text (Text)
import Data.String (fromString)

import SQL.CLI (SQLHENV, SQLHDBC, sql_handle_env, sql_handle_stmt, sql_null_data, sql_char)
import SQL.CLI.Utils (connect, freeHandle, disconnect, tables, allocHandle, getData, forAllRecords)
import SQL.CLI.ODBC (setupEnv)

import System.IO (hPutStrLn, stderr)

import Foreign.Marshal.Alloc (alloca, allocaBytes)
import Foreign.Storable (peek, poke)
import Foreign.C.String (peekCString)
import Foreign.Ptr (castPtr)

-- | runs the 'run' action, then, runs the 'afterRun' action no matter if the
-- 'run' action failed or succeeded
finally :: Monad m => m a -> ReaderT r (MaybeT m) b -> ReaderT r (MaybeT m) b
finally afterRun run = do
  env <- ask
  lift $ MaybeT $ runMaybeT (runReaderT run env) >>= (\ result -> afterRun >> return result)


-- | a 'MaybeT' only variant of 'finally'; it runs the second action and, then
-- the first action and returns the result of the second action
finally' :: (MonadIO m, MonadFail m) => IO a -> MaybeT IO b -> m b
finally' afterRun run = do
  result <- liftIO $ runMaybeT run >>= (\result -> afterRun >> return result)
  maybe (fail "action failed in finally'") return result

-- | calls fail on the MonadFail logging an error message
faillog :: (MonadIO m, MonadFail m) => String -> m a
faillog = faillogS $ fromString ""

-- | the variant of 'faillog' taking a log source as parameter
faillogS :: (MonadIO m, MonadFail m) => Text -> String -> m a
faillogS source msg = (liftIO $ loggingLogger LevelError source msg) >> fail msg

-- | setup db environment and connect to database
withConnection :: (MonadIO m)
  => String                             -- ^ datasource name
  -> String                             -- ^ user name
  -> String                             -- ^ password
  -> (SQLHENV -> SQLHDBC -> (ReaderT r (MaybeT m) a))         -- ^ a function that gets the newly allocated environment and connection handlers
  -> ReaderT r (MaybeT m) a
withConnection d u p f = do
  liftIO $ log $ fromString $ "connect to " ++ d
  henv <- setupEnv
  let freeEnvHandle = do
        liftIO $ log $ fromString "free environment handle"
        liftIO $ freeHandle sql_handle_env henv
    in finally freeEnvHandle $ do

    hdbc <- connect henv d u p
    let freeHDBC = do
          liftIO $ log $ fromString $ "disconnect from " ++ d
          liftIO $ disconnect hdbc
      in finally freeHDBC $ f henv hdbc

withConnection' :: (MonadIO m, HasDBInfo r) => (SQLHENV -> SQLHDBC -> (ReaderT r (MaybeT m) a)) -> ReaderT r (MaybeT m) a
withConnection' f = do
  dbi <- asks extractDBInfo
  let d = dbi_Datasource dbi
      u = dbi_User       dbi
      p = dbi_Password   dbi
  withConnection d u p f


-- | connect to database in an existing db environment
withEnvConnection :: (MonadIO m)
  => SQLHENV                            -- ^ handle to environment
  -> String                             -- ^ datasource name
  -> String                             -- ^ user name
  -> String                             -- ^ password
  -> (SQLHENV -> SQLHDBC -> (ReaderT r (MaybeT m) a))         -- ^ a function that gets the newly allocated environment and connection handlers
  -> ReaderT r (MaybeT m) a
withEnvConnection henv d u p f = do
  liftIO $ log $ fromString $ "withEnvConnection' using environment handle " ++ (show henv)
  hdbc <- connect henv d u p
  let freeHDBC = do
        liftIO $ log $ fromString $ "disconnect from " ++ d
        liftIO $ disconnect hdbc
    in finally freeHDBC $ f henv hdbc

-- | call 'withEnvConnect' within a 'ReaderT' environment containing database connnetion info
withEnvConnection' :: (MonadIO m, HasDBInfo r) => SQLHENV -> (SQLHENV -> SQLHDBC -> (ReaderT r (MaybeT m) a)) -> ReaderT r (MaybeT m) a
withEnvConnection' henv f = do
  dbi <- asks extractDBInfo
  let d = dbi_Datasource dbi
      u = dbi_User       dbi
      p = dbi_Password   dbi
  withEnvConnection henv d u p f

-- | call 'connect' within a ReaderT environment containing database connnetion info
connect' :: (MonadIO m, MonadFail m, HasDBInfo r) => SQLHENV -> ReaderT r m SQLHDBC
connect' henv = do
  dbi <- asks extractDBInfo
  let d = dbi_Datasource dbi
      u = dbi_User       dbi
      p = dbi_Password   dbi
  connect henv d u p
  
-- | the environment used to run the program
data ProgramOptions = ProgramOptions {
  po_Source           :: DBInfo,
  po_Dest             :: DBInfo
  }

-- | Information about source or destination db
data DBInfo = DBInfo {
  dbi_Datasource    :: String,
  dbi_User          :: String,
  dbi_Password      :: String,
  dbi_Schema        :: String
  }

class HasDBInfo a where
  extractDBInfo :: a -> DBInfo

instance HasDBInfo DBInfo where
  extractDBInfo = id

-- | an instance that deals only with source db
instance HasDBInfo ProgramOptions where
  extractDBInfo = po_Source


-- | run an action in the current environment on each table name from the current schema,
-- passing an accumulator value; returns the value of the accumulor
forAllTables :: (MonadFail m, MonadIO m, HasDBInfo r) => SQLHDBC -> a -> (a -> String -> ReaderT r (MaybeT m) a) -> ReaderT r (MaybeT m) a
forAllTables hdbc arg f = do
  tables_stmt <- allocHandle sql_handle_stmt hdbc
  liftIO $ log $ fromString $ "forAllTables allocated tables statement: " ++ (show tables_stmt)
  schema <- asks $ dbi_Schema.extractDBInfo

  finally (liftIO $ freeHandle sql_handle_stmt tables_stmt) $ do
    result <- liftIO $ runMaybeT $ tables tables_stmt Nothing (Just schema) Nothing (Just "TABLE")  
    let readTableName = liftIO $ allocaBytes 255
          (\ p_tableName ->
             alloca
             (\ p_tableName_ind -> do
                 poke p_tableName_ind 0
                 _ <- getData tables_stmt 3 sql_char p_tableName 255 p_tableName_ind
                 tableName_ind <- liftIO $ peek p_tableName_ind
                 tableName <- if tableName_ind == sql_null_data
                   then return Nothing
                   else (liftIO . peekCString . castPtr) p_tableName >>= (return.Just)
                 return tableName))
        withTableName arg' = do
          tableName <- readTableName
          maybe (return arg') (f arg') tableName
    result <- forAllRecords tables_stmt withTableName arg
    return result