-- |
--
-- Copyright:
--   This file is part of the package vimeta. It is subject to the
--   license terms in the LICENSE file found in the top-level
--   directory of this distribution and at:
--
--     https://github.com/pjones/vimeta
--
--   No part of this package, including this file, may be copied,
--   modified, propagated, or distributed except according to the terms
--   contained in the LICENSE file.
--
-- License: BSD-2-Clause
module Vimeta.Core.Format
  ( FormatTable,
    fromFormatString,
    formatYear,
    formatFullDate,
  )
where

import Data.Time (Day (..), defaultTimeLocale, formatTime)
import Relude.Extra.Map
import System.Process.Internals (translate)
import Text.Parsec hiding ((<|>))

-- | Mapping of format characters to their possible replacement text.
type FormatTable = Map Char (Maybe Text)

-- | Syntax tree for format strings.
data Replacement
  = -- | Replace the given character.
    Replace Char
  | -- | Conditional section.
    Condition [(Text, Replacement)]
  | -- | End of input (or condition).
    EndOfInput

-- | Parser type.
type Parser a = ParsecT Text () (Reader FormatTable) a

-- | Replace format characters prefixed with a @%@ with the
-- replacement text found in the given 'Map'.
fromFormatString ::
  -- | Format character mapping.
  FormatTable ->
  -- | Name of format string.
  String ->
  -- | Input text.
  Text ->
  -- | Output text or error.
  Either String Text
fromFormatString table name input =
  case runReader (runParserT parseFormatString () name input) table of
    Left e -> Left (show e)
    Right t -> Right t

-- | Format a 'Day' using the XML schema notation.
formatFullDate :: Maybe Day -> Maybe Text
formatFullDate = formatDay "%Y-%m-%dT00:00:00Z"

-- | Format a 'Day' displaying just the year.
formatYear :: Maybe Day -> Maybe Text
formatYear = formatDay "%Y"

formatDay :: String -> Maybe Day -> Maybe Text
formatDay fmt d = toText . formatTime defaultTimeLocale fmt <$> d

parseFormatString :: Parser Text
parseFormatString = manyTill go eof >>= renderFormatString
  where
    go = findFormatCharacter >>= mkReplacement

-- | Render a format string syntax table as a 'Text' value.
renderFormatString :: [(Text, Replacement)] -> Parser Text
renderFormatString rs = do
  table <- ask
  return (mconcat $ map (render table) rs)
  where
    escape :: Text -> Text
    escape = toText . translate . toString
    findChar :: FormatTable -> Char -> Text
    findChar t c = fromMaybe "" $ join (lookup c t)
    render :: FormatTable -> (Text, Replacement) -> Text
    render tbl (txt, Replace c) = txt <> escape (findChar tbl c)
    render tbl (txt, Condition c) = txt <> renderCondition tbl c
    render _ (txt, EndOfInput) = txt
    renderCondition :: FormatTable -> [(Text, Replacement)] -> Text
    renderCondition tbl conds =
      if all (checkCondition tbl) conds
        then mconcat $ map (render tbl) conds
        else mempty
    checkCondition :: FormatTable -> (Text, Replacement) -> Bool
    checkCondition tbl (_, Replace c) = isJust (join $ lookup c tbl)
    checkCondition tbl (_, Condition c) = all (checkCondition tbl) c
    checkCondition _ (_, EndOfInput) = True

-- | Location a format character preceded by a @'%'@ character.
-- Returns the text leading up to the format character and the
-- character itself.
findFormatCharacter :: Parser (Text, Maybe Char)
findFormatCharacter = do
  beforeText <- toText <$> manyTill anyChar (try eofOrFormatChar)
  formatChar <- try $ (Just <$> anyChar) <|> return Nothing
  return (beforeText, formatChar)
  where
    eofOrFormatChar :: Parser ()
    eofOrFormatChar = eof <|> void (char '%')

-- | Translate the output from 'findFormatCharacter' into a syntax node.
mkReplacement :: (Text, Maybe Char) -> Parser (Text, Replacement)
mkReplacement (beforeText, formatChar) =
  case formatChar of
    Nothing -> return (beforeText, EndOfInput)
    Just '{' -> (beforeText,) <$> (Condition <$> parseConditional)
    Just c -> return (beforeText, Replace c)

-- | Parse a conditional section out of a format string.
parseConditional :: Parser [(Text, Replacement)]
parseConditional = do
  (beforeText, formatChar) <- findFormatCharacter

  case formatChar of
    -- Reached the end of the format string.
    Nothing -> unexpected "end of format string, expected `%}'"
    -- Start another conditional.
    Just '{' -> do
      other <- parseConditional
      return [(beforeText, Condition other)]

    -- End this conditional.
    Just '}' -> return [(beforeText, EndOfInput)]
    -- Add this replacement to the list, fetch the next one.
    Just c -> do
      next <- parseConditional
      return ((beforeText, Replace c) : next)