module Penny.Lincoln.Balance (
    Balance
  , unBalance
  , Balanced(Balanced, Inferable, NotInferable)
  , balanced
  , isInferable
  , entryToBalance
  , entriesToBalanced
  , removeZeroCommodities
  , BottomLine(Zero, NonZero)
  , Column(Column, colDrCr, colQty)
  ) where

import Data.Map ( Map )
import qualified Data.Map as M
import Data.Monoid ( Monoid, mempty, mappend, mconcat )

import Penny.Lincoln.Bits (
  add, difference, Difference(LeftBiggerBy, RightBiggerBy, Equal))
import qualified Penny.Lincoln.Bits as B

-- | A balance summarizes several entries. You do not create a Balance
-- directly. Instead, use 'entryToBalance'.
newtype Balance = Balance (Map B.Commodity BottomLine)
                  deriving (Show, Eq)

-- | Returns a map where the keys are the commodities in the balance
-- and the values are the balance for each commodity. If there is no
-- balance at all, this map can be empty.
unBalance :: Balance -> Map B.Commodity BottomLine
unBalance (Balance m) = m

-- | Returned by 'balanced'.
data Balanced = Balanced
              | Inferable (B.Entry B.Qty)
              | NotInferable
              deriving (Show, Eq)

-- | Computes whether a Balance map is Balanced.
--
-- > balanced mempty == Balanced
balanced :: Balance -> Balanced
balanced (Balance m) = M.foldrWithKey f Balanced m where
  f c n b = case n of
    Zero -> b
    (NonZero col) -> case b of
      Balanced -> let
        dc = case colDrCr col of
          B.Debit -> B.Credit
          B.Credit -> B.Debit
        q = colQty col
        in Inferable (B.Entry dc (B.Amount q c))
      _ -> NotInferable

isInferable :: Balanced -> Bool
isInferable (Inferable _) = True
isInferable _ = False

-- | Converts an Entry to a Balance.
entryToBalance :: B.HasQty q => B.Entry q -> Balance
entryToBalance (B.Entry dc am) = Balance $ M.singleton c no where
  c = B.commodity am
  no = NonZero (Column dc (B.toQty . B.qty $ am))

-- | Converts multiple Entries to a Balanced.
entriesToBalanced :: B.HasQty q => [B.Entry q] -> Balanced
entriesToBalanced
  = balanced
  . mconcat
  . map entryToBalance

data BottomLine = Zero
            | NonZero Column
            deriving (Show, Eq)

instance Monoid BottomLine where
  mempty = Zero
  mappend n1 n2 = case (n1, n2) of
    (Zero, Zero) -> Zero
    (Zero, (NonZero c)) -> NonZero c
    ((NonZero c), Zero) -> NonZero c
    ((NonZero c1), (NonZero c2)) ->
      let (Column dc1 q1) = c1
          (Column dc2 q2) = c2
      in if dc1 == dc2
         then NonZero $ Column dc1 (q1 `add` q2)
         else case difference q1 q2 of
           LeftBiggerBy diff ->
             NonZero $ Column dc1 diff
           RightBiggerBy diff ->
             NonZero $ Column dc2 diff
           Equal -> Zero

data Column = Column { colDrCr :: B.DrCr
                     , colQty :: B.Qty }
              deriving (Show, Eq)


-- | Add two Balances together. Commodities are never removed from the
-- balance, even if their balance is zero. Instead, they are left in
-- the balance. Sometimes you want to know that a commodity was in the
-- account but its balance is now zero.
instance Monoid Balance where
  mempty = Balance M.empty
  mappend (Balance t1) (Balance t2) =
    Balance $ M.unionWith mappend t1 t2


-- | Removes zero balances from a Balance.
removeZeroCommodities :: Balance -> Balance
removeZeroCommodities (Balance m) =
  let p b = case b of
        Zero -> False
        _ -> True
      m' = M.filter p m
  in Balance m'