-------------------------------------------------------------------------------
-- |
-- Module      :  Text.SmallCaps.ConfigParser
-- Copyright   :  (c) Stefan Berthold 2014-2015
-- License     :  BSD3-style (see LICENSE)
--
-- Maintainer  :  stefan.berthold@gmx.net
-- Stability   :  unstable
-- Portability :  GHC
--
-- This module specifies inline configuration parsers. The parsers are also
-- used for the arguments in the command line interface.
--
-------------------------------------------------------------------------------

module Text.SmallCaps.ConfigParser where

import Prelude hiding ( lex, takeWhile )

import Data.Char                  ( isAlpha, isAlphaNum, isPunctuation )
import Data.Text hiding           ( replace, takeWhile )
import Data.Map                   ( Map )
import qualified Data.Map as Map  ( lookup )
import Data.Attoparsec.Text       ( Parser, parseOnly, char, takeWhile1, asciiCI, skipSpace, isEndOfLine )
import Data.Attoparsec.Combinator ( many' )
import Control.Monad              ( mplus, msum )

import Text.SmallCaps.LaTeX       ( LaTeXElement, name )
import Text.SmallCaps.Config      ( ParserState (..), Config (..), PatternReplace (..), defaultReplaceTemplate, defaultReplaceTemplate', blacklist, whitelist )
import Text.SmallCaps.TeXParser   ( macroBegin, macroName )

reconfigure :: ParserState -> Text -> Either (Text, Config) Config
reconfigure state = either (const (Right (config state))) id . parseOnly (reconfiguration state)

reconfiguration :: ParserState -> Parser (Either (Text, Config) Config)
reconfiguration state = preamble >> msum
  [ fmap Right  $ profileMain (profile state)
  , fmap Left   $ storeMain   conf
  , fmap Right  $ periodMain  conf
  , fmap Right  $ replaceMain conf
  , fmap Right  $ searchMain  conf
  , fmap Right  $ isolateMain conf
  , fmap Right  $ skipMain    conf
  , fmap Right  $ unskipMain  conf
  , fmap Right  $ eosMain     conf
  , fmap Right  $ exceptMain  conf
  ] where conf = config state

-- ** Lexer

lex :: Parser a -> Parser a
lex p = skipSpace >> p

-- ** Preamble

preamble :: Parser Text
preamble = char '%' >> lex (asciiCI (pack "smallcaps"))

-- ** Restore profile

profileMain :: Map Text Config -> Parser Config
profileMain ps = profilePre >> profileName ps

profilePre :: Parser Text
profilePre = lex (asciiCI (pack "reset") `mplus` asciiCI (pack "restore")) >> lex (asciiCI (pack "profile"))

profileName :: Map Text Config -> Parser Config
profileName ps = maybe (fail "profile not found") return . flip Map.lookup ps =<< lex (takeWhile1 isAlphaNum)

-- ** Store profile

storeMain :: Config -> Parser (Text, Config)
storeMain = (storePre >>) . storeName

storePre :: Parser Text
storePre = lex (asciiCI (pack "store")) >> lex (asciiCI (pack "profile"))

storeName :: Config -> Parser (Text, Config)
storeName conf = fmap (flip (,) conf) (lex $ takeWhile1 isAlphaNum)

-- ** Period chars

periodMain :: Config -> Parser Config
periodMain = (periodPre >>) . periodSigns

periodPre :: Parser Text
periodPre = lex (asciiCI (pack "periods")) >> lex (asciiCI (pack "are"))

periodSigns :: Config -> Parser Config
periodSigns conf = lex (takeWhile1 isPunctuation) >>= \s -> return $ conf { periodChars = unpack s }

-- ** Replace string

replaceMain :: Config -> Parser Config
replaceMain conf = replacePre >> msum
  [ replaceStyleNoarg
  , replaceStyleInarg
  ] >>= replaceMacro conf

replacePre :: Parser Text
replacePre = lex $ asciiCI (pack "substitution")

data Style = NoArg | InArg deriving (Show, Eq)

replaceStyleNoarg :: Parser Style
replaceStyleNoarg = lex (asciiCI (pack "in")) >> lex (asciiCI (pack "block")) >> lex (asciiCI (pack "with")) >> return NoArg

replaceStyleInarg :: Parser Style
replaceStyleInarg = lex (asciiCI (pack "as")) >> lex (asciiCI (pack "argument")) >> lex (asciiCI (pack "of")) >> return InArg

replaceMacro :: Config -> Style -> Parser Config
replaceMacro conf style
  | style == NoArg  = fun defaultReplaceTemplate
  | otherwise       = fun defaultReplaceTemplate'
  where fun gun = lex $ macroBegin >> macroName >>= \macro -> return $ conf { replace = gun (cons '\\' macro) }

-- ** Search filter

searchMain :: Config -> Parser Config
searchMain = (searchPre >>) . searchList

searchPre :: Parser Text
searchPre = lex $ asciiCI (pack "search")

searchList :: Config -> Parser Config
searchList conf = list' (search conf) >>= \fun -> return $ conf { search = fun }

-- ** Isolate filter

isolateMain :: Config -> Parser Config
isolateMain = (isolatePre >>) . isolateList

isolatePre :: Parser Text
isolatePre = lex $ asciiCI (pack "isolate")

isolateList :: Config -> Parser Config
isolateList conf = iList (isolate conf) >>= \fun -> return $ conf { isolate = fun }

-- ** Skip filter

skipMain :: Config -> Parser Config
skipMain = (skipPre >>) . skipList

skipPre :: Parser Text
skipPre = lex $ asciiCI (pack "skip")

skipList :: Config -> Parser Config
skipList conf = list (skip conf) >>= \fun -> return $ conf { skip = fun }

-- ** Unskip filter

unskipMain :: Config -> Parser Config
unskipMain = (unskipPre >>) . unskipList

unskipPre :: Parser Text
unskipPre = lex $ asciiCI (pack "unskip")

unskipList :: Config -> Parser Config
unskipList conf = list (unskip conf) >>= \fun -> return $ conf { unskip = fun }

-- ** End of sentence filter

eosMain :: Config -> Parser Config
eosMain = (eosPre >>) . eosList

eosPre :: Parser Text
eosPre = lex $ asciiCI (pack "eos")

eosList :: Config -> Parser Config
eosList conf = list (eos conf) >>= \fun -> return $ conf { eos = fun }

-- ** Except and replace words

exceptMain :: Config -> Parser Config
exceptMain = (exceptPre >>) . exceptTuple

exceptPre :: Parser Text
exceptPre = lex (asciiCI (pack "except"))

exceptTuple :: Config -> Parser Config
exceptTuple conf = do
  word <- lex (takeWhile1 isAlphaNum)
  repl <- (lex (asciiCI (pack "put")) >> lex (takeWhile1 (not . isEndOfLine))) `mplus` return word
  return $ conf { exceptions = PatternReplace
                                { pattern     = word
                                , replacement = repl
                                } : exceptions conf }

-- ** Macro/environment name list parser

list :: (LaTeXElement -> Bool) -> Parser (LaTeXElement -> Bool)
list fun = msum [listBlack fun, listWhite fun, listConstAll, listConstNone]

list' :: (LaTeXElement -> Bool) -> Parser (LaTeXElement -> Bool)
list' fun = msum [listBlack fun, listWhite fun, listConstAll', listConstNone']

listBlack :: (LaTeXElement -> Bool) -> Parser (LaTeXElement -> Bool)
listBlack fun = lex (char '-') >> listItems >>= \xs -> return (\x -> not (name x `elem` xs) && fun x)

listWhite :: (LaTeXElement -> Bool) -> Parser (LaTeXElement -> Bool)
listWhite fun = lex $ char '+' >> listItems >>= \xs -> return (\x -> name x `elem` xs || fun x)

listConstAll :: Parser (a -> Bool)
listConstAll = lex (char '*') >> return (const True)

listConstAll' :: Parser (LaTeXElement -> Bool)
listConstAll' = lex (char '*') >> return (blacklist [])

listConstNone :: Parser (a -> Bool)
listConstNone = lex (char '/') >> return (const False)

listConstNone' :: Parser (LaTeXElement -> Bool)
listConstNone' = lex (char '/') >> return (whitelist [])

-- ** Isolate list parser

iList :: (LaTeXElement -> Maybe Text) -> Parser (LaTeXElement -> Maybe Text)
iList fun = msum [iListBlack fun, iListWhite fun, iListConstAll, iListConstNone]

iListBlack :: (LaTeXElement -> Maybe Text) -> Parser (LaTeXElement -> Maybe Text)
iListBlack fun = do
  _   <- lex $ char '-'
  xs  <- listItems
  return $ \x ->  if x `isElement` xs
                  then Nothing
                  else fun x

iListWhite :: (LaTeXElement -> Maybe Text) -> Parser (LaTeXElement -> Maybe Text)
iListWhite fun = do
  c   <- lex $ takeWhile1 isAlphaNum `mplus` return (pack "default")
  _   <- lex $ char '+'
  xs  <- listItems
  return $ \x ->  if x `isElement` xs
                  then Just c
                  else fun x

iListConstAll :: Parser (LaTeXElement -> Maybe Text)
iListConstAll = do
  c   <- lex $ takeWhile1 isAlphaNum `mplus` return (pack "default")
  _   <- lex $ char '*'
  return $ const (Just c)

iListConstNone :: Parser (LaTeXElement -> Maybe Text)
iListConstNone = do
  _   <- lex $ char '/'
  return $ const Nothing

-- ** List item parser

listItems :: Parser [Text]
listItems = do
  x   <- listItem
  xs  <- many' (listItemSeparator >> listItem)
  return (x:xs)

listItem :: Parser Text
listItem = listItemMacro `mplus` listItemEnvironment

listItemMacro :: Parser Text
listItemMacro = lex (macroBegin >> fmap (cons '\\') macroName)

listItemEnvironment :: Parser Text
listItemEnvironment = lex (takeWhile1 isAlpha)

listItemSeparator :: Parser Char
listItemSeparator = lex $ char ','

isElement :: LaTeXElement -> [Text] -> Bool
isElement = elem . name

-- vim: ft=haskell:sts=2:sw=2:et:nu:ai