{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeSynonymInstances
, FlexibleInstances
, TypeFamilies
, ConstraintKinds
, ExistentialQuantification
, MultiParamTypeClasses
, UndecidableInstances
, NoMonomorphismRestriction
, DeriveGeneric
 #-}
{-# OPTIONS_HADDOCK not-home #-}


-- | This module contains the internal type and functions not to be
-- used directly as most of them are unsafe, meaning that they allow
-- actions that violate double-entry contraints or actions
-- on accounts other than the current body's accounts (via
-- 'UNSAFE_AccountN').
--
-- Therefore, __do not use this module directly__, use "HAX.Bookkeeping" instead.
module HAX.Bookkeeping.Internal where

import           HAX.Common
import           Control.Monad.RWS.Strict
import           Data.Array
import           Data.Array.Unsafe
import           Data.Functor.Compose
import qualified Data.Map as M
import           GHC.Generics


-- * Account Names and Numbers

type AccountNumber = Int
type AssetName = String
type EntityName = String

-- | uniquely identifying name used to lookup the account numbers
data FullAccountName = FAN
                       { fEntity :: EntityName
                       , fAccount :: String
                       }
                 deriving (Eq, Ord,Show, Generic)

swapFAN :: FullAccountName -> FullAccountName
swapFAN (FAN a b) = FAN b a

-- | this type is used to make functions taking 'AccountNames' polymorphic. 
data AccountName = AccountN String
                   -- ^ account for the current entity. Only these
                   -- accounts should be accessible in accounting
                   -- actions
                 | UNSAFE_AccountN FullAccountName
                   -- ^ full account for internal use only
                 deriving (Eq, Ord,Show)

instance PrintfArg AccountName where
  formatArg (AccountN s) = formatString s
  formatArg (UNSAFE_AccountN _) = error "not implemented: formatArg (UNSAFE_AccountN)"
             
                          
-- | 'String's are automatically converted to (safe) 'AccoutN'ames, if
-- the -XOverloadedStrings extensions is active. (See "Data.String").
instance IsString AccountName where
  fromString = AccountN 

  
-- | The map from 'FullAccountName's to 'AccountNumber's used
-- internally to address the efficient 'Ledger' array storage
type AccountsMap = M.Map FullAccountName AccountNumber

-- | Extract accounts names order by their internal account numbers
sortedAccountNames :: AccountsMap -> [FullAccountName]
sortedAccountNames = fmap fst . sortBy (comparing snd) . M.toList


-- | extract the 'AccountNumber' for a 'FullAccountName' from an 'AccountsMap'
internalAccountNumber :: FullAccountName -> AccountsMap -> AccountNumber
internalAccountNumber name accs = fromMaybe (error $
                                             printf "Account '%s' not found\nAvailable Accounts:\n%s"
                                             (show name) $ unlines $ show <$> M.toList accs)
                           $ M.lookup name accs
  

-- * Postings and Transactions

type Posting = (AccountName,Amount)
type InternalPosting = (AccountNumber,Amount)


-- | A transaction that is already balanced. Such an object can only
-- be built from 'BalancingTx' using 'balanceTx' and is never needed
-- as function input. This format is used to log the transactions in
-- the 'Ledger' 's 'LogEntry'.
data Tx = UNSAFE_Tx {  tComment  :: Comment
                    , tPostings :: [InternalPosting]
                    }
        deriving (Show,Generic)

-- | A transaction involving only accounts relative to a body, and
-- that is self balancing through the use of an account for the
-- remains
data BalancingTx = BalancingTx { txComment :: Comment
                               , txRemains :: AccountName
                               , txPostings :: [Posting]
                               }

-- | Balance a 'BalancingTx' and prepend the entitiyName to the
-- comment. This is only used internally
balanceTx :: (Monoid w) => BalancingTx -> Acc s l w Tx
balanceTx (BalancingTx comment remains postings) = do
  name <- nameErr "Transactions are only allowed in the presence of an entity"
  fmap (UNSAFE_Tx $ printf "(%s) %s" name comment) $ 
    -- convert to account numbers: 
    mapM (\(name,amount) -> do number <- accountNumber name
                               return (number,amount) )
    -- add a balancing posting:
    $ (remains,negate $ sum $ snd <$> postings):postings 
    
-- * The Ledger

-- | Information that is logged while the ledger is built
data LogEntry = LTx Tx -- ^ Transactions of the current time period
              | LComment String -- ^ Random comment to be put into the ledger
                deriving (Generic,Show)

type EntityLogEntry = (EntityName,LogEntry)

type LedgerIndex = (ADate,AccountNumber)
type LedgerBounds = (LedgerIndex, LedgerIndex)

-- | This class defines what a 'Ledger' is:
class Ledger l where
  -- | it has bounds
  lBounds :: l -> IO LedgerBounds
  -- | single entries for a given account and date
  -- consisting of the account balance and the log
  -- entries for that date can be read.
  lReadEntry :: LedgerIndex -> l -> IO (Amount,[EntityLogEntry])
  -- | the account history can be read
  lAccountHistory :: (ADate,ADate) -> (ADate -> LedgerIndex) -> l -> IO [Amount]
  -- | it can be fixed into an immutable type
  lFix :: l -> IO FixedLedger
  -- | fix withou making a copy. This is has to be safe to use, if the
  -- mutable version is never modified after the freeze operation.
  lunsafeFix :: l -> IO FixedLedger

  
-- | 'LedgerRW' implements a writable (within the IO monad) 'Ledger'.
-- 
-- The total balance is always zero and no transactions that depend on
-- future values are allowed.  This is guaranteed, by not exporting
-- UNSAFE_Ledger and instead, the ledger is only changed using the
-- exported safe functions. E.g. `tx`, 'fromTo', ...
data LedgerRW = UNSAFE_Ledger
  { lUNSAFE_Bals :: IOArray LedgerIndex Amount
  -- ^ Balances for each Date and Account
  , lUNSAFE_LogEntries :: IOArray ADate [EntityLogEntry]
  -- ^ Transactions and Comments for each Date
  }
                
-- | This type implements the Ledger in immutable form, suitable as
-- the main result of the whole program or for accounting accounts
-- that are garantueed to not change the ledger.
data FixedLedger =  FixedLedger
  { lBals :: Array LedgerIndex Amount
  , lLogEntries :: Array ADate [EntityLogEntry]
  }

data FullLedger = FullLedger
  { flLedger :: FixedLedger
  , flAccounts :: AccountsMap
  }

  -- lEase :: Acc s l a -> Accounting s a -- run any kind of action in the full read-write monad
  -- this is never needed, as there is reason to restrict a type to the
  -- FixedLedger. Only the other way round: if an action changes
  -- something, it will have the LedgerRW type and cannot be `fixed`/

instance Ledger LedgerRW where
  lBounds (UNSAFE_Ledger ledger _) = getBounds ledger
  lReadEntry (date,acc) (UNSAFE_Ledger bals txns) =
    liftM2 (,) (readArray bals (date,acc)) $ readArray txns date
  lAccountHistory times f (UNSAFE_Ledger bals _) =
    getElems  =<< mapIndices times f bals
  lFix (UNSAFE_Ledger bals txns) = liftM2 FixedLedger (freeze bals) $ freeze txns
  lunsafeFix (UNSAFE_Ledger bals txns) = liftM2 FixedLedger (unsafeFreeze bals) $ unsafeFreeze txns

  
instance Ledger FixedLedger where
  lBounds (FixedLedger ledger _) = return $ bounds ledger
  lReadEntry (date,acc) (FixedLedger bals txns) =
    return (bals ! (date,acc), txns ! date)
  lAccountHistory times f (FixedLedger bals _) =
    return $ elems $ ixmap times f bals
  lFix = return
  lunsafeFix = return

-- * Accounting Environment

-- | Represents the environment an accounting action is run on.
data Environment body ledger = Env {
  eLedger :: ledger  -- ^ the ledger of the whole world (i.e. all bodys
  , eAccounts :: AccountsMap -- ^ map of all accounts managed by the ledger
  , eDate :: ADate -- ^ current date
  , eBody :: body  -- ^ current body 
  , eName :: Maybe EntityName -- ^ current body's name
  }

                              
-- * Accounting Actions

-- | The Accounting Monad
--
-- This monad is a stack of Reader Writer and IO monad.
--
-- Actions from this monad can read an immutable environment. This
-- environment however, contains references to mutable arrays (see
-- 'LedgerRW'), which can be modified through IO actions lifted into
-- this monad into this
-- monad.
--
-- The 'body' type variable will contain the type of the 'Accounting.Body' the
-- current accounting action is concerned with. 
type Acc body ledger writer = RWST (Environment body ledger) writer () IO
                    
-- | A specializations for read-write accounting actions with no
-- (i.e. trivial '()') writer output
type AccountingRW body = Acc body LedgerRW ()
-- | A specialization for read-only actions. These actions can however
-- produce read-write actions as output via the 'Writer' Monad. This
-- is used in 'fixed'.
type AccountingReadOnly body = Acc body FixedLedger (AccountingRW body ())

instance Monad m => Monoid (m ()) where
  mappend = (>>)
  mempty = return ()


-- | Short-cut class used in type signatures involving 'Acc' and its derivatives
class (Monoid w, Ledger l) => AccPair l w where

instance (Monoid w, Ledger l) => AccPair l w where
  

-- | type synonym for an accounting action that has an amount as result
type AmountRW body = AccountingRW body Amount
type AmountA body l w = Acc body l w Amount

-- | run a read only action and its genrated read-write output within
-- a general accounting action and pass on its result.
fixed :: AccountingReadOnly s a -> AccountingRW s a
fixed actionRO = do e <- ask
                    l' <- lift . lFix $ eLedger e
                    (res,actionRW) <- lift $ evalRWST actionRO e{eLedger = l'} ()
                    actionRW
                    return res

-- ################ Instances #######################
instance Monoid w => Eq (AmountA s l w) where
  (==) = error "Eq (AmountA s l) is impossible"
  
-- | Allows to use 'min' and 'max' and its derivaties directly on actions that
-- return an amount:
-- 
-- > min ( "Cash") (balanceAt date "Cash") :: AmountA s l
--
instance Monoid w => Ord (AmountA s l w) where
  min = liftM2 min
  max = liftM2 max
  (<=) = error "(<=) for (AmountA s l) is impossible"

-- | Allows to use '+','-','*','negate' directly on actions that
-- return an amount. Furthermore any numeral can be used directly as
-- (trivial) accounting action:
-- 
-- > soll "Cash" + 4 :: AmountA s l
--
instance Monoid w => Num (AmountA s l w) where
  (*) = liftM2 (*)
  (+) = liftM2 (+)
  (-) = liftM2 (-)
  negate = fmap negate
  abs = fmap abs
  signum = fmap signum
  fromInteger = return . fromInteger

instance Monoid w => Fractional (AmountA s l w) where
  (/) = liftM2 (/)
  recip = fmap recip
  fromRational = return . fromRational

-- * Internal Helper Functions


ledgerBounds :: (Monoid w, Ledger l) => Acc s l w (LedgerIndex, LedgerIndex)
ledgerBounds = lift . lBounds =<< reader eLedger

readEntryForNumber ::  (Monoid w, Ledger l) => AccountNumber -> Acc s l w (Amount,[EntityLogEntry])
readEntryForNumber acc = do date  <- reader eDate
                            lift . lReadEntry (date,acc) =<< reader eLedger


accountsNumbers :: (Monoid w, Ledger l) => Acc s l w [AccountNumber]
accountsNumbers = range2 <$> ledgerBounds


timeInterval :: (Monoid w, Ledger l) => Acc s l w (ADate,ADate)
timeInterval = both fst <$> ledgerBounds

accountNumber :: Monoid w => AccountName -> Acc s l w AccountNumber
accountNumber (UNSAFE_AccountN acc) = internalAccountNumber acc <$> reader eAccounts
accountNumber (AccountN acc) = do
  entName <- reader $ nameErr $ printf "'AccountN %s' is not defined" acc
  internalAccountNumber (FAN entName acc) <$> reader eAccounts

-- | Tries to get the 'eName' of the current entity and throws an
-- error if it is Nothing.
nameErr msg = reader (fromMaybe err . eName)
  where err = error $ printf
              "There is no current entity. "++ msg

readEntryForName :: (Monoid w, Ledger l) => AccountName -> Acc s l w (Amount,[EntityLogEntry])
readEntryForName acc = readEntryForNumber =<< accountNumber acc


-- * Internal UNSAFE Functions

-- | perform an accounting action at any date
uNSAFE_at :: Monoid w => ADate -> Acc s l w a -> Acc s l w a
uNSAFE_at date = local (\e -> e{eDate=date})

uNSAFE_addToBalance :: InternalPosting -> AccountingRW s ()
uNSAFE_addToBalance (acc,amount) = do
  UNSAFE_Ledger bals _ <- reader eLedger
  date  <- reader eDate
  lift $ updateArray bals (date,acc) (+amount)

-- | add last month's balances to previous month's. This is performed
-- once for every time step in generate
uNSAFE_carryOver :: AccountingRW s ()
uNSAFE_carryOver = do
  start <- fst <$> timeInterval
  oldD <- reader $ (shift $ - 1) . eDate
  when (start <= oldD) $ accountsNumbers >>=
    (mapM_  $ \ac -> do
        oldB <- fst <$> uNSAFE_at oldD (readEntryForNumber ac)
        uNSAFE_addToBalance (ac,oldB))