{-# LANGUAGE OverloadedStrings #-}

-- | Functions that return a boolean based upon some criterion that
-- matches something, often a PostFam. Useful when filtering
-- Postings.
module Penny.Lincoln.Predicates.Siblings
  ( LPdct
  , MakePdct
  , payee
  , number
  , flag
  , postingMemo
  , qty
  , parseQty
  , drCr
  , debit
  , credit
  , commodity
  , account
  , accountLevel
  , accountAny
  , tag
  , reconciled

  -- * Serials
  , serialPdct
  , MakeSerialPdct
  , fwdGlobalPosting
  , backGlobalPosting
  , fwdFilePosting
  , backFilePosting
  , fwdGlobalTransaction
  , backGlobalTransaction
  , fwdFileTransaction
  , backFileTransaction
  ) where


import Control.Arrow (second)
import Data.List (intersperse)
import Data.Maybe (catMaybes)
import Data.Monoid ((<>))
import Data.Text (Text)
import qualified Data.Text as X
import qualified Penny.Lincoln.Bits as B
import qualified Penny.Lincoln.Ents as E
import Penny.Lincoln.Serial (forward, backward)
import Penny.Lincoln.HasText (HasText, text, HasTextList, textList)
import qualified Penny.Lincoln.Queries.Siblings as Q
import Penny.Lincoln.Ents (Posting)
import qualified Text.Matchers as M
import qualified Data.Prednote.Pdct as P

type LPdct = P.Pdct Posting

type MakePdct = M.Matcher -> LPdct

-- * Matching helpers
match
  :: HasText a
  => Text
  -- ^ Description of this field
  -> (Posting -> [a])
  -- ^ Function that returns the field being matched
  -> M.Matcher
  -> LPdct
match t f m = P.operand desc pd
  where
    desc = makeDesc t m
    pd = any (M.match m) . map text . f

matchMaybe
  :: HasText a
  => Text
  -- ^ Description of this field
  -> (Posting -> [Maybe a])
  -> M.Matcher
  -> LPdct
matchMaybe t f m = P.operand desc pd
  where
    desc = makeDesc t m
    pd = any (== (Just True))
         . map (fmap (M.match m . text))
         . f

makeDesc :: Text -> M.Matcher -> Text
makeDesc t m
  = "subject: " <> t <> " (any sibling posting) matcher: "
  <> M.matchDesc m

-- | Does the given matcher match any of the elements of the Texts in
-- a HasTextList?
matchAny
  :: HasTextList a
  => Text
  -> (Posting -> [a])
  -> M.Matcher
  -> LPdct
matchAny t f m = P.operand desc pd
  where
    desc = makeDesc t m
    pd = any (any (M.match m)) . map textList . f

-- | Does the given matcher match the text that is at the given
-- element of a HasTextList? If the HasTextList does not have a
-- sufficent number of elements to perform this test, returns False.
matchLevel
  :: HasTextList a
  => Int
  -> Text
  -> (Posting -> [a])
  -> M.Matcher
  -> LPdct
matchLevel l d f m = P.operand desc pd
  where
    desc = makeDesc ("level " <> X.pack (show l) <> " of " <> d) m
    pd pf = let doMatch list = if l < 0 || l >= length list
                               then False
                               else M.match m (list !! l)
            in any doMatch . map textList . f $ pf

-- | Does the matcher match the text of the memo? Joins each line of
-- the memo with a space.
matchMemo
  :: Text
  -> (Posting -> [Maybe B.Memo])
  -> M.Matcher
  -> LPdct
matchMemo t f m = P.operand desc pd
  where
    desc = makeDesc t m
    pd = any (maybe False doMatch) . f
    doMatch = M.match m
              . X.intercalate (X.singleton ' ')
              . B.unMemo

matchDelimited
  :: HasTextList a
  => Text
  -- ^ Separator
  -> Text
  -- ^ Label
  -> (Posting -> [a])
  -> M.Matcher
  -> LPdct
matchDelimited sep lbl f m = match lbl f' m
  where
    f' = map (X.concat . intersperse sep . textList) . f

-- * Pattern matching fields

payee :: MakePdct
payee = matchMaybe "payee" Q.payee

number :: MakePdct
number = matchMaybe "number" Q.number

flag :: MakePdct
flag = matchMaybe "flag" Q.flag

postingMemo :: MakePdct
postingMemo = matchMemo "posting memo" Q.postingMemo

-- | A Pdct that returns True if @compare subject qty@ returns the
-- given Ordering.
qty :: Ordering -> B.Qty -> LPdct
qty o q = P.operand desc pd
  where
    desc = "quantity of any sibling is " <> dd <> " " <> X.pack (show q)
    dd = case o of
      LT -> "less than"
      GT -> "greater than"
      EQ -> "equal to"
    pd = any ((== o) . (`compare` q)) . Q.qty

parseQty
  :: X.Text
  -> Maybe (B.Qty -> LPdct)
parseQty x
  | x == "==" = Just (qty EQ)
  | x == "=" = Just (qty EQ)
  | x == ">" = Just (qty GT)
  | x == "<" = Just (qty LT)
  | x == "/=" = Just (\q -> P.not (qty EQ q))
  | x == "!=" = Just (\q -> P.not (qty EQ q))
  | x == ">=" = Just (\q -> P.or [qty GT q, qty EQ q])
  | x == "<=" = Just (\q -> P.or [qty LT q, qty EQ q])
  | otherwise = Nothing

drCr :: B.DrCr -> LPdct
drCr dc = P.operand desc pd
  where
    desc = "entry of any sibling is a " <> s
    s = case dc of { B.Debit -> "debit"; B.Credit -> "credit" }
    pd = any (== dc) . Q.drCr

debit :: LPdct
debit = drCr B.Debit

credit :: LPdct
credit = drCr B.Credit

commodity :: M.Matcher -> LPdct
commodity = match "commodity" Q.commodity

account :: M.Matcher -> LPdct
account = matchDelimited ":" "account" Q.account

accountLevel :: Int -> M.Matcher -> LPdct
accountLevel i = matchLevel i "account" Q.account

accountAny :: M.Matcher -> LPdct
accountAny = matchAny "any sub-account" Q.account

tag :: M.Matcher -> LPdct
tag = matchAny "any tag" Q.tags

-- | True if a posting is reconciled; that is, its flag is exactly
-- @R@.
reconciled :: LPdct
reconciled = P.operand d p
  where
    d = "posting flag is exactly \"R\" (is reconciled)"
    p = any (maybe False ((== X.singleton 'R') . B.unFlag))
        . Q.flag

--
-- Serials
--

-- | Makes Pdct based on comparisons against a particular serial.

serialPdct
  :: Text
  -- ^ Name of the serial, e.g. @globalPosting@

  -> ((B.TopLineData, E.Ent B.PostingData) -> Maybe Int)
  -- ^ How to obtain the serial from the item being examined

  -> Int
  -- ^ The right hand side

  -> Ordering
  -- ^ The Pdct returned will be Just True if the item has a serial
  -- and @compare ser rhs@ returns this Ordering; Just False if the
  -- item has a srerial and @compare@ does not return this Ordering;
  -- Nothing if the item does not have a serial.

  -> P.Pdct E.Posting

serialPdct name getSer i o = P.Pdct n (P.Operand f)
  where
    n = "serial " <> name <> " is " <> descCmp <> " "
        <> X.pack (show i)
    descCmp = case o of
      EQ -> "equal to"
      LT -> "less than"
      GT -> "greater than"
    f = Just
        . any (\ser -> compare ser i == o )
        . catMaybes
        . map getSer
        . E.unrollSnd
        . second (\(x, xs) -> (x:xs))
        . second E.tailEnts
        . E.unPosting

type MakeSerialPdct = Int -> Ordering -> P.Pdct Posting

fwdGlobalPosting :: MakeSerialPdct
fwdGlobalPosting =
  serialPdct "fwdGlobalPosting"
  $ fmap (forward . B.unGlobalPosting)
  . B.pdGlobal
  . E.meta
  . snd

backGlobalPosting :: MakeSerialPdct
backGlobalPosting =
  serialPdct "revGlobalPosting"
  $ fmap (backward . B.unGlobalPosting)
  . B.pdGlobal
  . E.meta
  . snd

fwdFilePosting :: MakeSerialPdct
fwdFilePosting
  = serialPdct "fwdFilePosting"
  $ fmap (forward . B.unFilePosting . B.pFilePosting)
  . B.pdFileMeta
  . E.meta
  . snd

backFilePosting :: MakeSerialPdct
backFilePosting
  = serialPdct "revFilePosting"
  $ fmap (backward . B.unFilePosting . B.pFilePosting)
  . B.pdFileMeta
  . E.meta
  . snd

fwdGlobalTransaction :: MakeSerialPdct
fwdGlobalTransaction
  = serialPdct "fwdGlobalTransaction"
  $ fmap (forward . B.unGlobalTransaction)
  . B.tlGlobal
  . fst

backGlobalTransaction :: MakeSerialPdct
backGlobalTransaction
  = serialPdct "backGlobalTransaction"
  $ fmap (backward . B.unGlobalTransaction)
  . B.tlGlobal
  . fst

fwdFileTransaction :: MakeSerialPdct
fwdFileTransaction
  = serialPdct "fwdFileTransaction"
  $ fmap (forward . B.unFileTransaction . B.tFileTransaction)
  . B.tlFileMeta
  . fst

backFileTransaction :: MakeSerialPdct
backFileTransaction
  = serialPdct "backFileTransaction"
  $ fmap (backward . B.unFileTransaction . B.tFileTransaction)
  . B.tlFileMeta
  . fst