{-# LANGUAGE DeriveFunctor #-}

-- | Containers for entries.
--
-- This module is the key guardian of the core principle of
-- double-entry accounting, which is that debits and credits must
-- always balance. An 'Ent' is a container for an 'Entry'. An 'Entry'
-- holds a 'DrCr' and an 'Amount' which, in turn, holds a 'Commodity'
-- and a 'Qty'. For a given 'Commodity' in a particular transaction,
-- the sum of the debits must always be equal to the sum of the
-- credits.
--
-- In addition to the 'Entry', the 'Ent' holds information about
-- whether the particular 'Entry' it holds is inferred or not. An Ent
-- is @inferred@ if the user did not supply the entry, but Penny was
-- able to deduce its 'Entry' because proper entries were supplied for
-- all the other postings in the transaction. The 'Ent' also holds
-- arbitrary metadata--which will typically be other information about
-- the particular posting, such as the payee, account, etc.
--
-- A collection of 'Ent' is an 'Ents'. This module will only create an
-- 'Ent' as part of an 'Ents' (though you can later separate the 'Ent'
-- from its other 'Ents' if you like.) In any given 'Ents', all of the
-- 'Ent' collectively have a zero balance.
--
-- This module also contains type synonyms used to represent a
-- Posting, which is an Ent bundled with its sibling Ents, and a
-- Transaction.

module Penny.Lincoln.Ents
  ( -- * Ent
    Ent
  , entry
  , meta
  , inferred

  -- * Ents
  , Ents
  , unEnts
  , tupleEnts
  , mapEnts
  , traverseEnts
  , ents
  , rEnts
  , headEnt
  , tailEnts

  -- * Postings and transactions
  , Posting(..)
  , Transaction(..)
  , transactionToPostings
  , views
  , unrollSnd
  ) where

import Control.Applicative
import Control.Arrow (second)
import qualified Penny.Lincoln.Bits as B
import qualified Penny.Lincoln.Balance as Bal
import Control.Monad (guard)
import qualified Penny.Lincoln.Equivalent as Ev
import Penny.Lincoln.Equivalent ((==~))
import Data.Monoid (mconcat, (<>))
import Data.List (foldl', unfoldr, sortBy)
import Data.Maybe (catMaybes)
import qualified Data.Traversable as Tr
import qualified Data.Foldable as Fdbl


-- | Information about an entry, along with whether it is inferred and
-- its metadata.
data Ent m = Ent
  { entry :: Either (B.Entry B.QtyRep) (B.Entry B.Qty)
  -- ^ The entry from an Ent.  If the Ent is inferred--that is, if the
  -- user did not supply an entry for it and Penny was able to infer
  -- the entry--this will be a Right with the inferred Entry.

  , meta :: m
  -- ^ The metadata accompanying an Ent

  , inferred :: Bool
  -- ^ True if the entry was inferred.
  } deriving (Eq, Ord, Show)

-- | Two Ents are equivalent if the entries are equivalent and the
-- metadata is equivalent (whether the Ent is inferred or not is
-- ignored.)
instance Ev.Equivalent m => Ev.Equivalent (Ent m) where
  equivalent (Ent e1 m1 _) (Ent e2 m2 _) = e1 ==~ e2 && m1 ==~ m2

  compareEv (Ent e1 m1 _) (Ent e2 m2 _) =
    Ev.compareEv e1 e2 <> Ev.compareEv m1 m2

instance Functor Ent where
  fmap f (Ent e m i) = Ent e (f m) i

newtype Ents m = Ents { unEnts :: [Ent m] }
  deriving (Eq, Ord, Show, Functor)

-- | Ents are equivalent if the content Ents of each are
-- equivalent. The order of the ents is insignificant.
instance Ev.Equivalent m => Ev.Equivalent (Ents m) where
  equivalent (Ents e1) (Ents e2) =
    let (e1', e2') = (sortBy Ev.compareEv e1, sortBy Ev.compareEv e2)
    in and $ (length e1 == length e2)
           : zipWith Ev.equivalent e1' e2'

  compareEv (Ents e1) (Ents e2) =
    let (e1', e2') = (sortBy Ev.compareEv e1, sortBy Ev.compareEv e2)
    in mconcat $ compare (length e1) (length e2)
               : zipWith Ev.compareEv e1' e2'

instance Fdbl.Foldable Ents where
  foldr f z (Ents ls) = case ls of
    [] -> z
    x:xs -> f (meta x) (Fdbl.foldr f z (map meta xs))

instance Tr.Traversable Ents where
  sequenceA = fmap Ents . Tr.sequenceA . map seqEnt . unEnts

-- | Alter the metadata Ents, while examining the Ents themselves. If
-- you only want to change the metadata and you don't need to examine
-- the other contents of the Ent, use the Functor instance. You cannot
-- change non-metadata aspects of the Ent.
mapEnts :: (Ent a -> b) -> Ents a -> Ents b
mapEnts f = Ents . map f' . unEnts where
  f' e = e { meta = f e }

-- | Alter the metadata of Ents while examing their contents. If you
-- do not need to examine their contents, use the Traversable
-- instance.
traverseEnts :: Applicative f => (Ent a -> f b) -> Ents a -> f (Ents b)
traverseEnts f = fmap Ents . Tr.traverse f' . unEnts where
  f' en@(Ent e _ i) = Ent <$> pure e <*> f en <*> pure i

seqEnt :: Applicative f => Ent (f a) -> f (Ent a)
seqEnt (Ent e m i) = Ent <$> pure e <*> m <*> pure i

-- | Every Ents alwas contains at least two ents, and possibly
-- additional ones.
tupleEnts :: Ents m -> (Ent m, Ent m, [Ent m])
tupleEnts (Ents ls) = case ls of
  t1:t2:ts -> (t1, t2, ts)
  _ -> error "tupleEnts: ents does not have two ents"

-- | In a Posting, the Ent at the front of the list of Ents is the
-- main posting. There are additional postings. This function
-- rearranges the Ents multiple times so that each posting is at the
-- head of the list exactly once.
views :: Ents m -> [Ents m]
views = map Ents . orderedPermute . unEnts

-- | > unrollSnd (undefined, []) == []
--   > unrollSnd (1, [1,2,3]) = [(1,1), (1,2), (1,3)]

unrollSnd :: (a, [b]) -> [(a, b)]
unrollSnd = unfoldr f where
  f (_, []) = Nothing
  f (a, b:bs) = Just ((a, b), (a, bs))

-- | Splits a Transaction into Postings.
transactionToPostings :: Transaction -> [Posting]
transactionToPostings =
  map Posting . unrollSnd . second views . unTransaction

-- | Get information from the head posting in the View, which is the
-- one you are most likely interested in. This never fails, as every
-- Ents has at least two postings.
headEnt :: Ents m -> Ent m
headEnt (Ents ls) = case ls of
  [] -> error "ents: empty view"
  x:_ -> x

-- | Get information on sibling Ents.
tailEnts :: Ents m -> (Ent m, [Ent m])
tailEnts (Ents ls) = case ls of
  [] -> error "ents: tailEnts: empty view"
  _:xs -> case xs of
    [] -> error "ents: tailEnts: only one sibling"
    s2:ss -> (s2, ss)

-- | A Transaction and a Posting are identical on the inside, but they
-- have different semantic meanings so they are wrapped in newtypes.
newtype Transaction = Transaction
  { unTransaction :: ( B.TopLineData, Ents B.PostingData ) }
  deriving (Eq, Show)

-- | In a Posting, the Ent yielded by 'headEnt' will be the posting of
-- interest. The other sibling postings are also available for
-- inspection.
newtype Posting = Posting
  { unPosting :: ( B.TopLineData, Ents B.PostingData ) }
  deriving (Eq, Show)

-- | Returns a list of lists where each element in the original list
-- is in the front of a new list once.
--
-- > orderedPermute [1,2,3] == [[1,2,3], [2,3,1], [3,1,2]]
orderedPermute :: [a] -> [[a]]
orderedPermute ls = take (length ls) (iterate toTheBack ls)
  where
    toTheBack [] = []
    toTheBack (a:as) = as ++ [a]

-- | Creates an 'Ents'. At most, one of the Maybe Entry can be Nothing
-- and this function will infer the remaining Entry. This function
-- fails if it cannot create a balanced Ents.
ents
  :: [(Maybe (Either (B.Entry B.QtyRep) (B.Entry B.Qty)), m)]
  -> Maybe (Ents m)
ents ls = do
  guard . not . null $ ls
  let nNoEntries = length . filter (== Nothing) . map fst $ ls
  case Bal.entriesToBalanced
       . map (either (fmap B.toQty) id)
       . catMaybes
       . map fst
       $ ls of
    Bal.NotInferable -> Nothing
    Bal.Inferable e -> do
      guard $ nNoEntries == 1
      let makeEnt (mayEn, mt) = case mayEn of
            Nothing -> Ent (Right e) mt True
            Just en -> Ent en mt False
      return . Ents $ map makeEnt ls
    Bal.Balanced ->
      let makeEnt (mayEn, mt) = case mayEn of
            Nothing -> Nothing
            Just en -> Just $ Ent en mt False
      in fmap Ents $ mapM makeEnt ls


-- | Creates 'Ents'. Unlike 'ents' this function never fails because
-- you are restricted in the inputs that you can give it. It will
-- always infer the last Entry. All Entries except one will have the
-- same DrCr; the last, inferred one will have the opposite DrCr.
rEnts
  :: B.Commodity
  -- ^ Commodity for all postings
  -> B.DrCr
  -- ^ DrCr for all non-inferred postings
  -> (Either B.QtyRep B.Qty, m)
  -- ^ Non-inferred posting 1
  -> [(Either B.QtyRep B.Qty, m)]
  -- ^ Remaining non-inferred postings
  -> m
  -- ^ Metadata for inferred posting
  -> Ents m
rEnts com dc (q1, m1) nonInfs lastMeta =
  let tot = foldl' B.add (either B.toQty id q1)
            . map (either B.toQty id . fst) $ nonInfs
      p1 = makePstg (q1, m1)
      ps = map makePstg nonInfs
      makeEntry = either (\q -> Left (B.Entry dc (B.Amount q com)))
                         (\q -> Right (B.Entry dc (B.Amount q com)))
      makePstg (q, m) = Ent (makeEntry q) m False
      lastPstg = Ent (Right (B.Entry (B.opposite dc)
                                     (B.Amount tot com))) lastMeta True
  in Ents $ p1:ps ++ [lastPstg]