{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE DeriveTraversable #-}
#if __GLASGOW_HASKELL__ < 800
{-# LANGUAGE ExplicitNamespaces #-}
#endif
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
-- | Using the 'G.Generic' facility, this module can derive 'Env.Parser's automatically.
--
-- If you have a simple record:
--
-- @
-- {-\# LANGUAGE DeriveGeneric #-}
-- {-\# LANGUAGE MultiParamTypeClasses #-}
--
-- import Env
-- import Env.Generic
--
-- data Hello = Hello
--   { name  :: String
--   , count :: Int
--   , quiet :: Bool
--   } deriving (Show, Eq, Generic)
--
-- instance Record Error Hello
--
-- main :: IO ()
-- main = do
--   hello <- Env.parse (header "envparse example") 'record'
--   print (hello :: Hello)
-- @
--
-- The generic implementation of the 'record' method translates named fields to field parsers:
--
-- @
-- % NAME=bob COUNT=3 runhaskell -isrc example/Generic0.hs
-- Hello {name = "bob", count = 3, quiet = False}
-- @
--
-- If you want to adorn the ugly default help message, augment the fields with descriptions:
--
-- @
-- {-\# LANGUAGE DataKinds #-}
-- {-\# LANGUAGE DeriveGeneric #-}
-- {-\# LANGUAGE MultiParamTypeClasses #-}
-- {-\# LANGUAGE TypeOperators #-}
--
-- import Env
-- import Env.Generic
--
-- data Hello = Hello
--   { name  :: String ? __"Whom shoud I greet?"__
--   , count :: Int    ? __"How many times to greet them?"__
--   , quiet :: Bool   ? __"Should I be quiet instead?"__
--   } deriving (Show, Eq, Generic)
--
-- instance Record Error Hello
--
-- main :: IO ()
-- main = do
--   hello <- Env.parse (header "envparse example") record
--   print (hello :: Hello)
-- @
--
-- @
-- % runhaskell -isrc example/Generic1.hs
-- envparse example
--
-- Available environment variables:
--
--   COUNT                  __How many times to greet them?__
--   NAME                   __Whom shoud I greet?__
--   QUIET                  __Should I be quiet instead?__
--
-- Parsing errors:
--
--   COUNT is unset
--   NAME is unset
-- @
--
-- Note that this has an effect of wrapping the values in the 'Help' constructor:
--
-- @
-- % NAME=bob COUNT=3 QUIET='YES' runhaskell -isrc example/Generic1.hs
-- Hello {name = Help {unHelp = "bob"}, count = Help {unHelp = 3}, quiet = Help {unHelp = True}}
-- @
module Env.Generic
  ( Record(..)
  , Field(..)
#if __GLASGOW_HASKELL__ < 800
  , (?)(..)
#else
  , type (?)(..)
#endif
  , G.Generic
  ) where

import           Control.Applicative (liftA2, (<|>))
import           Control.Monad (guard)
import qualified Data.Char as Char
import           Data.Int (Int8, Int16, Int32, Int64)
import           Data.Word (Word8, Word16, Word32, Word64)
import qualified Data.List as List
import           Data.Maybe (fromMaybe)
import           Data.Proxy (Proxy(Proxy))
import qualified GHC.Generics as G
import qualified GHC.TypeLits as G
import           Numeric.Natural (Natural)
import           Prelude hiding (mod)

import qualified Env


-- | Given a @Record e a@ instance, a value of the type @a@ can be parsed from the environment.
-- If the parsing fails, a value of an error type @e@ is returned.
--
-- The 'record' method has a default implementation for any type that has a 'G.Generic' instance. If you
-- need to choose a concrete type for @e@, the default error type 'Env.Error' is a good candidate. Otherwise,
-- the features you'll use in your parsers will naturally guide GHC to compute the set of required
-- constraints on @e@.
class Record e a where
  record :: Env.Parser e a
  default record :: (r ~ G.Rep a, G.Generic a, GRecord e r) => Env.Parser e a
  record =
    fmap G.to (gr State {statePrefix="", stateCon="", stateVar=""})

data State = State
  { statePrefix :: String -- All the variables' names have this prefix.
  , stateCon    :: String -- The constructor currently being processed.
  , stateVar    :: String -- The variable name to use for the next component.
  } deriving (Show, Eq)

class GRecord e f where
  gr :: State -> Env.Parser e (f a)

instance GRecord e a => GRecord e (G.D1 c a) where
  gr =
    fmap G.M1 . gr

-- Constant values are converted to 'Env.Parser's using their 'Field' instance.
instance Field e a => GRecord e (G.K1 i a) where
  gr State {stateVar} =
    fmap G.K1 (field stateVar Nothing)

-- Constructor's name is used as a prefix to try to remove from
-- selectors when building environment variable names.
instance (G.Constructor c, GRecord e a) => GRecord e (G.C1 c a) where
  gr state =
    fmap G.M1 (gr state {stateCon=con})
   where
    con = G.conName (G.M1 Proxy :: G.M1 t c Proxy b)

instance (GRecord e f, GRecord e g) => GRecord e (f G.:*: g) where
  gr x =
    liftA2 (G.:*:) (gr x) (gr x)

instance (GRecord e f, GRecord e g) => GRecord e (f G.:+: g) where
  gr x =
    fmap G.L1 (gr x) <|> fmap G.R1 (gr x)

#if __GLASGOW_HASKELL__ < 800
type family Type x :: ConType where
  Type G.NoSelector = 'Plain
  Type x = 'Record

data ConType = Plain | Record

instance (G.Selector c, Type c ~ 'Record, GRecord e a) => GRecord e (G.S1 c a) where
#else
instance (G.Selector c, c ~ 'G.MetaSel ('Just x1) x2 x3 x4, GRecord e a) => GRecord e (G.S1 c a) where
#endif
  gr state@State {statePrefix, stateCon} =
    fmap G.M1 (gr state {stateVar=statePrefix ++ suffix})
   where
    sel = G.selName (G.M1 Proxy :: G.M1 t c Proxy b)
    suffix = let
        x = camelTo2 sel
      in fromMaybe x $ do
        y <- List.stripPrefix (map Char.toLower stateCon) sel
        camelTo2 y <$ guard (not (List.null y))

camelTo2 :: String -> String
camelTo2 = map Char.toUpper . go2 . go1
 where
  go1 "" = ""
  go1 (x:u:l:xs) | Char.isUpper u && Char.isLower l = x : '_' : u : l : go1 xs
  go1 (x:xs) = x : go1 xs

  go2 "" = ""
  go2 (l:u:xs) | Char.isLower l && Char.isUpper u = l : '_' : u : go2 xs
  go2 (x:xs) = x : go2 xs

-- | Given a @Field e a@ instance, a value of the type @a@ can be parsed from an environment variable.
-- If the parsing fails, a value of an error type @e@ is returned.
--
-- The 'field' method has a default implementation for any type that has a 'Read' instance. If you
-- need to choose a concrete type for @e@, the default error type 'Env.Error' is a good candidate. Otherwise,
-- the features you'll use in your parsers will naturally guide GHC to compute the set of required
-- constraints on @e@.
--
-- The annotated instances do not use the default implementation.
class Field e a where
  field :: String -> Maybe String -> Env.Parser e a
  default field :: (Env.AsUnset e, Env.AsUnread e, Read a) => String -> Maybe String -> Env.Parser e a
  field name help =
    Env.var Env.auto name (foldMap Env.help help)

instance (Env.AsUnset e, Env.AsUnread e) => Field e Int

instance (Env.AsUnset e, Env.AsUnread e) => Field e Int8

instance (Env.AsUnset e, Env.AsUnread e) => Field e Int16

instance (Env.AsUnset e, Env.AsUnread e) => Field e Int32

instance (Env.AsUnset e, Env.AsUnread e) => Field e Int64

instance (Env.AsUnset e, Env.AsUnread e) => Field e Integer

instance (Env.AsUnset e, Env.AsUnread e) => Field e Word

instance (Env.AsUnset e, Env.AsUnread e) => Field e Word8

instance (Env.AsUnset e, Env.AsUnread e) => Field e Word16

instance (Env.AsUnset e, Env.AsUnread e) => Field e Word32

instance (Env.AsUnset e, Env.AsUnread e) => Field e Word64

instance (Env.AsUnset e, Env.AsUnread e) => Field e Natural

instance (Env.AsUnset e, Env.AsUnread e) => Field e Float

instance (Env.AsUnset e, Env.AsUnread e) => Field e Double

-- | Uses the 'String' value verbatim.
instance Env.AsUnset e => Field e String where
  field name help =
    Env.var Env.str name (foldMap Env.help help)

-- | Expects a single-character 'String' value.
instance (Env.AsUnset e, Env.AsUnread e) => Field e Char where
  field name help =
    Env.var reader name (foldMap Env.help help)
   where
    reader = \case
      [c] -> pure c
      str -> Left (Env.unread str)

-- | Any set and non-empty value parses to a 'True'; otherwise, it's a 'False'. This parser
-- never fails.
instance Field e Bool where
  field name help =
    Env.switch name (foldMap Env.help help)

-- | A field annotation.
--
-- If you annotate a record field with a 'Symbol' literal (that is, a statically known type level string)
-- the derivation machinery will use the literal in the help message.
--
-- Please remember that the values of the annotated fields are wrapped in the 'Help' constructor.
newtype a ? tag = Help { unHelp :: a }
    deriving (Show, Eq, Functor, Foldable, Traversable)

-- | Augments the underlying field parser with the help message.
instance (G.KnownSymbol tag, Field e a) => Field e (a ? tag) where
  field name _ =
    fmap Help (field name (pure (G.symbolVal (Proxy :: Proxy tag))))