{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# OPTIONS_GHC -fno-warn-warnings-deprecations #-} -- Because of camelTo
{-# LANGUAGE DeriveGeneric     #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards   #-}

module Vgrep.Environment.Config.Sources.File
    ( configFromFile
    , Attr
    , Color
    , Style
    ) where

import           Control.Monad           ((<=<))
import           Control.Monad.IO.Class
import           Data.Aeson.Types
    ( FromJSON (..)
    , Options (..)
    , camelTo
    , defaultOptions
    , genericParseJSON
    , withObject
    , (.!=)
    , (.:?)
    )
import           Data.Map.Strict         (Map)
import qualified Data.Map.Strict         as M
import           Data.Maybe
import           Data.Monoid
import           Data.Yaml.Aeson
    ( decodeFileEither
    , prettyPrintParseException
    )
import           GHC.Generics
import qualified Graphics.Vty.Attributes as Vty
import           System.Directory
import           System.IO
import           Text.Read               (readMaybe)

import           Vgrep.Command
import           Vgrep.Environment.Config.Monoid
import qualified Vgrep.Key                       as Key
import           Vgrep.KeybindingMap

-- $setup
-- >>> import Data.List (isInfixOf)
-- >>> import Data.Yaml.Aeson (decodeEither, ParseException)


{- |
Reads the configuration from a JSON or YAML file. The file should be
located in one of the following places:

* @~\/.vgrep\/config.yaml@,
* @~\/.vgrep\/config.yml@,
* @~\/.vgrep\/config.json@ or
* @~\/.vgrep\/config@.

When none of these files exist, no error is raised. When a file exists, but
cannot be parsed, a warning is written to stderr.

Supported formats are JSON and YAML. The example YAML config given in the
project directory (@config.yaml.example@) is equivalent to the default
config:

>>> import qualified Vgrep.Environment.Config as C
>>> Right config <- decodeFileEither "config.yaml.example" :: IO (Either ParseException ConfigMonoid)
>>> C.fromConfigMonoid config == C.defaultConfig
True

Example YAML config file for 'Vgrep.Environment.Config.defaultConfig':

> colors:
>   line-numbers:
>     fore-color: blue
>   line-numbers-hl:
>     fore-color: blue
>     style: bold
>   normal: {}
>   normal-hl:
>     style: bold
>   file-headers:
>     back-color: green
>   selected:
>     style: standout
> tabstop: 8
> editor: "vi"

Example JSON file for the same config:

> {
>   "colors": {
>     "line-numbers" : {
>       "fore-color": "blue"
>     },
>     "line-numbers-hl": {
>       "fore-color": "blue",
>       "style": "bold"
>     },
>     "normal": {},
>     "normal-hl": {
>       "style": "bold"
>     },
>     "file-headers": {
>       "back-color": "green"
>     },
>     "selected": {
>       "style": "standout"
>     }
>   },
>   "tabstop": 8,
>   "editor": "vi"
> }

The JSON/YAML keys correspond to the lenses in "Vgrep.Environment.Config",
the values for 'Vty.Color' and 'Vty.Style' can be obtained from the
corresponding predefined constants in "Graphics.Vty.Attributes".
-}
configFromFile :: MonadIO io => io ConfigMonoid
configFromFile = liftIO $ do
    configDir <- getAppUserDataDirectory "vgrep"
    let configFiles = map (configDir </>)
            [ "config.yaml"
            , "config.yml"
            , "config.json"
            , "config" ]
    findExistingFile configFiles >>= \case
        Nothing         -> pure mempty
        Just configFile -> decodeFileEither configFile >>= \case
            Right config -> pure config
            Left err     -> do
                hPutStrLn stderr $
                    "Could not parse config file " ++ configFile ++ ":"
                    ++ "\n" ++ prettyPrintParseException err
                    ++ "\nFalling back to default config."
                pure mempty
  where
    findExistingFile :: [FilePath] -> IO (Maybe FilePath)
    findExistingFile = \case
        [] -> pure Nothing
        f : fs -> do
            exists <- doesFileExist f
            if exists then pure (Just f) else findExistingFile fs

    (</>) :: FilePath -> FilePath -> FilePath
    dir </> file = dir <> "/" <> file


instance FromJSON ConfigMonoid where
    parseJSON = withObject "ConfigMonoid" $ \o -> do
        _mcolors  <- o .:? "colors" .!= mempty
        _mtabstop <- fmap First (o .:? "tabstop")
        _meditor  <- fmap First (o .:? "editor")
        _mkeybindings <- o .:? "keybindings" .!= mempty
        pure ConfigMonoid{..}

instance FromJSON ColorsMonoid where
    parseJSON = genericParseJSON jsonOptions

instance FromJSON Vty.Attr where
    parseJSON = fmap attrToVty . parseJSON


{- |
A JSON-parsable data type for 'Vty.Attr'.

JSON example:

>>> decodeEither "{\"fore-color\": \"black\", \"style\": \"standout\"}" :: Either String Attr
Right (Attr {foreColor = Just Black, backColor = Nothing, style = Just Standout})

JSON example without quotes:
>>> decodeEither "{fore-color: black, style: standout}" :: Either String Attr
Right (Attr {foreColor = Just Black, backColor = Nothing, style = Just Standout})

YAML example:

>>> :{
>>> decodeEither
>>>   $  "fore-color: \"blue\"\n"
>>>   <> "back-color: \"bright-blue\"\n"
>>>   <> "style: \"reverse-video\"\n"
>>>   :: Either String Attr
>>> :}
Right (Attr {foreColor = Just Blue, backColor = Just BrightBlue, style = Just ReverseVideo})

YAML example without quotes:

>>> :{
>>> decodeEither
>>>   $  "fore-color: blue\n"
>>>   <> "back-color: bright-blue\n"
>>>   <> "style: reverse-video\n"
>>>   :: Either String Attr
>>> :}
Right (Attr {foreColor = Just Blue, backColor = Just BrightBlue, style = Just ReverseVideo})

An empty JSON/YAML object yields the default colors:

>>> decodeEither "{}" :: Either String Attr
Right (Attr {foreColor = Nothing, backColor = Nothing, style = Nothing})
-}
data Attr = Attr
    { foreColor :: Maybe Color
    , backColor :: Maybe Color
    , style     :: Maybe Style
    }
    deriving (Eq, Show, Generic)

instance FromJSON Attr where
    parseJSON = genericParseJSON jsonOptions

attrToVty :: Attr -> Vty.Attr
attrToVty Attr{..} = foldAttrs
    [ fmap (flip Vty.withForeColor . colorToVty) foreColor
    , fmap (flip Vty.withBackColor . colorToVty) backColor
    , fmap (flip Vty.withStyle     . styleToVty) style ]
  where
    foldAttrs = foldr ($) Vty.defAttr . catMaybes


{- |
A JSON-parsable data type for 'Vty.Color'.

>>> decodeEither "[\"black\",\"red\",\"bright-black\"]" :: Either String [Color]
Right [Black,Red,BrightBlack]

Also works without quotes:

>>> decodeEither "[black,red,bright-black]" :: Either String [Color]
Right [Black,Red,BrightBlack]

Fails with error message if the 'Color' cannot be parsed:

>>> let Left err = decodeEither "foo" :: Either String Color
>>> "The key \"foo\" was not found" `isInfixOf` err
True
-}
data Color
    = Black
    | Red
    | Green
    | Yellow
    | Blue
    | Magenta
    | Cyan
    | White
    | BrightBlack
    | BrightRed
    | BrightGreen
    | BrightYellow
    | BrightBlue
    | BrightMagenta
    | BrightCyan
    | BrightWhite
    deriving (Eq, Show, Generic)

instance FromJSON Color where
    parseJSON = genericParseJSON jsonOptions

colorToVty :: Color -> Vty.Color
colorToVty = \case
    Black         -> Vty.black
    Red           -> Vty.red
    Green         -> Vty.green
    Yellow        -> Vty.yellow
    Blue          -> Vty.blue
    Magenta       -> Vty.magenta
    Cyan          -> Vty.cyan
    White         -> Vty.white
    BrightBlack   -> Vty.brightBlack
    BrightRed     -> Vty.brightRed
    BrightGreen   -> Vty.brightGreen
    BrightYellow  -> Vty.brightYellow
    BrightBlue    -> Vty.brightBlue
    BrightMagenta -> Vty.brightMagenta
    BrightCyan    -> Vty.brightCyan
    BrightWhite   -> Vty.brightWhite


{- |
A JSON-parsable data type for 'Vty.Style'.

>>> decodeEither "[\"standout\", \"underline\", \"bold\"]" :: Either String [Style]
Right [Standout,Underline,Bold]

Also works without quotes:

>>> decodeEither "[standout, underline, bold]" :: Either String [Style]
Right [Standout,Underline,Bold]

Fails with error message if the 'Style' cannot be parsed:

>>> let Left err = decodeEither "foo" :: Either String Style
>>> "The key \"foo\" was not found" `isInfixOf` err
True
-}
data Style
    = Standout
    | Underline
    | ReverseVideo
    | Blink
    | Dim
    | Bold
    deriving (Eq, Show, Generic)

instance FromJSON Style where
    parseJSON = genericParseJSON jsonOptions

styleToVty :: Style -> Vty.Style
styleToVty = \case
    Standout     -> Vty.standout
    Underline    -> Vty.underline
    ReverseVideo -> Vty.reverseVideo
    Blink        -> Vty.blink
    Dim          -> Vty.dim
    Bold         -> Vty.bold


instance FromJSON KeybindingsMonoid where
    parseJSON = genericParseJSON jsonOptions

instance FromJSON Command where
    parseJSON = genericParseJSON jsonOptions

instance FromJSON KeybindingMap where
    parseJSON = fmap KeybindingMap . mapMKeys parseChord <=< parseJSON

mapMKeys :: (Monad m, Ord k') => (k -> m k') -> Map k v -> m (Map k' v)
mapMKeys f = fmap M.fromList . M.foldrWithKey go (pure [])
  where
    go k x mxs = do
        k' <- f k
        xs <- mxs
        pure ((k', x) : xs)

parseChord :: Monad m => String -> m Key.Chord
parseChord = \case
    'C' : '-' : t -> fmap (`Key.withModifier` Key.Ctrl)  (parseChord t)
    'S' : '-' : t -> fmap (`Key.withModifier` Key.Shift) (parseChord t)
    'M' : '-' : t -> fmap (`Key.withModifier` Key.Meta)  (parseChord t)
    [c]           -> pure (Key.key (Key.Char c))
    "PgUp"        -> pure (Key.key Key.PageUp)
    "PgDown"      -> pure (Key.key Key.PageDown)
    "PgDn"        -> pure (Key.key Key.PageDown)
    s | Just k <- readMaybe s
                  -> pure (Key.key k)
      | otherwise -> fail ("Unknown key '" <> s <> "'")

jsonOptions :: Options
jsonOptions = defaultOptions
    { constructorTagModifier = camelTo '-'
    , fieldLabelModifier     = camelTo '-' . stripPrefix }
  where
    stripPrefix = \case
        '_' : 'm' : name -> name
        '_' : name       -> name
        name             -> name