{-# LANGUAGE ExistentialQuantification #-}
module Moo.Core where

import Control.Applicative ((<$>), (<*>))
import Control.Monad.Reader (ReaderT)
import qualified Data.Configurator as C
import Data.Configurator.Types (Config)
import qualified Data.Text as T
import Database.HDBC.PostgreSQL (connectPostgreSQL)
import Database.HDBC.Sqlite3 (connectSqlite3)
import System.Environment (getEnvironment)

import Database.Schema.Migrations ()
import Database.Schema.Migrations.Store (MigrationStore, StoreData)
import Database.Schema.Migrations.Backend
import Database.Schema.Migrations.Backend.HDBC

-- |The monad in which the application runs.
type AppT a = ReaderT AppState IO a

-- |The type of actions that are invoked to handle specific commands
type CommandHandler = StoreData -> AppT ()

-- |Application state which can be accessed by any command handler.
data AppState = AppState { _appOptions         :: CommandOptions
                         , _appCommand         :: Command
                         , _appRequiredArgs    :: [String]
                         , _appOptionalArgs    :: [String]
                         , _appStore           :: MigrationStore
                         , _appDatabaseConnStr :: DbConnDescriptor
                         , _appDatabaseType    :: String
                         , _appStoreData       :: StoreData
                         }

type ShellEnvironment = [(String, String)]

data Configuration = Configuration
    { _connectionString   :: String
    , _databaseType       :: String
    , _migrationStorePath :: FilePath
    }

loadConfiguration :: Maybe FilePath -> IO (Either String Configuration)
loadConfiguration pth = do
    mCfg <- case pth of
        Nothing -> fromShellEnvironment <$> getEnvironment
        Just path -> fromConfigurator =<< C.load [C.Required path]

    case mCfg of
        Nothing -> do
            case pth of
                Nothing -> return $ Left "Missing required environment variables"
                Just path -> return $ Left $ "Could not load configuration from " ++ path
        Just cfg -> return $ Right cfg

fromShellEnvironment :: ShellEnvironment -> Maybe Configuration
fromShellEnvironment env = Configuration <$> connectionString
                                         <*> databaseType
                                         <*> migrationStorePath
    where
      connectionString = envLookup envDatabaseName
      databaseType = envLookup envDatabaseType
      migrationStorePath = envLookup envStoreName
      envLookup = (\evar -> lookup evar env)

fromConfigurator :: Config -> IO (Maybe Configuration)
fromConfigurator conf = do
    let configLookup = C.lookup conf . T.pack
    connectionString <- configLookup envDatabaseName
    databaseType <- configLookup envDatabaseType
    migrationStorePath <- configLookup envStoreName

    return $ Configuration <$> connectionString
                           <*> databaseType
                           <*> migrationStorePath

-- |CommandOptions are those options that can be specified at the command
-- prompt to modify the behavior of a command.
data CommandOptions = CommandOptions { _configFilePath :: Maybe String
                                     , _test           :: Bool
                                     , _noAsk          :: Bool
                                     }

-- |A command has a name, a number of required arguments' labels, a
-- number of optional arguments' labels, and an action to invoke.
data Command = Command { _cName           :: String
                       , _cRequired       :: [String]
                       , _cOptional       :: [String]
                       , _cAllowedOptions :: [String]
                       , _cDescription    :: String
                       , _cHandler        :: CommandHandler
                       }

-- |ConfigOptions are those options read from configuration file
data ConfigData = ConfigData { _dbTypeStr     :: String
                             , _dbConnStr     :: String
                             , _fileStorePath :: String
                             }

newtype DbConnDescriptor = DbConnDescriptor String

-- |The values of DBM_DATABASE_TYPE and their corresponding connection
-- factory functions.
databaseTypes :: [(String, String -> IO Backend)]
databaseTypes = [ ("postgresql", fmap hdbcBackend . connectPostgreSQL)
                , ("sqlite3", fmap hdbcBackend . connectSqlite3)
                ]

envDatabaseType :: String
envDatabaseType = "DBM_DATABASE_TYPE"

envDatabaseName :: String
envDatabaseName = "DBM_DATABASE"

envStoreName :: String
envStoreName = "DBM_MIGRATION_STORE"