module Penny.Zinc.Parser.Filter (
  parseFilter
  , Error(LibertyError, TokenParseError)
  , NeedsHelp(NeedsHelp)
  , Result(Result, resultFactory, resultSensitive, sorterFilterer)
  ) where

import Control.Applicative ((<|>), (<$>), Applicative, pure, many)
import Control.Monad ((>=>))
import qualified Control.Monad.Exception.Synchronous as Ex
import Data.Monoid (mempty, mappend)
import Data.Text (Text)
import qualified Text.Matchers.Text as M
import qualified System.Console.MultiArg.Combinator as C
import System.Console.MultiArg.Prim (Parser)

import qualified Penny.Copper as Cop
import qualified Penny.Lincoln as L
import qualified Penny.Liberty as Ly
import qualified Penny.Liberty.Expressions as X

import qualified Penny.Zinc.Parser.Defaults as D
import qualified Penny.Zinc.Parser.Defaults as Defaults

-- | Parses all filtering options. Returns a parser that contains an
-- Exception if some error occurred after parsing the options, or a
-- Success with a result if the parse was successful.
parseFilter ::
  Defaults.T
  -> Parser (Ex.Exceptional Error (Either NeedsHelp Result))
parseFilter d = fmap f (many parser) where
  f ls =
    let k = foldl (>=>) return ls
    in case k (newState d) of
      Ex.Success st' ->
        if help st'
        then return . Left $ NeedsHelp
        else
          case Ly.parsePredicate . tokens $ st' of
            Nothing -> Ex.throw TokenParseError
            Just pdct ->
              let fn = Ly.xactionsToFiltered pdct
                       (postFilter st') (orderer st')
                  r = Result { resultFactory = factory st'
                             , resultSensitive = sensitive st'
                             , sorterFilterer = fn }
              in return . Right $ r
      Ex.Exception e -> Ex.Exception e

data Error = LibertyError Ly.Error
             | TokenParseError
             deriving Show

-- | Returned if the user requested help.
data NeedsHelp = NeedsHelp
                 deriving Show

-- | Indicates the result of a successful parse of filtering options.
data Result =
  Result { resultFactory :: M.CaseSensitive
                            -> Text -> Ex.Exceptional Text (Text -> Bool)
           -- ^ The factory indicated, so that it can be used in
           -- subsequent parses of the same command line.

         , resultSensitive :: M.CaseSensitive
           -- ^ Indicated case sensitivity, so that it can be used in
           -- subsequent parses of the command line.
           
         , sorterFilterer :: [L.Transaction] -> [L.Box Ly.LibertyMeta]
           -- ^ Applied to a list of Transaction, will sort and filter
           -- the transactions and assign them LibertyMeta.
         }


data State =
  State { sensitive :: M.CaseSensitive
        , factory :: M.CaseSensitive
                     -> Text -> Ex.Exceptional Text (Text -> Bool)
        , tokens :: [X.Token (L.PostFam -> Bool)]
        , postFilter :: [Ly.PostFilterFn]
        , orderer :: Ly.Orderer
        , help :: Bool
        , currentTime :: L.DateTime
        , defaultTimeZone :: Cop.DefaultTimeZone
        , radGroup :: Cop.RadGroup }

newState ::
  Defaults.T
  -> State
newState d =
  State { sensitive = D.sensitive d
        , factory = D.factory d
        , tokens = []
        , postFilter = []
        , orderer = mempty
        , help = False
        , currentTime = D.currentTime d
        , defaultTimeZone = D.defaultTimeZone d
        , radGroup = D.radGroup d }

parser :: Parser (State -> Ex.Exceptional Error State)
parser =
  operand
  <|> parsePostFilter
  <|> impurify parseMatcherSelect
  <|> impurify parseCaseSelect
  <|> impurify parseOperator
  <|> parseSort
  <|> impurify parseHelp

option :: [String] -> [Char] -> C.ArgSpec a -> Parser a
option ss cs a = C.parseOption [C.OptSpec ss cs a]

operand :: Parser (State -> Ex.Exceptional Error State)
operand = f <$> Ly.parseOperand
  where
    f lyFn =
      let g st =
            let r = lyFn (currentTime st) (defaultTimeZone st)
                    (radGroup st) (sensitive st) (factory st)
            in case r of
              Ex.Exception e -> Ex.throw . LibertyError $ e
              Ex.Success (X.Operand o) ->
                let tok' = tokens st ++ [X.TokOperand o]
                in return st { tokens = tok' }
      in g
                   
parsePostFilter :: Parser (State -> Ex.Exceptional Error State)
parsePostFilter = f <$> Ly.parsePostFilter
  where
    f lyResult =
      let g st = case lyResult of
            Ex.Exception e -> Ex.throw . LibertyError $ e
            Ex.Success pf ->
              let ls' = postFilter st ++ [pf]
              in return st { postFilter = ls' }
      in g

impurify ::
  (Functor f, Applicative a)
  => f (b -> b)
  -> f (b -> a b)
impurify = fmap (pure .)

parseMatcherSelect :: Parser (State -> State)
parseMatcherSelect = f <$> Ly.parseMatcherSelect
  where
    f fty = g
      where
        g st = st { factory = fty }

parseCaseSelect :: Parser (State -> State)
parseCaseSelect = f <$> Ly.parseCaseSelect
  where
    f sel = g
      where
        g st = st { sensitive = sel }

parseOperator :: Parser (State -> State)
parseOperator = f <$> Ly.parseOperator
  where
    f tok = g
      where
        g st = st { tokens = tokens st ++ [tok] }

parseSort :: Parser (State -> Ex.Exceptional Error State)
parseSort = f <$> Ly.parseSort
  where
    f exOrd = g
      where
        g st = case exOrd of
          Ex.Exception e -> Ex.throw . LibertyError $ e
          Ex.Success o ->
            return st { orderer = mappend o (orderer st) }

parseHelp :: Parser (State -> State)
parseHelp = option ["help"] ['h'] (C.NoArg f)
  where
    f st = st { help = True }

{-
wrapLiberty ::
  DefaultTimeZone
  -> DateTime
  -> RadGroup
  -> State
  -> ParserE Error State
wrapLiberty dtz dt rg st = let
  toLibSt = LF.State { LF.sensitive = sensitive st
                     , LF.factory = factory st
                     , LF.tokens = tokens st
                     , LF.postFilter = postFilter st }
  fromLibSt libSt = State { sensitive = LF.sensitive libSt
                          , factory = LF.factory libSt
                          , tokens = LF.tokens libSt
                          , postFilter = LF.postFilter libSt
                          , orderer = orderer st
                          , help = help st }
  in fromLibSt <$> LF.parseOption dtz dt rg toLibSt

wrapOrderer :: State -> ParserE Error State
wrapOrderer st = mkSt <$> S.sort where
  mkSt o = st { orderer = o `mappend` (orderer st) }

helpOpt :: ParserE Error ()
helpOpt = do
  let lo = makeLongOpt . pack $ "help"
      so = makeShortOpt 'h'
  _ <- mixedNoArg lo [] [so]
  return ()

wrapHelp :: State -> ParserE Error State
wrapHelp st = (\_ -> st { help = Help }) <$> helpOpt

parseOption ::
  DefaultTimeZone
  -> DateTime
  -> RadGroup
  -> State
  -> ParserE Error State
parseOption dtz dt rg st =
  wrapLiberty dtz dt rg st
  <|> wrapOrderer st
  <|> wrapHelp st

parseOptions ::
  DefaultTimeZone
  -> DateTime
  -> RadGroup
  -> State
  -> ParserE Error State
parseOptions dtz dt rg st =
  option st $ do
    rs <- runUntilFailure (parseOption dtz dt rg) st
    if null rs then return st else return (last rs)

parseFilter ::
  DefaultTimeZone
  -> DateTime
  -> RadGroup
  -> ParserE Error (Either NeedsHelp Result)
parseFilter dtz dt rg = do
  st' <- parseOptions dtz dt rg newState
  case help st' of
    Help -> return . Left $ NeedsHelp
    NoHelp -> do
      p <- case Oo.getPredicate (tokens st') of
        Just pr -> return pr
        Nothing -> throw E.BadExpression
      let f = sortFilterAndPostFilter (orderer st') p (postFilter st')
          r = Result { resultFactory = factory st'
                     , resultSensitive = sensitive st'
                     , sorterFilterer = f }
      return . Right $ r

sortFilterAndPostFilter ::
  S.Orderer
  -> (T.PostingInfo -> Bool)
  -> ([T.PostingInfo] -> [T.PostingInfo])
  -> [PostingBox] -> [T.PostingInfo]
sortFilterAndPostFilter o p pf =
  pf
  . filter p
  . PSq.sortedPostingInfos o
-}