{-# LANGUAGE
 ExistentialQuantification
, NoMonomorphismRestriction
 #-}
-- | This module provides 
--
-- * the types to build a 'World' of accounting 'Entity's and
--
-- * functions to compute the ledger resulting from the entities'
-- accounting actions
--
-- * built from combinators found in "Bookkeeping" and "Assets".
--
-- Use "Report" to display the results.
module HAX.Accounting where

import HAX.Bookkeeping.Internal
import HAX.Bookkeeping
import Control.Monad.RWS.Strict
import HAX.Common
import qualified Data.Map as M

  
-- * Accounting Entities

data World = World { wLife :: (ADate,ADate) -- ^ time interval
                   , wEntites :: [Entity]
                   }


  
             
-- | An entity keeping accounts over its assets.
data Entity = forall body . Body body => Entity  {
  entName :: EntityName
  -- ^ the name of the entity. This will serve as an 'AccountName'
  -- when booking transaction between this entity an another entity.
  , entAssets :: [AssetName]
  -- ^ the entity's asset accounts
  , entMonthlyAction ::  AccountingRW body ()
  -- the entity's monthly accounting action
  , entBody :: body
  -- ^ the entity's 'Body', defining its nominal acounts.
  }

-- | The body of an entity. 
class Body body where
  -- | The only requirement is that each Body has a list of nominal
  -- accounts associated with it. See "Germany" for instances.
  nominalAccounts :: body -> [String]
  bodyMonthly :: AccountingRW body () -- ^ custom actin of the entity
                 -> AccountingRW body ()  -- ^ monthly action

-- * Running actions and generating ledgers

-- | Executes an accounting action in a simple environment (for
-- testing purposes) with no accounts.
--
-- >>> simple (date 12 2016) NatuerlichePerson{pGeburt=date 12 1960} alter
-- 56

simple :: ADate -> body -> AccountingRW body a -> IO a
simple date body action = do
  bals <- newArray ((date,1),(date,1)) 0
  txns <- newArray (date,date) []
  let ledger = UNSAFE_Ledger bals txns
  fst <$> evalRWST action (Env ledger M.empty date body Nothing) ()
  

-- | Generate the ledger of the 'World'. It will include all nominal
-- and asset accounts of all 'n' entities, as well as 'n*(n-1)'
-- transactional accounts between each pair of entities.
--
-- Furthermore, it will check the balances of these transactional
-- accounts at the end of every month using
-- 'checkTransactionalAcountSymmetry'.
generate ::  World -> IO FullLedger
generate (World times entities) = do
  let entityNames = entName <$> entities
      eAccounts (Entity name assets _ body) =
        assets ++ nominalAccounts body
        ++ (filter (name /=) entityNames)
      allAccounts = M.fromList $  zip accountList [1..]
      accountList = [ FAN (entName e) a | e <- entities, a <- eAccounts e ]
      nAccs = M.size allAccounts
  when (length accountList /= M.size allAccounts) $
    error $ "AccountNames are not unique: "++
    (unlines$show<$>accountList)
  printf "Created a ledger with %v accounts\n" nAccs
  bals <- newArray ((fst times,1),(snd times,nAccs)) 0
  txns <- newArray times []
  let ledger = UNSAFE_Ledger bals txns
      monthAction = do
        uNSAFE_carryOver
        checkTransactionalAcountSymmetry entityNames
        sequence_ [withRWT (\e -> e{eBody=body, eName=Just name})
                   $ bodyMonthly action
                  | (Entity name _ action body) <- entities ]
  result <- sequence [ runRWST monthAction
                         (Env ledger allAccounts date () Nothing) ()
                      | date <- range times  ]
  -- this is safe, as the ioarrays do not leave this function
  ledger' <- lunsafeFix ledger
  return $ FullLedger ledger' allAccounts

withRWT :: (r' -> r) -> RWST r w () m a -> RWST r' w () m a
withRWT f = withRWST $ \r s -> (f r,())
evalRWT :: Monad m => RWST r () () m a -> r -> m a
evalRWT action env = liftM fst $ evalRWST action env ()

-- * Helpers

-- | For each pair of entities, 'entity1' and 'entity2', there are two
-- transactional accounts, as each entity keeps its own books about
-- its transactions with the other entity. If no entity made a
-- mistake, the accounts should have opposite balances that cancel
-- each other. This function informs about a violation of this
-- property.
checkTransactionalAcountSymmetry :: (Monoid w, Ledger l) => [EntityName] -> Acc s l w ()
checkTransactionalAcountSymmetry entities = do
  let pairs = [  FAN e1 e2 | e1:rest <- tails entities,  e2 <- rest ]
      cb = currentBalance . UNSAFE_AccountN
  date <- reader eDate
  forM_ pairs $ \p -> do
    v1 <- cb p
    v2 <- negate <$> cb (swapFAN p)
    when (v1 /= v2) $ lift $ printf
      "symmtery violation at %v: %s %v /= %s %v\n"
      date (show p) v1 (show $ swapFAN p) v2
  

-- | constructs an action reading an information from the current body
readBody :: Monoid w => (s -> a) -> Acc s l w a
readBody f = reader $ f . eBody

-- | construct an action that depends on the body
withBody :: Monoid w => (s -> Acc s l w a) -> Acc s l w a
withBody = (reader eBody >>=)