module Yam.Config(
    Config(..)
  , HasValue(..)
  , defaultConfig
  ) where

import           Control.Exception       (catch, throw)
import           Data.Aeson
import           Data.Foldable           (foldl')
import qualified Data.HashMap.Strict     as H
import           Data.Monoid             ((<>))
import           Data.String.Conversions (cs)
import           Data.Text               (Text, splitOn)
import qualified Data.Text               as T
import           Data.Yaml
import           System.Environment

type Required = Bool

class Config c where
  fetch   :: Text -> c -> Either String c
  merge   :: [c] -> c
  from    ::  (String, String)  -> c
  merge'  :: [IO c] -> IO c
  merge' c' = merge <$> sequence c'
  fromFile :: FilePath -> Required -> IO c
  fromEnv :: [(String, String)] -> c
  fromEnv = merge . fmap from
  fromCommandLine :: [String]   -> c
  fromCommandLine = merge . fmap (from . select . break (=='='))
    where
      select (k, '=':vs) = (k,vs)
      select v           = v
  {-# MINIMAL fetch,merge,from,fromFile #-}

keys :: Text -> Text -> [Text]
keys sep = dropWhile T.null . splitOn sep

defaultConfig :: Config c => IO c
defaultConfig = do
  args <- getArgs
  envs <- getEnvironment
  return $ merge [fromCommandLine args, fromEnv envs]

instance Config Value where
  fetch key = go key (keys "." key)
    where
      go _  []             c  = Right c
      go k' (k:ks) (Object c) = case H.lookup k c of
        Nothing -> Left $ "Key " <> T.unpack k' <> " Not Found"
        Just v  -> go k' ks v
      go k' _              _  = Left $ "Key " <> T.unpack k' <> " Not Match"
  merge = foldl' merge2 Null
    where
      merge2 Null    a             = a
      merge2 (Object a) (Object b) = Object (H.unionWith merge2 a b)
      merge2 a       _             = a
  fromFile f required = (decodeFileEither f >>= either throw return) `catch` go required
    where
      go :: Required -> ParseException -> IO Value
      go False (InvalidYaml (Just (YamlException _))) = return Null
      go _     e                                      = throw e
  from (k,v) = case Data.Yaml.decode $ cs v of
    Nothing -> Null
    Just a  -> go' (keys "_" $ T.toLower $ T.pack k) a
    where
      go' []      v' = v'
      go' (k':ks) v' = Object $ H.insert k' (go' ks v') H.empty

class (Monad m, Config c) => HasValue m c v where
  parse :: c -> m (Either String v)
  getValue :: Text -> c -> m (Either String v)
  getValue k c = case fetch k c of
    Left  e -> return $ Left e
    Right v -> parse v
  getValueOrDef :: v -> Text -> c -> m v
  getValueOrDef v k c = do
    v' <- getValue k c
    case v' of
      Left  _ -> return v
      Right w -> return w
  requireValue :: Text -> c -> m v
  requireValue k c = do
    v' <- getValue k c
    case v' of
      Left  e -> fail e
      Right v -> return v
  {-# MINIMAL parse #-}

instance (Monad m, FromJSON v) => HasValue m Value v where
  parse c = return $ case fromJSON c of
    Success a -> Right a
    Error   e -> Left  e