{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE RecordWildCards            #-}
{-# LANGUAGE StandaloneDeriving         #-}
{-# LANGUAGE TypeOperators              #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE DefaultSignatures          #-}
{-# LANGUAGE ScopedTypeVariables        #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE LambdaCase                 #-}
{-# LANGUAGE DataKinds                  #-}
{-# LANGUAGE KindSignatures             #-}
module System.Envy
       ( 
         FromEnv (..)
       , ToEnv   (..)
       , Var     (..)
       , EnvList (..)
       , EnvVar  (..)
       , Parser  (..)
        
       , decodeEnv
       , decodeWithDefaults
       , decode
       , showEnv
       , setEnvironment
       , setEnvironment'
       , unsetEnvironment
       , unsetEnvironment'
       , makeEnv
       , env
       , envMaybe
       , (.=)
       , (.!=)
         
       , ReadShowVar (..)
         
       , DefConfig (..)
       , Option (..)
       , defOption
       , runEnv
       , gFromEnvCustom
       ) where
import           Control.Applicative
import           Control.Monad.Except
import           Control.Monad.Fail
import           Control.Exception
import           Data.Functor.Identity
import           Data.Maybe
import           Data.Monoid
import           Data.Char
import           Data.Time
import           GHC.Generics
import           Data.Typeable
import           System.Environment.Blank
import           Text.Read (readMaybe)
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import           Data.Text (Text)
import           Data.Word
import           Data.Int
import qualified Data.ByteString.Char8 as B8
import qualified Data.ByteString.Lazy.Char8 as BL8
newtype Parser a = Parser { runParser :: ExceptT String IO a }
  deriving ( Functor, Monad, Applicative, MonadError String
           , MonadIO, Alternative, MonadPlus )
instance MonadFail Parser where
  fail = Parser . throwError
data EnvVar = EnvVar {
  variableName :: String,
  
  variableValue :: String
  
  }
  deriving (Show, Eq)
evalParser :: Parser a -> IO (Either String a)
evalParser = runExceptT . runParser
runEnv :: Parser a -> IO (Either String a)
runEnv = runExceptT . runParser
env :: Var a
    => String   
    -> Parser a 
env key = do
  result <- liftIO (getEnv key)
  case result of
    Nothing -> throwError $ "Variable not found for: " ++ key
    Just dv ->
      case fromVar dv of
        Nothing -> throwError $ ("Parse failure: could not parse variable "
                                 ++ show key ++ " into type "
                                 ++ show (typeOf dv))
        Just x -> return x
envMaybe :: Var a
         => String           
         -> Parser (Maybe a) 
envMaybe key = do
  val <- liftIO (getEnv key)
  return $ case val of
   Nothing -> Nothing
   Just x -> fromVar x
(.!=) :: Parser (Maybe a) 
      -> a                
      -> Parser a         
(.!=) parser def  = fromMaybe def <$> parser
(.=) :: Var a
     => String 
     -> a      
     -> EnvVar 
(.=) variableName value = EnvVar variableName (toVar value)
class FromEnv a where
  fromEnv :: Maybe a -> Parser a
  default fromEnv :: (Generic a, GFromEnv (Rep a)) => Maybe a -> Parser a
  fromEnv oa = gFromEnvCustom defOption oa
gFromEnvCustom :: forall a. (Generic a, GFromEnv (Rep a))
               => Option
               -> Maybe a
               -> Parser a
gFromEnvCustom opts oa = to <$> gFromEnv opts (from <$> oa)
class GFromEnv f where
  gFromEnv :: Option -> Maybe (f a) ->  Parser (f a)
class DefConfig a where defConfig :: a
data Option = Option {
    dropPrefixCount :: Int  
  , customPrefix :: String  
  } deriving Show
defOption :: Option
defOption = Option 0 mempty
instance (GFromEnv a, GFromEnv b) => GFromEnv (a :*: b) where
  gFromEnv opts ox = let (oa, ob) = case ox of
                                             (Just (a :*: b)) -> (Just a, Just b)
                                             _ -> (Nothing, Nothing) in
                            liftA2 (:*:) (gFromEnv opts oa) (gFromEnv opts ob)
instance GFromEnv a => GFromEnv (C1 i a) where
  gFromEnv  opts (Just (M1 x))= M1 <$> gFromEnv opts (Just x)
  gFromEnv  opts            _ = M1 <$> gFromEnv opts Nothing
instance GFromEnv a => GFromEnv (D1 i a) where
  gFromEnv opts (Just (M1 x)) = M1 <$> gFromEnv opts (Just x)
  gFromEnv opts             _ = M1 <$> gFromEnv opts Nothing
instance (Selector s, Var a) => GFromEnv (S1 s (K1 i a)) where
  gFromEnv opts ox =
    let p = case ox of
               Just (M1 (K1 def)) -> envMaybe envName .!= def
               _                  -> env envName in
    M1 . K1 <$> p
     where
      envName = toEnvName opts $ selName (SelectorProxy :: SelectorProxy s Proxy ())
      toEnvName :: Option -> String -> String
      toEnvName Option{..} xs =
        let name = snake (drop dropPrefixCount xs)
        in if customPrefix == mempty
             then name
             else map toUpper customPrefix ++ "_" ++ name
      applyFirst :: (Char -> Char) -> String -> String
      applyFirst _ []     = []
      applyFirst f [x]    = [f x]
      applyFirst f (x:xs) = f x: xs
      snakeCase :: String -> String
      snakeCase = u . applyFirst toLower
        where u []                 = []
              u (x:xs) | isUpper x = '_' : toLower x : snakeCase xs
                       | otherwise = x : u xs
      snake :: String -> String
      snake = map toUpper . snakeCase
data SelectorProxy (s :: Meta) (f :: * -> *) a = SelectorProxy
class ToEnv a where
  
  toEnv :: a -> EnvList a
data EnvList a = EnvList [EnvVar] deriving (Show)
makeEnv :: [EnvVar] -> EnvList a
makeEnv = EnvList
class Typeable a => Var a where
  
  toVar   :: a -> String
  
  fromVar :: String -> Maybe a
instance Var Text where toVar = T.unpack; fromVar = Just . T.pack
instance Var TL.Text where toVar = TL.unpack; fromVar = Just . TL.pack
instance Var BL8.ByteString where toVar = BL8.unpack; fromVar = Just . BL8.pack
instance Var B8.ByteString where toVar = B8.unpack; fromVar = Just . B8.pack
instance Var Int where toVar = show; fromVar = readMaybe
instance Var Int8 where toVar = show; fromVar = readMaybe
instance Var Int16 where toVar = show; fromVar = readMaybe
instance Var Int32 where toVar = show; fromVar = readMaybe
instance Var Int64 where toVar = show; fromVar = readMaybe
instance Var Integer where toVar = show; fromVar = readMaybe
instance Var UTCTime where toVar = show; fromVar = readMaybe
instance Var Day where toVar = show; fromVar = readMaybe
instance Var Word8 where toVar = show; fromVar = readMaybe
instance Var Bool where toVar = show; fromVar = readMaybe
instance Var Double where toVar = show; fromVar = readMaybe
instance Var Word16 where toVar = show; fromVar = readMaybe
instance Var Word32 where toVar = show; fromVar = readMaybe
instance Var Word64 where toVar = show; fromVar = readMaybe
instance Var String where toVar = id; fromVar = Just
instance Var () where toVar = const "()"; fromVar = const $ Just ()
instance Var a => Var (Maybe a) where
  toVar = maybe "" toVar
  fromVar "" = Nothing
  fromVar  s = Just <$> fromVar s
deriving instance (Var a, Typeable a) => Var (Last a)
deriving instance (Var a, Typeable a) => Var (First a)
deriving instance (Var a, Typeable a) => Var (Identity a)
newtype ReadShowVar a = ReadShowVar { unReadShowVar :: a }
instance (Typeable a, Show a, Read a) => Var (ReadShowVar a) where
  toVar = show . unReadShowVar
  fromVar = fmap ReadShowVar . readMaybe
decodeEnv :: FromEnv a => IO (Either String a)
decodeEnv = evalParser (fromEnv Nothing)
decode :: FromEnv a => IO (Maybe a)
decode = fmap eitherToMaybe decodeEnv
  where
    eitherToMaybe (Left _)  = Nothing
    eitherToMaybe (Right x) = Just x
decodeWithDefaults :: FromEnv a => a -> IO a
decodeWithDefaults def = (\(Right x) -> x) <$> evalParser (fromEnv (Just def))
wrapIOException :: IO a -> IO (Either String a)
wrapIOException action = try action >>= \case
  Left (ex :: IOException) -> return $ Left $ show ex
  Right x -> return $ Right x
setEnvironment :: EnvList a -> IO (Either String ())
setEnvironment (EnvList envVars) = wrapIOException $ mapM_ set envVars
  where set var = setEnv (variableName var) (variableValue var) True
setEnvironment' :: ToEnv a => a -> IO (Either String ())
setEnvironment' = setEnvironment . toEnv
unsetEnvironment :: EnvList a -> IO (Either String ())
unsetEnvironment (EnvList envVars) = wrapIOException $ mapM_ unset envVars
  where unset var = unsetEnv (variableName var)
unsetEnvironment' :: ToEnv a => a -> IO (Either String ())
unsetEnvironment' = unsetEnvironment . toEnv
showEnv :: IO ()
showEnv = mapM_ print =<< getEnvironment