{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Conferer.FromConfig.Basics where

import           Control.Monad (join)
import           Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import           Data.ByteString (ByteString)
import           Data.Maybe (fromMaybe)
import           Control.Exception
import           Data.Char (toLower)
import           Data.Typeable (Typeable, typeRep, Proxy(..))
import           Data.String (IsString, fromString)
import           Text.Read (readMaybe)
import           GHC.Generics

import           Conferer.Types
import           Conferer.Core (getKey, (/.), getFromConfig)

updateAllAtOnceUsingFetch :: forall a. (FromConfig a, Typeable a) => Key -> Config -> a -> IO a
updateAllAtOnceUsingFetch key config old = do
  fetchFromConfig key config
    >>= \case
      Just new -> do
        evaluate new
      Nothing -> do
        evaluate old

instance FromConfig Int where
  updateFromConfig = updateAllAtOnceUsingFetch
  fetchFromConfig = fetchFromConfigByRead

instance FromConfig Integer where
  updateFromConfig = updateAllAtOnceUsingFetch
  fetchFromConfig = fetchFromConfigByRead

instance FromConfig Float where
  updateFromConfig = updateAllAtOnceUsingFetch
  fetchFromConfig = fetchFromConfigByRead

instance FromConfig ByteString where
  updateFromConfig = updateAllAtOnceUsingFetch
  fetchFromConfig = fetchFromConfigWith (Just . Text.encodeUtf8)

instance DefaultConfig (Maybe a) where
  configDef = Nothing
instance (FromConfig a) => FromConfig (Maybe a) where
  updateFromConfig k config (Just a) = do
    res <- updateFromConfig k config a
    Just <$> evaluate res
  updateFromConfig k config Nothing = do
    fetchFromConfig k config
  fetchFromConfig k config = do
    fetchFromConfig @a k config
      >>= \case
        Just res -> Just <$> Just <$> evaluate res
        Nothing -> return $ Just Nothing


instance FromConfig String where
  updateFromConfig = updateAllAtOnceUsingFetch
  fetchFromConfig = fetchFromConfigWith (Just . Text.unpack)

instance FromConfig Text where
  updateFromConfig = updateAllAtOnceUsingFetch
  fetchFromConfig = fetchFromConfigWith Just


instance FromConfig Bool where
  updateFromConfig = updateAllAtOnceUsingFetch
  fetchFromConfig = fetchFromConfigWith parseBool

parseBool text =
  case Text.toLower text of
    "false" -> Just False
    "true" -> Just True
    _ -> Nothing

updateFromConfigByRead :: (Typeable a, Read a) => Key -> Config -> a -> IO (a)
updateFromConfigByRead = updateFromConfigWith (readMaybe . Text.unpack)

updateFromConfigByIsString :: (Typeable a, IsString a) => Key -> Config -> a -> IO (a)
updateFromConfigByIsString = updateFromConfigWith (Just . fromString . Text.unpack)

fetchFromConfigByRead :: (Typeable a, Read a) => Key -> Config -> IO (Maybe a)
fetchFromConfigByRead = fetchFromConfigWith (readMaybe . Text.unpack)

fetchFromConfigByIsString :: (Typeable a, IsString a) => Key -> Config -> IO (Maybe a)
fetchFromConfigByIsString = fetchFromConfigWith (Just . fromString . Text.unpack)

fromValueWith :: (Text -> Maybe a) -> Text -> Maybe a
fromValueWith parseValue valueAsText = parseValue valueAsText

fetchFromConfigWith :: forall a. Typeable a => (Text -> Maybe a) -> Key -> Config -> IO (Maybe a)
fetchFromConfigWith parseValue key config = do
  getKey key config >>=
    \case
      Just value ->
        return $ Just $
          fromMaybe (throw $ ConfigParsingError key value (typeRep (Proxy :: Proxy a))) $
          fromValueWith parseValue value
      Nothing ->
        return Nothing

updateFromConfigWith :: forall a. Typeable a => (Text -> Maybe a) -> Key -> Config -> a -> IO a
updateFromConfigWith parseValue key config a = do
  getKey key config >>=
    \case
      Just value ->
        return $
          fromMaybe (throw $ ConfigParsingError key value (typeRep (Proxy :: Proxy a))) $
          fromValueWith parseValue value
      Nothing -> return a

-- | Concatenate many transformations to the config based on keys and functions
findKeyAndApplyConfig ::
  forall newvalue config.
  FromConfig newvalue
  => Config -- ^ Complete config
  -> Key -- ^ Key that indicates the part of the config that we care about
  -> Key -- ^ Key that we use to find the config (usually concatenating with the
         -- other key)
  -> (config -> newvalue) -- ^ Function that knows how to use the
                                    -- value to update the config
  -> (newvalue -> config -> config) -- ^ Function that knows how to use the
                                    -- value to update the config
  -> config -- ^ Result of the last config updating
  -> IO config -- ^ Updated config
findKeyAndApplyConfig config k relativeKey get set customConfig = do
  newValue <- updateFromConfig @newvalue (k /. relativeKey) config (get customConfig)
  return $ set newValue customConfig

instance FromConfigG inner =>
    FromConfigG (D1 metadata inner) where
  updateFromConfigG key config (M1 inner) =
    M1 <$> updateFromConfigG key config inner
  fetchFromConfigG key config =
    fmap M1 <$> fetchFromConfigG key config

instance (FromConfigWithConNameG inner, Constructor constructor) =>
    FromConfigG (C1 constructor inner) where
  updateFromConfigG key config (M1 inner) =
    M1 <$> updateFromConfigWithConNameG @inner (conName @constructor undefined) key config inner
  fetchFromConfigG key config =
    fmap M1 <$> fetchFromConfigWithConNameG @inner (conName @constructor undefined) key config

class FromConfigWithConNameG f where
  updateFromConfigWithConNameG :: String -> Key -> Config -> f a -> IO (f a)
  fetchFromConfigWithConNameG :: String -> Key -> Config -> IO (Maybe (f a))

instance (FromConfigWithConNameG left, FromConfigWithConNameG right) =>
    FromConfigWithConNameG (left :*: right) where
  updateFromConfigWithConNameG s key config (left :*: right) = do
    leftValue <- updateFromConfigWithConNameG @left s key config left
    rightValue <- updateFromConfigWithConNameG @right s key config right
    return (leftValue :*: rightValue)

  fetchFromConfigWithConNameG s key config = do
    leftValue <- fetchFromConfigWithConNameG @left s key config
    rightValue <- fetchFromConfigWithConNameG @right s key config
    case (leftValue, rightValue) of
      (Just l, Just r) -> return $ Just (l :*: r)
      _ -> return Nothing

instance (FromConfigG inner, Selector selector) =>
    FromConfigWithConNameG (S1 selector inner) where
  updateFromConfigWithConNameG s key config (M1 inner) =
    let
      applyFirst :: (Char -> Char) -> Text -> Text
      applyFirst f t = case Text.uncons t of
        Just (c, ts) -> Text.cons (f c) ts
        Nothing -> t

      fieldName = Text.pack $ selName @selector undefined
      prefix = applyFirst toLower $ Text.pack  s
      scopedKey =
        case Text.stripPrefix prefix fieldName of
          Just stripped -> applyFirst toLower stripped
          Nothing -> fieldName
    in M1 <$> updateFromConfigG @inner (key /. Path [scopedKey]) config inner

  fetchFromConfigWithConNameG s key config =
    let
      applyFirst :: (Char -> Char) -> Text -> Text
      applyFirst f t = case Text.uncons t of
        Just (c, ts) -> Text.cons (f c) ts
        Nothing -> t

      fieldName = Text.pack $ selName @selector undefined
      prefix = applyFirst toLower $ Text.pack s
      scopedKey =
        case Text.stripPrefix prefix fieldName of
          Just stripped -> applyFirst toLower stripped
          Nothing -> fieldName
    in fmap M1 <$> fetchFromConfigG @inner (key /. Path [scopedKey]) config

-- | Purely 'Generics' machinery, ignore...
instance (FromConfig inner) => FromConfigG (Rec0 inner) where
  updateFromConfigG key config (K1 inner) = do
    K1 <$> updateFromConfig @inner key config inner
  fetchFromConfigG key config = do
    fmap K1 <$> fetchFromConfig @inner key config