{-|
Module      : MasterPlan.Parser
Description : export parser for project systems
Copyright   : (c) Rodrigo Setti, 2017
License     : MIT
Maintainer  : rodrigosetti@gmail.com
Stability   : experimental
Portability : POSIX
-}
{-# LANGUAGE UnicodeSyntax #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
module MasterPlan.Parser (runParser) where

import           Control.Monad.State
import           Data.Generics
import           Data.List                  (nub)
import qualified Data.List.NonEmpty         as NE
import qualified Data.Map                   as M
import qualified Data.Text                  as T
import           Data.Void
import           MasterPlan.Data
import           Text.Megaparsec            hiding (State, runParser)
import           Text.Megaparsec.Char
import qualified Text.Megaparsec.Char.Lexer as L
import           Text.Megaparsec.Expr

type Parser = ParsecT Void T.Text (State ProjectSystem)

-- |Space consumer
sc  Parser ()
sc = L.space space1 lineCmnt blockCmnt
  where
    lineCmnt  = L.skipLineComment "//"
    blockCmnt = L.skipBlockComment "/*" "*/"

lexeme  Parser a  Parser a
lexeme = L.lexeme sc

symbol  T.Text  Parser T.Text
symbol = L.symbol sc

-- | 'parens' parses something between parenthesis.
parens  Parser a  Parser a
parens = between (symbol "(") (symbol ")")

-- |list of reserved words
rws  [String]
rws = map show [minBound :: ProjProperty ..]

identifier  Parser String
identifier = (lexeme . try) (p >>= check)
  where
    p       = (:) <$> letterChar <*> many alphaNumChar
    check x
      | x `elem` rws = fail $ "keyword " ++ show x ++ " cannot be an identifier"
      | otherwise    = pure x

stringLiteral :: Parser String
stringLiteral = char '"' >> manyTill L.charLiteral (char '"')

percentage :: Parser Float
percentage = do n <- L.float <?> "percentage value"
                when (n > 100) $ fail $ "number " ++ show n ++ " is not within (0,100) range"
                void $ symbol "%"
                pure $ n / 100

nonNegativeNumber :: Parser Float
nonNegativeNumber = L.float

definition  Parser ()
definition =
    choice ([ propsProp PTitle stringLiteral (\v p -> p { title = v })
            , propsProp PDescription stringLiteral (\v p -> p { description = Just v})
            , propsProp PUrl stringLiteral (\v p -> p { url = Just v})
            , propsProp POwner stringLiteral (\v p -> p { owner = Just v})
            , taskProp  PCost nonNegativeNumber (\v b -> case b of BindingAtomic r _ t p -> BindingAtomic r v t p; _ -> b)
            , taskProp  PTrust percentage (\v b -> case b of BindingAtomic r c _ p -> BindingAtomic r c v p; _ -> b)
            , taskProp  PProgress percentage (\v b -> case b of BindingAtomic r c t _ -> BindingAtomic r c t v; _ -> b)
            , structure ] :: [Parser ()])
  where
    structure :: Parser ()
    structure = do projName <- identifier
                   projectExpr <- symbol "=" *> expressionParser
                   sys <- lift get

                   -- check if it's recursive
                   let deps = dependencies sys projectExpr
                   when (projName `elem` deps) $ fail $ "definition of \"" ++ projName ++ "\" is recursive"

                   let binding = M.lookup projName $ bindings sys
                   newBinding <- case binding of
                                   Nothing -> pure $ BindingExpr (defaultProjectProps { title=projName }) projectExpr
                                   Just BindingExpr {} -> fail $ "Redefinition of \"" ++ projName ++ "\"."
                                   Just (BindingPlaceholder p) -> pure $ BindingExpr p projectExpr
                                   Just BindingAtomic {} -> fail $ "ProjectExpr \"" ++ projName ++ "\" is atomic"

                   lift $ put $ sys { bindings = M.insert projName newBinding $ bindings sys }

    propsProp :: ProjProperty -> Parser a -> (a -> ProjectProperties -> ProjectProperties) -> Parser ()
    propsProp prop valueParser modifier =
       property prop valueParser setter
     where
       setter projName val Nothing = pure $ BindingPlaceholder $ modifier val $ defaultProjectProps { title=projName }
       setter _ val (Just p) = pure $ everywhere (mkT $ modifier val) p

    taskProp :: ProjProperty -> Parser a -> (a -> Binding -> Binding) -> Parser ()
    taskProp prop valueParser modifier =
       property prop valueParser setter
     where
       setter projName val Nothing = pure $ modifier val $ defaultTaskProj defaultProjectProps { title=projName }
       setter projName _ (Just BindingExpr {}) = fail $ "ProjectExpr \"" ++ projName ++ "\" is not atomic."
       setter _ val (Just (BindingPlaceholder p)) = pure $ modifier val $ defaultTaskProj p
       setter _ val (Just p@BindingAtomic {}) = pure $ modifier val p

    property  ProjProperty  Parser a  (String -> a -> Maybe Binding -> Parser Binding) -> Parser ()
    property prop valueParser setter =
       do void $ symbol $ T.pack $ show prop
          projName <- parens identifier
          mBinding <- lift $ M.lookup projName <$> gets bindings
          value <- symbol "=" *> valueParser
          newBinding <- setter projName value mBinding
          let modifySys :: ProjectSystem -> ProjectSystem
              modifySys sys = sys { bindings = M.insert projName newBinding $ bindings sys }
          lift $ modify modifySys

expressionParser  Parser ProjectExpr
expressionParser =
    simplifyProj <$> makeExprParser term table <?> "expression"
  where
    term = parens expressionParser <|> (Reference <$> identifier)
    table = [[binary "*" (combineWith Product)]
            ,[binary "->" (combineWith Sequence)]
            ,[binary "+" (combineWith Sum)]]
    binary  op f = InfixL  (f <$ symbol op)

    combineWith :: (NE.NonEmpty ProjectExpr -> ProjectExpr) -> ProjectExpr -> ProjectExpr -> ProjectExpr
    combineWith c p1 p2 = c $ p1 NE.<| [p2]

dependencies  ProjectSystem -> ProjectExpr  [ProjectKey]
dependencies sys = everything (++) ([] `mkQ` collectDep)
  where
    collectDep (Reference n) = nub $ n : everything (++) ([] `mkQ` collectDep) (M.lookup n $ bindings sys)
    collectDep _ = []

projectSystem :: Parser ProjectSystem
projectSystem =
    do between sc eof definitionSeq
       lift get
 where
   definitionSeq = void $ endBy1 definition (symbol ";")


runParser :: FilePath -> T.Text -> Either String ProjectSystem
runParser filename contents = let mr = runParserT projectSystem filename contents
                                  initialPS = ProjectSystem M.empty
                              in case evalState mr initialPS of
                                    Left e -> Left $ parseErrorPretty' contents e
                                    Right v -> Right v