{-# Language DeriveDataTypeable #-}
{-| Extensions to 'Data.ConfigFile' and utility functions for dealing with
    configuration in general and reading/writing files. -}
module B9.ConfigUtils ( allOn
                      , lastOn
                      , SystemPath (..)
                      , resolve
                      , ensureDir
                      , readIniFile
                      , getOptionM
                      , getOption
                      , getOptionOr
                      , IniFileException(..)
                      , module Data.ConfigFile
                      , UUID (..)
                      , randomUUID
                      , tell
                      , consult
                      , getDirectoryFiles
                      , maybeConsult
                      , maybeConsultSystemPath
                      ) where

import Data.Monoid
import Data.Function ( on )
import Data.Typeable
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative
#endif
import Control.Exception
import Control.Monad.IO.Class
import System.Directory
import Text.Read ( readEither )
import System.Random ( randomIO )
import Data.Word ( Word16, Word32 )
import System.FilePath
import Text.Printf
import Data.ConfigFile
import Data.Data
import Text.Show.Pretty (ppShow)

allOn :: (a -> Maybe Bool) -> a -> a -> Maybe Bool
allOn getter x y = getAll <$> on mappend (fmap All . getter) x y

lastOn :: (a -> Maybe b) -> a -> a -> Maybe b
lastOn getter x y = getLast $ on mappend (Last . getter) x y

data SystemPath = Path FilePath
                | InHomeDir FilePath
                | InB9UserDir FilePath
                | InTempDir FilePath
  deriving (Eq, Read, Show, Typeable, Data)

resolve :: MonadIO m => SystemPath -> m FilePath
resolve (Path p) = return p
resolve (InHomeDir p) = liftIO $ do
  d <- getHomeDirectory
  return $ d </> p
resolve (InB9UserDir p) = liftIO $ do
  d <- getAppUserDataDirectory "b9"
  return $ d </> p
resolve (InTempDir p) = liftIO $ do
  d <- getTemporaryDirectory
  return $ d </> p

-- | Get all files from 'dir' that is get ONLY files not directories
getDirectoryFiles :: MonadIO m => FilePath -> m [FilePath]
getDirectoryFiles dir = do
  entries <- liftIO (getDirectoryContents dir)
  fileEntries <- mapM (liftIO . doesFileExist . (dir </>)) entries
  return (snd <$> filter fst (fileEntries `zip` entries))

ensureDir :: MonadIO m => FilePath -> m ()
ensureDir p = liftIO (createDirectoryIfMissing True $ takeDirectory p)

data ReaderException = ReaderException FilePath String
  deriving (Show, Typeable)
instance Exception ReaderException

tell :: (MonadIO m, Show a) => FilePath -> a -> m ()
tell f x = do
  ensureDir f
  liftIO (writeFile f (ppShow x))

consult :: (MonadIO m, Read a) => FilePath -> m a
consult f = liftIO $ do
  c <- readFile f
  case readEither c of
    Left e ->
      throwIO $ ReaderException f e
    Right a ->
      return a

maybeConsult :: (MonadIO m, Read a) => Maybe FilePath -> a -> m a
maybeConsult Nothing defaultArg = return defaultArg
maybeConsult (Just f) defaultArg = liftIO $ do
  exists <- doesFileExist f
  if exists
    then consult f
    else return defaultArg

maybeConsultSystemPath :: (MonadIO m, Read a) => Maybe SystemPath -> a -> m a
maybeConsultSystemPath Nothing defaultArg = return defaultArg
maybeConsultSystemPath (Just f) defaultArg = liftIO $ do
  f' <- resolve f
  exists <- doesFileExist f'
  if exists
    then consult f'
    else return defaultArg

data IniFileException = IniFileException FilePath CPError
                      deriving (Show, Typeable)
instance Exception IniFileException

readIniFile :: MonadIO m => SystemPath -> m ConfigParser
readIniFile cfgFile' = do
  cfgFile <- resolve cfgFile'
  cp' <- liftIO $ readfile emptyCP cfgFile
  case cp' of
    Left e   -> liftIO $ throwIO (IniFileException cfgFile e)
    Right cp -> return cp

getOption :: (Get_C a, Monoid a) => ConfigParser -> SectionSpec -> OptionSpec -> a
getOption cp sec key = either (const mempty) id $ get cp sec key

getOptionM :: (Get_C a, Read a) => ConfigParser -> SectionSpec -> OptionSpec -> Maybe a
getOptionM cp sec key = either (const Nothing) id $ get cp sec key

getOptionOr :: (Get_C a, Read a) => ConfigParser -> SectionSpec -> OptionSpec -> a -> a
getOptionOr cp sec key dv = either (const dv) id $ get cp sec key

newtype UUID = UUID (Word32, Word16, Word16, Word16, Word32, Word16)
             deriving (Read, Show, Eq, Ord)

instance PrintfArg UUID where
  formatArg (UUID (a, b, c, d, e, f)) fmt
    | fmtChar (vFmt 'U' fmt) == 'U' =
        let str = (printf "%08x-%04x-%04x-%04x-%08x%04x" a b c d e f :: String)
        in formatString str (fmt { fmtChar = 's', fmtPrecision = Nothing })
    | otherwise = errorBadFormat $ fmtChar fmt


randomUUID :: MonadIO m => m UUID
randomUUID = liftIO
               (UUID <$> ((,,,,,) <$> randomIO
                                  <*> randomIO
                                  <*> randomIO
                                  <*> randomIO
                                  <*> randomIO
                                  <*> randomIO))