{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE RecordWildCards            #-}
{-# LANGUAGE TypeOperators              #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE DefaultSignatures          #-}
{-# LANGUAGE ScopedTypeVariables        #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE LambdaCase                 #-}
module System.Envy
       ( 
         FromEnv (..)
       , ToEnv   (..)
       , Var     (..)
       , EnvList
       , Parser  (..)
        
       , decodeEnv
       , decode
       , showEnv
       , setEnvironment
       , setEnvironment'
       , unsetEnvironment
       , unsetEnvironment'
       , makeEnv
       , env
       , envMaybe
       , (.=)
       , (.!=)
         
       , DefConfig (..)
       , Option (..)
       , runEnv
       , gFromEnvCustom
       ) where
import           Control.Applicative
import           Control.Monad.Except
import           Control.Exception
import           Data.Maybe
import           Data.Char
import           Data.Time
import           GHC.Generics
import           Data.Typeable
import           System.Environment
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 )
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 (lookupEnv 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 (lookupEnv 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 :: Parser a
  default fromEnv :: (DefConfig a, Generic a, GFromEnv (Rep a)) => Parser a
  fromEnv = gFromEnvCustom defOption
gFromEnvCustom :: forall a. (DefConfig a, Generic a, GFromEnv (Rep a))
               => Option
               -> Parser a
gFromEnvCustom opts = to <$> gFromEnv (from (defConfig :: a)) opts
class GFromEnv f where
  gFromEnv :: f a -> Option -> 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 (a :*: b) opts = liftA2 (:*:) (gFromEnv a opts) (gFromEnv b opts)
instance GFromEnv a => GFromEnv (C1 i a) where
  gFromEnv (M1 x) opts = M1 <$> gFromEnv x opts
instance GFromEnv a => GFromEnv (D1 i a) where
  gFromEnv (M1 x) opts = M1 <$> gFromEnv x opts
instance (Selector s, Var a) => GFromEnv (S1 s (K1 i a)) where
  gFromEnv m@(M1 (K1 def)) opts =
      M1 . K1 <$> envMaybe (toEnvName opts $ selName m) .!= def
    where
      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
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
decodeEnv :: FromEnv a => IO (Either String a)
decodeEnv = evalParser fromEnv
decode :: FromEnv a => IO (Maybe a)
decode = fmap eitherToMaybe decodeEnv
  where
    eitherToMaybe (Left _)  = Nothing
    eitherToMaybe (Right x) = Just x
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)
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