{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# OPTIONS_GHC -fno-warn-deprecations #-} -- FIXME: to be removed when dropping GHC-7.6 support
module Hcompta.Balance where

-- import           Control.Applicative (Const(..))
import           Control.Exception (assert)
import           Data.Data
import qualified Data.Foldable
-- import           Data.Foldable (Foldable(..))
import qualified Data.Map.Strict as Data.Map
import           Data.Map.Strict (Map)
import qualified Data.Strict.Maybe as Strict
import           Data.Typeable ()

-- import           Hcompta.Lib.Consable (Consable(..))
import qualified Hcompta.Lib.Foldable as Lib.Foldable
import qualified Hcompta.Lib.TreeMap as Lib.TreeMap
import           Hcompta.Lib.TreeMap (TreeMap)
import qualified Hcompta.Account as Account
import           Hcompta.Account (Account)

-- * Requirements' interface

-- ** Class 'Amount'
class
 ( Data     (Amount_Unit a)
 , Ord      (Amount_Unit a)
 , Show     (Amount_Unit a)
 , Typeable (Amount_Unit a)
 ) => Amount a where
	type Amount_Unit a
	amount_null   :: a -> Bool
	amount_add    :: a -> a -> a
	amount_negate :: a -> a

-- ** Class 'Posting'

-- | A 'posting' used to produce a 'Balance'
--   must be an instance of this class.
class Amount (Posting_Amount p) => Posting p where
	type Posting_Amount p
	posting_account     :: p -> Account
	posting_amounts     :: p -> Map (Amount_Unit (Posting_Amount p)) (Posting_Amount p)
	posting_set_amounts ::      Map (Amount_Unit (Posting_Amount p)) (Posting_Amount p) -> p -> p

{- NOTE: not needed so far.
instance (Amount amount, unit ~ Amount_Unit amount)
 => Posting (Account, Map unit amount)
 where
	type Posting_Amount (Account, Map unit amount) = amount
	posting_account = fst
	posting_amounts = snd
	posting_set_amounts amounts (acct, _) = (acct, amounts)
-}

instance (Amount amount)
 => Posting (Account, Account_Sum amount)
 where
	type Posting_Amount (Account, Account_Sum amount) = amount
	posting_account = fst
	posting_amounts (_, Account_Sum x) = x
	posting_set_amounts amounts (acct, _) = (acct, Account_Sum amounts)

-- * Type 'Balance'

-- | Sum by 'Account' and sum by 'unit' of some 'Posting's.
--
-- NOTE: to reduce memory consumption when 'cons'ing iteratively,
--       the fields are explicitely stricts.
data Amount  amount
 =>  Balance amount
 =   Balance
 { balance_by_account :: !(Balance_by_Account amount)
 , balance_by_unit    :: !(Balance_by_Unit    amount)
 }
deriving instance ( Amount amount
                  , Data   amount
                  ) => Data (Balance amount)
deriving instance ( Amount amount
                  , Eq     amount
                  ) => Eq (Balance amount)
deriving instance ( Amount amount
                  , Show   amount
                  ) => Show (Balance amount)
deriving instance Typeable1 Balance
 -- FIXME: use 'Typeable' when dropping GHC-7.6 support

instance Amount amount => Monoid (Balance amount) where
	mempty  = empty
	mappend = union

-- ** Type 'Balance_by_Account'
type Balance_by_Account amount
 = TreeMap Account.Name
           (Account_Sum amount)

-- *** Type 'Account_Sum'
-- | A sum of 'amount's,
-- concerning a single 'Account'.
newtype Amount amount
 => Account_Sum amount
 =  Account_Sum (Map (Amount_Unit amount) amount)
get_Account_Sum :: Amount amount => Account_Sum amount -> Map (Amount_Unit amount) amount
get_Account_Sum (Account_Sum m) = m
deriving instance ( Amount amount
                  , Data   amount
                  ) => Data (Account_Sum amount)
deriving instance ( Amount amount
                  , Eq     amount
                  ) => Eq (Account_Sum amount)
deriving instance ( Amount amount
                  , Show   amount
                  ) => Show (Account_Sum amount)
deriving instance Typeable1 Account_Sum
 -- FIXME: use 'Typeable' when dropping GHC-7.6 support

instance Amount amount
 => Monoid (Account_Sum amount) where
	mempty = Account_Sum mempty
	mappend
	 (Account_Sum a0)
	 (Account_Sum a1) =
		Account_Sum $ Data.Map.unionWith amount_add a0 a1

-- ** Type 'Balance_by_Unit'
newtype Amount      amount
 => Balance_by_Unit amount
 =  Balance_by_Unit (Map (Amount_Unit amount) (Unit_Sum amount))
deriving instance ( Amount amount
                  , Data   amount
                  ) => Data (Balance_by_Unit amount)
deriving instance ( Amount amount
                  , Eq     amount
                  ) => Eq (Balance_by_Unit amount)
deriving instance ( Amount amount
                  , Show   amount
                  ) => Show (Balance_by_Unit amount)
deriving instance Typeable1 Balance_by_Unit
 -- FIXME: use 'Typeable' when dropping GHC-7.6 support

instance Amount amount
 => Monoid (Balance_by_Unit amount) where
	mempty = Balance_by_Unit mempty
	mappend = union_by_unit

-- *** Type 'Unit_Sum'

-- | A sum of 'amount's with their 'Account's involved,
-- concerning a single 'unit'.
data Unit_Sum amount
 =   Unit_Sum
 { unit_sum_amount   :: !amount -- ^ The sum of 'amount's for a single 'unit'.
 , unit_sum_accounts :: !(Map Account ()) -- ^ The 'Account's involved to build 'unit_sum_amount'.
 } deriving (Data, Eq, Show, Typeable)

-- ** Constructors

empty :: Amount amount => Balance amount
empty =
	Balance
	 { balance_by_account = mempty
	 , balance_by_unit    = mempty
	 }

-- | Return the given 'Balance'
--   updated by the given 'Posting'.
cons ::
 ( Posting posting
 , balance ~ Balance (Posting_Amount posting) )
 => posting -> balance -> balance
cons post bal =
	bal
	 { balance_by_account = cons_by_account post (balance_by_account bal)
	 , balance_by_unit    = cons_by_unit    post (balance_by_unit    bal)
	 }

-- | Return the given 'Balance'
--   updated by the given 'Posting's.
postings ::
 ( Posting posting
 , balance ~ Balance (Posting_Amount posting)
 , Foldable foldable )
 => foldable posting -> balance -> balance
postings = flip (Data.Foldable.foldr cons)

-- | Return the first given 'Balance'
--   updated by the second given 'Balance'.
union :: Amount amount
 => Balance amount -> Balance amount -> Balance amount
union
 (Balance b0a b0u)
 (Balance b1a b1u) =
	Balance
	 { balance_by_account = union_by_account b0a b1a
	 , balance_by_unit    = union_by_unit    b0u b1u
	 }

-- | Return the given 'Balance_by_Account'
--   updated by the given 'Posting'.
cons_by_account ::
 ( Posting posting
 , amount ~ Posting_Amount posting
 , unit   ~ Amount_Unit amount
 )
 => posting
 -> Balance_by_Account amount
 -> Balance_by_Account amount
cons_by_account post =
	Lib.TreeMap.insert mappend
	 (posting_account post)
	 (Account_Sum $ posting_amounts post)

-- | Return the given 'Balance_by_Unit'
--   updated by the given 'Posting'.
cons_by_unit ::
 ( Posting posting
 , amount ~ Posting_Amount posting
 , unit   ~ Amount_Unit amount )
 => posting
 -> Balance_by_Unit amount
 -> Balance_by_Unit amount
cons_by_unit post =
	union_by_unit $
	Balance_by_Unit $
	Data.Map.map
	 (\amount -> Unit_Sum
		 { unit_sum_amount   = amount
		 , unit_sum_accounts = Data.Map.singleton (posting_account post) ()
		 })
	 (posting_amounts post)

-- | Return a 'Balance_by_Unit'
--   derived from the given 'Balance_by_Account'.
by_unit_of_by_account ::
 ( Amount amount
 , unit ~ Amount_Unit amount
 )
 => Balance_by_Account amount
 -> Balance_by_Unit    amount
 -> Balance_by_Unit    amount
by_unit_of_by_account =
	flip $ Lib.TreeMap.foldr_with_Path $ curry cons_by_unit

-- | Return the first given 'Balance_by_Account'
--   updated by the second given 'Balance_by_Account'.
union_by_account :: Amount amount
 => Balance_by_Account amount
 -> Balance_by_Account amount
 -> Balance_by_Account amount
union_by_account = Lib.TreeMap.union mappend

-- | Return the first given 'Balance_by_Unit'
--   updated by the second given 'Balance_by_Unit'.
union_by_unit :: (Amount amount, unit ~ Amount_Unit amount)
 => Balance_by_Unit amount
 -> Balance_by_Unit amount
 -> Balance_by_Unit amount
union_by_unit
 (Balance_by_Unit a0)
 (Balance_by_Unit a1) =
	Balance_by_Unit $
	Data.Map.unionWith
	 (\new old -> Unit_Sum
		 { unit_sum_amount = amount_add
			 (unit_sum_amount old)
			 (unit_sum_amount new)
		 , unit_sum_accounts = Data.Map.unionWith
			 (const::()->()->())
			 (unit_sum_accounts old)
			 (unit_sum_accounts new)
		 })
	 a0 a1

-- * Type 'Deviation'

-- | The 'Balance_by_Unit' whose 'unit_sum_amount'
--   is not zero and possible 'Account' to 'infer_equilibrium'.
newtype Amount amount
 => Deviation amount
 =  Deviation (Balance_by_Unit amount)
deriving instance ( Amount amount
                  , Data   amount
                  ) => Data (Deviation amount)
deriving instance ( Amount amount
                  , Eq     amount
                  ) => Eq (Deviation amount)
deriving instance ( Amount amount
                  , Show   amount
                  ) => Show (Deviation amount)
deriving instance Typeable1 Deviation
 -- FIXME: use 'Typeable' when dropping GHC-7.6 support

-- | Return the 'balance_by_unit' of the given 'Balance' with:
--
-- * 'unit's whose 'unit_sum_amount' verifying 'amount_is_zero' removed,
--
-- * and remaining 'unit's having their 'unit_sum_accounts'
-- complemented with the 'balance_by_account' of the given 'Balance'
-- (i.e. now mapping to 'Account's __not__ involved to build the 'Unit_Sum').
deviation
 :: Amount amount
 => Balance amount
 -> Deviation amount
deviation Balance
 { balance_by_account=ba
 , balance_by_unit=Balance_by_Unit bu
 } = do
	let all_accounts = Lib.TreeMap.flatten (const ()) ba
	let max_accounts = Data.Map.size all_accounts
	Deviation $
		Data.Map.foldlWithKey
		 (\(Balance_by_Unit m) unit Unit_Sum{unit_sum_amount, unit_sum_accounts} ->
			Balance_by_Unit $
			if amount_null unit_sum_amount
			then m
			else
				case Data.Map.size unit_sum_accounts of
				 n | n == max_accounts ->
					Data.Map.insert unit Unit_Sum
					 { unit_sum_amount
					 , unit_sum_accounts = Data.Map.empty
					 } m
				 _ -> do
					let diff = Data.Map.difference all_accounts unit_sum_accounts
					Data.Map.insert unit Unit_Sum
					 { unit_sum_amount
					 , unit_sum_accounts = diff
					 } m
		 )
		 mempty
		 bu

-- ** The equilibrium

-- | Return the 'Balance' (adjusted by inferred 'Amount's)
--   of the given 'Posting's and either:
--
--   * 'Left': the 'Posting's that cannot be inferred.
--   * 'Right': the given 'Posting's with inferred 'Amount's inserted.
infer_equilibrium ::
 ( Posting posting )
 => Map Account [posting]
 -> ( Balance (Posting_Amount posting)
    , Either [Unit_Sum (Posting_Amount posting)] (Map Account [posting])
    )
infer_equilibrium posts = do
	let bal_initial = Data.Foldable.foldr postings empty posts
	let Deviation (Balance_by_Unit dev) = deviation bal_initial
	let (bal_adjusted, eithers) =
		Data.Map.foldrWithKey
		 (\unit unit_sum@(Unit_Sum{unit_sum_amount, unit_sum_accounts})
		 (bal, lr) ->
			case Data.Map.size unit_sum_accounts of
			 1 ->
				let acct = fst $ Data.Map.elemAt 0 unit_sum_accounts in
				let amt = amount_negate unit_sum_amount in
				let amts = Data.Map.singleton unit amt in
				( cons (acct, Account_Sum amts) bal
				, Right (acct, unit, amt) : lr
				)
			 _ -> (bal, Left [unit_sum] : lr))
		 (bal_initial, [])
		 dev
	let (l, r) = Lib.Foldable.accumLeftsAndFoldrRights
		 (\(acct, unit, amt) ->
			Data.Map.insertWith
			 (\_new_ps -> insert_amount (unit, amt))
			 acct (assert False []))
		 posts eithers
	case l of
	 [] -> (bal_adjusted, Right r)
	 _  -> (bal_adjusted, Left  l)
	where
		insert_amount
		 :: Posting posting
		 => (Amount_Unit (Posting_Amount posting), Posting_Amount posting)
		 -> [posting] -> [posting]
		insert_amount p@(unit, amt) ps =
			case ps of
			 [] -> assert False []
			 (x:xs) | Data.Map.null (posting_amounts x) ->
				posting_set_amounts (Data.Map.singleton unit amt) x:xs
			        | Data.Map.notMember unit (posting_amounts x) ->
				let amts = Data.Map.insertWith
					 (assert False undefined)
					 unit amt (posting_amounts x) in
				posting_set_amounts amts x:xs
			 (x:xs) -> x:insert_amount p xs

-- | Return 'True' if and only if the given 'Deviation' maps no 'unit'.
is_at_equilibrium :: Amount amount => Deviation amount -> Bool
is_at_equilibrium (Deviation (Balance_by_Unit dev)) = Data.Map.null dev

-- | Return 'True' if and only if the given 'Deviation'
-- maps only to 'Unit_Sum's whose 'unit_sum_accounts'
-- maps exactly one 'Account'.
is_equilibrium_inferrable :: Amount amount => Deviation amount -> Bool
is_equilibrium_inferrable (Deviation (Balance_by_Unit dev)) =
	Data.Foldable.all
	 (\Unit_Sum{unit_sum_accounts} -> Data.Map.size unit_sum_accounts == 1)
	 dev

-- | Return 'True' if and only if the given 'Deviation'
-- maps to at least one 'Unit_Sum' whose 'unit_sum_accounts'
-- maps more than one 'Account'.
is_equilibrium_non_inferrable :: Amount amount => Deviation amount -> Bool
is_equilibrium_non_inferrable (Deviation (Balance_by_Unit dev)) =
	Data.Foldable.any
	 (\Unit_Sum{unit_sum_accounts} -> Data.Map.size unit_sum_accounts > 1)
	 dev

-- * Type 'Expanded'

-- | Descending propagation of 'Amount's accross 'Account's.
type Expanded amount
 = TreeMap Account.Name (Account_Sum_Expanded amount)
data Amount amount => Account_Sum_Expanded amount
 =   Account_Sum_Expanded
 { exclusive :: !(Account_Sum amount)
 , inclusive :: !(Account_Sum amount) -- ^ 'amount_add' folded over 'exclusive' and 'inclusive' of 'Lib.TreeMap.node_descendants'
 }
deriving instance ( Amount amount
                  , Data   amount
                  ) => Data (Account_Sum_Expanded amount)
deriving instance ( Amount amount
                  , Eq     amount
                  ) => Eq (Account_Sum_Expanded amount)
deriving instance ( Amount amount
                  , Show   amount
                  ) => Show (Account_Sum_Expanded amount)
deriving instance Typeable1 Account_Sum_Expanded
 -- FIXME: use 'Typeable' when dropping GHC-7.6 support

instance Amount amount => Monoid (Account_Sum_Expanded amount) where
	mempty = Account_Sum_Expanded mempty mempty
	mappend
	 (Account_Sum_Expanded e0 i0)
	 (Account_Sum_Expanded e1 i1) =
		Account_Sum_Expanded
		 (mappend e0 e1)
		 (mappend i0 i1)

-- | Return the given 'Balance_by_Account' with:
--
-- * all missing 'Account.ascending' 'Account's inserted,
--
-- * and every mapped 'Amount'
-- added with any 'Amount'
-- of the 'Account's for which it is 'Account.ascending'.
expanded
 :: Amount amount
 => Balance_by_Account amount
 -> Expanded amount
expanded =
	Lib.TreeMap.map_by_depth_first
	 (\descendants value ->
		let exclusive = Strict.fromMaybe mempty value in
		Account_Sum_Expanded
		 { exclusive
		 , inclusive =
			Data.Map.foldl'
			 ( flip $ mappend . inclusive
			 . Strict.fromMaybe (assert False undefined)
			 . Lib.TreeMap.node_value)
			 exclusive $
			Lib.TreeMap.nodes descendants
		 })

-- | Return a 'Balance_by_Unit'
--   derived from the given 'Expanded' balance.
--
--   NOTE: also correct if the 'Expanded' has been filtered.
by_unit_of_expanded ::
 ( Amount amount
 , unit ~ Amount_Unit amount
 )
 => Expanded amount
 -> Balance_by_Unit amount
 -> Balance_by_Unit amount
by_unit_of_expanded =
	go []
	where
		go p (Lib.TreeMap.TreeMap m) bal =
			Data.Map.foldrWithKey
			 (\k Lib.TreeMap.Node{Lib.TreeMap.node_value, Lib.TreeMap.node_descendants} acc ->
				case node_value of
				 Strict.Nothing -> go (k:p) node_descendants acc
				 Strict.Just a  ->
					let account = Lib.TreeMap.reverse $ Lib.TreeMap.path k p in
					cons_by_unit (account, inclusive a) acc)
			 bal m