{-|

A 'Ledger' is derived from a 'Journal' by applying a filter specification
to select 'Transaction's and 'Posting's of interest. It contains the
filtered journal and knows the resulting chart of accounts, account
balances, and postings in each account.

-}

{-# LANGUAGE OverloadedStrings #-}

module Hledger.Data.Ledger (
   nullledger
  ,ledgerFromJournal
  ,ledgerAccountNames
  ,ledgerAccount
  ,ledgerRootAccount
  ,ledgerTopAccounts
  ,ledgerLeafAccounts
  ,ledgerPostings
  ,ledgerDateSpan
  ,ledgerCommodities
  ,tests_Ledger
)
where

import Data.Map qualified as M
import Safe (headDef)
import Text.Printf

import Test.Tasty (testGroup)
import Test.Tasty.HUnit ((@?=), testCase)
import Hledger.Data.Types
import Hledger.Data.Account
import Hledger.Data.Dates (nulldate)
import Hledger.Data.Journal
import Hledger.Query


instance Show Ledger where
    show l = printf "Ledger with %d transactions, %d accounts\n" --"%s"
             (length (jtxns $ ljournal l) +
              length (jtxnmodifiers $ ljournal l) +
              length (jperiodictxns $ ljournal l))
             (length $ ledgerAccountNames l)
             -- (showtree $ ledgerAccountNameTree l)

nullledger :: Ledger
nullledger = Ledger {
  ljournal = nulljournal,
  laccounts = []
  }

-- | Filter a journal's transactions with the given query, then build
-- a "Ledger", containing the journal plus the tree of all its
-- accounts with their subaccount-inclusive and subaccount-exclusive
-- balances. If the query includes a depth limit, the ledger's journal
-- will be depth limited, but the ledger's account tree will not.
ledgerFromJournal :: Query -> Journal -> Ledger
ledgerFromJournal q j = nullledger{ljournal=j'', laccounts=as}
  where
    (q',depthq)  = (filterQuery (not . queryIsDepth) q, filterQuery queryIsDepth q)
    j'  = filterJournalAmounts (filterQuery queryIsSym q) $ -- remove amount parts which the query's sym: terms would exclude
          filterJournalPostings q' j
    -- Ledger does not use date-separated balances, so dates are left empty
    as  = accountsFromPostings (const $ Just nulldate) $ journalPostings j'
    j'' = filterJournalPostings depthq j'

-- | List a ledger's account names.
ledgerAccountNames :: Ledger -> [AccountName]
ledgerAccountNames = drop 1 . map aname . laccounts

-- | Get the named account from a ledger.
ledgerAccount :: Ledger -> AccountName -> Maybe (Account BalanceData)
ledgerAccount l a = lookupAccount a $ laccounts l

-- | Get this ledger's root account, which is a dummy "root" account
-- above all others. This should always be first in the account list,
-- if somehow not this returns a null account.
ledgerRootAccount :: Ledger -> Account BalanceData
ledgerRootAccount = headDef nullacct . laccounts

-- | List a ledger's top-level accounts (the ones below the root), in tree order.
ledgerTopAccounts :: Ledger -> [Account BalanceData]
ledgerTopAccounts = asubs . headDef nullacct . laccounts

-- | List a ledger's bottom-level (subaccount-less) accounts, in tree order.
ledgerLeafAccounts :: Ledger -> [Account BalanceData]
ledgerLeafAccounts = filter (null.asubs) . laccounts

-- | List a ledger's postings, in the order parsed.
ledgerPostings :: Ledger -> [Posting]
ledgerPostings = journalPostings . ljournal

-- | The (fully specified) date span containing all the ledger's (filtered) transactions,
-- or DateSpan Nothing Nothing if there are none.
ledgerDateSpan :: Ledger -> DateSpan
ledgerDateSpan = journalDateSpanBothDates . ljournal

-- | All commodities used in this ledger.
ledgerCommodities :: Ledger -> [CommoditySymbol]
ledgerCommodities = M.keys . jinferredcommoditystyles . ljournal

-- tests

tests_Ledger =
  testGroup "Ledger" [
    testCase "ledgerFromJournal" $ do
        length (ledgerPostings $ ledgerFromJournal Any nulljournal) @?= 0
        length (ledgerPostings $ ledgerFromJournal Any samplejournal) @?= 13
        length (ledgerPostings $ ledgerFromJournal (Depth 2) samplejournal) @?= 7
  ]
