/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
*/
package org.apache.ofbiz.accounting.reports

import org.apache.ofbiz.accounting.util.UtilAccounting
import org.apache.ofbiz.base.util.UtilDateTime
import org.apache.ofbiz.base.util.UtilMisc
import org.apache.ofbiz.base.util.UtilProperties
import org.apache.ofbiz.entity.GenericValue
import org.apache.ofbiz.entity.condition.EntityCondition
import org.apache.ofbiz.entity.condition.EntityOperator
import org.apache.ofbiz.party.party.PartyWorker

import java.sql.Timestamp

if (!fromDate) {
    return
}
thruDate = thruDate ?: UtilDateTime.nowTimestamp()
parameters.glFiscalTypeId = parameters.glFiscalTypeId ?: 'ACTUAL'

uiLabelMap = UtilProperties.getResourceBundleMap('AccountingUiLabels', locale)
parametersFromDate = fromDate

// Setup the divisions for which the report is executed
List partyIds = PartyWorker.getAssociatedPartyIdsByRelationshipType(delegator,
        parameters.get('ApplicationDecorator|organizationPartyId'), 'GROUP_ROLLUP')
partyIds.add(parameters.get('ApplicationDecorator|organizationPartyId'))

// Get the group of account classes that will be used to position accounts in the proper section of the  Cash Flow statement
GenericValue glAccountClass = from('GlAccountClass').where('glAccountClassId', 'CASH_EQUIVALENT').cache(true).queryOne()
List glAccountClassIds = UtilAccounting.getDescendantGlAccountClassIds(glAccountClass)

List cashFlowBalanceTotalList = []

// Find the last closed time period to get the fromDate for the transactions in the current period and the ending balances of the last closed period
Map lastClosedTimePeriodResult = runService('findLastClosedDate',
        ['organizationPartyId': parameters.get('ApplicationDecorator|organizationPartyId'), 'findDate': parametersFromDate, 'userLogin': userLogin])
Timestamp periodClosingFromDate = (Timestamp)lastClosedTimePeriodResult.lastClosedDate
if (!periodClosingFromDate) {
    return
}
GenericValue lastClosedTimePeriod = (GenericValue)lastClosedTimePeriodResult.lastClosedTimePeriod
// Get the opening balances of Cash Account
Map openingCashBalances = [:]
if (lastClosedTimePeriod) {
    List timePeriodAndExprs = []
    timePeriodAndExprs.add(EntityCondition.makeCondition('organizationPartyId', EntityOperator.IN, partyIds))
    timePeriodAndExprs.add(EntityCondition.makeCondition('glAccountClassId', EntityOperator.IN, glAccountClassIds))
    timePeriodAndExprs.add(EntityCondition.makeCondition('endingBalance', EntityOperator.NOT_EQUAL, BigDecimal.ZERO))
    timePeriodAndExprs.add(EntityCondition.makeCondition('customTimePeriodId', EntityOperator.EQUALS, lastClosedTimePeriod.customTimePeriodId))
    List lastTimePeriodHistories = from('GlAccountAndHistory').where(timePeriodAndExprs).queryList()
    lastTimePeriodHistories.each { lastTimePeriodHistory ->
        Map accountMap = ['glAccountId': lastTimePeriodHistory.glAccountId, 'accountCode': lastTimePeriodHistory.accountCode,
                          'accountName': lastTimePeriodHistory.accountName, 'balance': lastTimePeriodHistory.getBigDecimal('endingBalance'),
                          'D': lastTimePeriodHistory.getBigDecimal('postedDebits'), 'C': lastTimePeriodHistory.getBigDecimal('postedCredits')]
        openingCashBalances.(lastTimePeriodHistory.glAccountId) = accountMap
    }
}
List mainAndExprs = []
mainAndExprs.add(EntityCondition.makeCondition('organizationPartyId', EntityOperator.IN, partyIds))
mainAndExprs.add(EntityCondition.makeCondition('isPosted', EntityOperator.EQUALS, 'Y'))
mainAndExprs.add(EntityCondition.makeCondition('glFiscalTypeId', EntityOperator.EQUALS, parameters.glFiscalTypeId))
mainAndExprs.add(EntityCondition.makeCondition('glAccountClassId', EntityOperator.IN, glAccountClassIds))

// All GlAccount's transactions (from last closing period to parameter's fromDate)
accountBalanceList = []
transactionTotals = []
balanceTotal = BigDecimal.ZERO
List openingCashBalanceAndExprs = mainAndExprs as LinkedList
openingCashBalanceAndExprs.add(EntityCondition.makeCondition('transactionDate', EntityOperator.GREATER_THAN_EQUAL_TO, periodClosingFromDate))
openingCashBalanceAndExprs.add(EntityCondition.makeCondition('transactionDate', EntityOperator.LESS_THAN, parametersFromDate))
transactionTotals = select('glAccountId', 'accountName', 'accountCode', 'debitCreditFlag', 'amount')
        .from('AcctgTransEntrySums').where(openingCashBalanceAndExprs).orderBy('glAccountId').queryList()
transactionTotalsMap = [:]
transactionTotalsMap.putAll(openingCashBalances)
transactionTotals.each { transactionTotal ->
    Map accountMap = (Map)transactionTotalsMap.get(transactionTotal.glAccountId)
    if (!accountMap) {
        accountMap = UtilMisc.makeMapWritable(transactionTotal)
        accountMap.remove('debitCreditFlag')
        accountMap.remove('amount')
        accountMap.D = BigDecimal.ZERO
        accountMap.C = BigDecimal.ZERO
        accountMap.balance = BigDecimal.ZERO
    }
    if (accountMap.debitCreditFlag && accountMap.amount) {
        accountMap.remove('debitCreditFlag')
        accountMap.remove('amount')
    }
    if (transactionTotal.debitCreditFlag == 'C') {
        accountMap.C = ((BigDecimal)accountMap.get('C')).add(transactionTotal.amount)
        accountMap.balance = (accountMap.balance).subtract(transactionTotal.amount)
    } else {
        accountMap.D = ((BigDecimal)accountMap.get('D')).add(transactionTotal.amount)
        accountMap.balance = (accountMap.balance).add(transactionTotal.amount)
    }

    transactionTotalsMap.put(transactionTotal.glAccountId, accountMap)
}
glAccountIdList = []
accountBalanceList = UtilMisc.sortMaps(transactionTotalsMap.values().asList(), UtilMisc.toList('accountCode'))
accountBalanceList.each { accountBalance ->
    balanceTotal = balanceTotal.add(accountBalance.balance)
}
openingCashBalanceTotal = balanceTotal
context.openingCashBalanceList = accountBalanceList
cashFlowBalanceTotalList.add('totalName': 'AccountingOpeningCashBalance', 'balance': balanceTotal)
openingTransactionKeySet = transactionTotalsMap.keySet()

// PERIOD CASH BALANCE
// GlAccounts from parameter's fromDate to parameter's thruDate.
accountBalanceList = []
transactionTotals = []
balanceTotal = BigDecimal.ZERO
List periodCashBalanceAndExprs = mainAndExprs as LinkedList
periodCashBalanceAndExprs.add(EntityCondition.makeCondition('transactionDate', EntityOperator.GREATER_THAN_EQUAL_TO, parametersFromDate))
periodCashBalanceAndExprs.add(EntityCondition.makeCondition('transactionDate', EntityOperator.LESS_THAN, thruDate))
transactionTotals = select('glAccountId', 'accountName', 'accountCode', 'debitCreditFlag', 'amount')
        .from('AcctgTransEntrySums').where(periodCashBalanceAndExprs).orderBy('glAccountId').queryList()
if (transactionTotals) {
    Map transactionTotalsMap = [:]
    balanceTotalCredit = BigDecimal.ZERO
    balanceTotalDebit = BigDecimal.ZERO
    transactionTotals.each { transactionTotal ->
        Map accountMap = (Map)transactionTotalsMap.get(transactionTotal.glAccountId)
        if (!accountMap) {
            accountMap = UtilMisc.makeMapWritable(transactionTotal)
            accountMap.remove('debitCreditFlag')
            accountMap.remove('amount')
            accountMap.D = BigDecimal.ZERO
            accountMap.C = BigDecimal.ZERO
            accountMap.balance = BigDecimal.ZERO
        }
        UtilMisc.addToBigDecimalInMap(accountMap, transactionTotal.debitCreditFlag, transactionTotal.amount)
        if (transactionTotal.debitCreditFlag == 'D') {
            balanceTotalDebit = balanceTotalDebit.add(transactionTotal.amount)
        } else {
            balanceTotalCredit = balanceTotalCredit.add(transactionTotal.amount)
        }
        BigDecimal debitAmount = (BigDecimal)accountMap.D
        BigDecimal creditAmount = (BigDecimal)accountMap.C
        BigDecimal balance = debitAmount.subtract(creditAmount)
        accountMap.balance = balance
        transactionTotalsMap.(transactionTotal.glAccountId) = accountMap
    }
    accountBalanceList = UtilMisc.sortMaps(transactionTotalsMap.values().asList(), UtilMisc.toList('accountCode'))
    balanceTotal = balanceTotalDebit.subtract(balanceTotalCredit)
}
periodCashBalanceTotal = balanceTotal
context.periodCashBalanceList = accountBalanceList
context.periodCashBalanceList.add('accountName': uiLabelMap.AccountingTotalPeriodCashBalance, 'balance': balanceTotal)
cashFlowBalanceTotalList.add('totalName': 'AccountingPeriodCashBalance', 'balance': balanceTotal)

// CLOSING BALANCE
// GlAccounts from parameter's fromDate to parameter's thruDate.
accountBalanceList = []
balanceTotal = BigDecimal.ZERO
List transactionTotals = []
transactionTotals.addAll(new LinkedList(context.openingCashBalanceList))
transactionTotals.addAll(new LinkedList(context.periodCashBalanceList))
transactionTotals = UtilMisc.sortMaps(transactionTotals, UtilMisc.toList('accountCode'))
closingTransactionKeySet = []
if (transactionTotals) {
    Map transactionTotalsMap = [:]
    balanceTotalCredit = BigDecimal.ZERO
    balanceTotalDebit = BigDecimal.ZERO
    transactionTotals.each { transactionTotal ->
        if (transactionTotal.D != null) {
            if (transactionTotalsMap.(transactionTotal.glAccountId)) {
                totalDebitBalance = (transactionTotal.D).add(transactionTotalsMap.(transactionTotal.glAccountId).D)
                totalCreditBalance = (transactionTotal.C).add(transactionTotalsMap.(transactionTotal.glAccountId).C)
                if (transactionTotalsMap.(transactionTotal.glAccountId).D == 0 && transactionTotalsMap.(transactionTotal.glAccountId).C == 0) {
                    transactionTotalsMap.(transactionTotal.glAccountId).balance =
                            (transactionTotal.balance).add(transactionTotalsMap.(transactionTotal.glAccountId).balance)
                } else {
                    transactionTotalsMap.(transactionTotal.glAccountId).D = totalDebitBalance
                    transactionTotalsMap.(transactionTotal.glAccountId).C = totalCreditBalance
                    transactionTotalsMap.(transactionTotal.glAccountId).balance = totalDebitBalance.subtract(totalCreditBalance)
                }
            } else {
                transactionTotalsMap.(transactionTotal.glAccountId) = transactionTotal
            }
            accountBalanceList = UtilMisc.sortMaps(transactionTotalsMap.values().asList(), UtilMisc.toList('accountCode'))
        }
    }
    closingTransactionKeySet = transactionTotalsMap.keySet()
}
accountBalanceList.each { accountBalance ->
    balanceTotal = balanceTotal.add(accountBalance.balance)
}
context.closingCashBalanceList = accountBalanceList
context.closingCashBalanceList.add('accountName': uiLabelMap.AccountingTotalClosingCashBalance, 'balance': balanceTotal)

// Get differences of glAccount in closing and opening list and then add difference to opening list.
if (closingTransactionKeySet) {
    closingTransactionKeySet.removeAll(openingTransactionKeySet)
    closingTransactionKeySet.each { closingTransactionKey ->
        glAccount = from('GlAccount').where('glAccountId', closingTransactionKey).cache(true).queryOne()
        context.openingCashBalanceList.add(['glAccountId': glAccount.glAccountId, 'accountName': glAccount.accountName,
                                            accountCode: glAccount.accountCode, balance: BigDecimal.ZERO, D: BigDecimal.ZERO, C: BigDecimal.ZERO])
    }
}
context.openingCashBalanceList.add(['accountName': uiLabelMap.AccountingTotalOpeningCashBalance, 'balance': openingCashBalanceTotal])

// CASH FLOW STATEMENT ENDING BALANCE
// ENDING BALANCE = OPENING CASH BALANCE + PERIOD CASH BALANCE
endingCashBalanceTotal = openingCashBalanceTotal.add(periodCashBalanceTotal)
cashFlowBalanceTotalList.add('totalName': 'AccountingEndingCashBalance', 'balance': endingCashBalanceTotal)
context.cashFlowBalanceTotalList = cashFlowBalanceTotalList
