/*
 * The Kuali Financial System, a comprehensive financial management system for higher education.
 *
 * Copyright 2005-2016 The Kuali Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.kuali.kfs.fp.businessobject;

import org.kuali.kfs.krad.bo.TransientBusinessObjectBase;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.rice.core.api.datetime.DateTimeService;
import org.kuali.rice.core.api.util.type.KualiDecimal;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * This class represents a cashiering-related transaction used in the cash management document
 */
public class CashieringTransaction extends TransientBusinessObjectBase {
    public static final String DETAIL_DOCUMENT_TYPE = "CMD";

    private String campusCode;
    private String referenceFinancialDocumentNumber;

    // money in properties
    private List<Check> moneyInChecks;
    private CoinDetail moneyInCoin;
    private CurrencyDetail moneyInCurrency;
    private CashieringItemInProcess newItemInProcess;
    private List<Check> baselineChecks;
    private Check newCheck;
    private KualiDecimal checkTotal;

    // money out properties
    private CoinDetail moneyOutCoin;
    private CurrencyDetail moneyOutCurrency;
    private List<CashieringItemInProcess> openItemsInProcess;

    private java.util.Date transactionStarted;
    private java.util.Date transactionEnded;

    // incrementers for detail lines
    private Integer nextCheckSequenceId;


    /**
     * Constructs a CashieringTransaction
     */
    public CashieringTransaction(String campusCode, String referenceFinancialDocumentNumber) {
        super();
        this.campusCode = campusCode;
        this.referenceFinancialDocumentNumber = referenceFinancialDocumentNumber;
        this.transactionStarted = SpringContext.getBean(DateTimeService.class).getCurrentDate();

        moneyInCoin = new CoinDetail();
        moneyInCurrency = new CurrencyDetail();

        moneyOutCoin = new CoinDetail();
        moneyOutCurrency = new CurrencyDetail();

        newItemInProcess = new CashieringItemInProcess();
        moneyInChecks = new ArrayList<Check>();
        newCheck = new CheckBase();
        baselineChecks = new ArrayList<Check>();
        openItemsInProcess = new ArrayList<CashieringItemInProcess>();
        nextCheckSequenceId = new Integer(1);
    }

    /**
     * Gets the moneyInChecks attribute.
     *
     * @return Returns the moneyInChecks.
     */
    public List<Check> getMoneyInChecks() {
        return moneyInChecks;
    }

    /**
     * Sets the moneyInChecks attribute value.
     *
     * @param moneyInChecks The moneyInChecks to set.
     */
    public void setMoneyInChecks(List<Check> moneyInChecks) {
        this.moneyInChecks = moneyInChecks;
    }

    /**
     * Retrieves a specific check from the list, by array index
     *
     * @param index the index of the checks array to retrieve the check from
     * @return a Check
     */
    public Check getMoneyInCheck(int index) {
        if (index >= moneyInChecks.size()) {
            for (int i = moneyInChecks.size(); i <= index; i++) {
                moneyInChecks.add(createNewCheck());
            }
        }
        return moneyInChecks.get(index);
    }

    /**
     * Gets the moneyInCoin attribute.
     *
     * @return Returns the moneyInCoin.
     */
    public CoinDetail getMoneyInCoin() {
        return moneyInCoin;
    }

    /**
     * Sets the moneyInCoin attribute value.
     *
     * @param moneyInCoin The moneyInCoin to set.
     */
    public void setMoneyInCoin(CoinDetail moneyInCoin) {
        this.moneyInCoin = moneyInCoin;
    }

    /**
     * Gets the moneyInCurrency attribute.
     *
     * @return Returns the moneyInCurrency.
     */
    public CurrencyDetail getMoneyInCurrency() {
        return moneyInCurrency;
    }

    /**
     * Sets the moneyInCurrency attribute value.
     *
     * @param moneyInCurrency The moneyInCurrency to set.
     */
    public void setMoneyInCurrency(CurrencyDetail moneyInCurrency) {
        this.moneyInCurrency = moneyInCurrency;
    }

    /**
     * Gets the moneyOutCoin attribute.
     *
     * @return Returns the moneyOutCoin.
     */
    public CoinDetail getMoneyOutCoin() {
        return moneyOutCoin;
    }

    /**
     * Sets the moneyOutCoin attribute value.
     *
     * @param moneyOutCoin The moneyOutCoin to set.
     */
    public void setMoneyOutCoin(CoinDetail moneyOutCoin) {
        this.moneyOutCoin = moneyOutCoin;
    }

    /**
     * Gets the transactionEnded attribute.
     *
     * @return Returns the transactionEnded.
     */
    public java.util.Date getTransactionEnded() {
        return transactionEnded;
    }

    /**
     * Sets the transactionEnded attribute value.
     *
     * @param transactionEnded The transactionEnded to set.
     */
    public void setTransactionEnded(java.util.Date transactionEnded) {
        this.transactionEnded = transactionEnded;
    }

    /**
     * Gets the transactionStarted attribute.
     *
     * @return Returns the transactionStarted.
     */
    public java.util.Date getTransactionStarted() {
        return transactionStarted;
    }

    /**
     * Sets the transactionStarted attribute value.
     *
     * @param transactionStarted The transactionStarted to set.
     */
    public void setTransactionStarted(java.util.Date transactionStarted) {
        this.transactionStarted = transactionStarted;
    }

    /**
     * Gets the campusCode attribute.
     *
     * @return Returns the campusCode.
     */
    public String getCampusCode() {
        return campusCode;
    }

    /**
     * Sets the campusCode attribute value.
     *
     * @param campusCode The campusCode to set.
     */
    public void setCampusCode(String campusCode) {
        this.campusCode = campusCode;
    }

    /**
     * Sets the moneyOutCurrency attribute value.
     *
     * @param moneyOutCurrency The moneyOutCurrency to set.
     */
    public void setMoneyOutCurrency(CurrencyDetail moneyOutCurrency) {
        this.moneyOutCurrency = moneyOutCurrency;
    }

    /**
     * Gets the referenceFinancialDocumentNumber attribute.
     *
     * @return Returns the referenceFinancialDocumentNumber.
     */
    public String getReferenceFinancialDocumentNumber() {
        return referenceFinancialDocumentNumber;
    }

    /**
     * Sets the referenceFinancialDocumentNumber attribute value.
     *
     * @param referenceFinancialDocumentNumber The referenceFinancialDocumentNumber to set.
     */
    public void setReferenceFinancialDocumentNumber(String referenceFinancialDocumentNumber) {
        this.referenceFinancialDocumentNumber = referenceFinancialDocumentNumber;
    }

    /**
     * Gets the moneyOutCurrency attribute.
     *
     * @return Returns the moneyOutCurrency.
     */
    public CurrencyDetail getMoneyOutCurrency() {
        return moneyOutCurrency;
    }

    /**
     * Gets the newItemInProcess attribute.
     *
     * @return Returns the newItemInProcess.
     */
    public CashieringItemInProcess getNewItemInProcess() {
        return newItemInProcess;
    }

    /**
     * Sets the newItemInProcess attribute value.
     *
     * @param newItemInProcess The newItemInProcess to set.
     */
    public void setNewItemInProcess(CashieringItemInProcess newItemInProcess) {
        this.newItemInProcess = newItemInProcess;
    }

    /**
     * Gets the openItemsInProcess attribute.
     *
     * @return Returns the openItemsInProcess.
     */
    public List<CashieringItemInProcess> getOpenItemsInProcess() {
        return openItemsInProcess;
    }

    /**
     * Sets the openItemsInProcess attribute value.
     *
     * @param openItemsInProcess The openItemsInProcess to set.
     */
    public void setOpenItemsInProcess(List<CashieringItemInProcess> openItemsInProcess) {
        this.openItemsInProcess = openItemsInProcess;
    }

    /**
     * This method returns a single open item in process
     *
     * @return a cashiering item in process
     */
    public CashieringItemInProcess getOpenItemInProcess(int index) {
        extendOpenItemsList(index);
        return this.openItemsInProcess.get(index);
    }

    /**
     * make the open items in process list bigger, so it doesn't return a null value
     *
     * @param minSize the minsize to make the list
     */
    private void extendOpenItemsList(int minSize) {
        while (this.openItemsInProcess.size() <= minSize) {
            this.openItemsInProcess.add(new CashieringItemInProcess());
        }
    }

    /**
     * Gets the newCheck attribute.
     *
     * @return Returns the newCheck.
     */
    public Check getNewCheck() {
        return newCheck;
    }

    /**
     * Sets the newCheck attribute value.
     *
     * @param newCheck The newCheck to set.
     */
    public void setNewCheck(Check newCheck) {
        this.newCheck = newCheck;
    }

    /**
     * This method will make sure that all of the various currency, coin, check, and item in process detail records are populated
     * with the correct info.
     */
    public void prepareForSave() {
        moneyInCoin.setDocumentNumber(this.referenceFinancialDocumentNumber);
        moneyInCoin.setFinancialDocumentTypeCode(DETAIL_DOCUMENT_TYPE);
        moneyInCoin.setCashieringStatus(KFSConstants.CurrencyCoinSources.CASH_MANAGEMENT_IN);

        moneyInCurrency.setDocumentNumber(this.referenceFinancialDocumentNumber);
        moneyInCurrency.setFinancialDocumentTypeCode(DETAIL_DOCUMENT_TYPE);
        moneyInCurrency.setCashieringStatus(KFSConstants.CurrencyCoinSources.CASH_MANAGEMENT_IN);

        moneyOutCoin.setDocumentNumber(this.referenceFinancialDocumentNumber);
        moneyOutCoin.setFinancialDocumentTypeCode(DETAIL_DOCUMENT_TYPE);
        moneyOutCoin.setCashieringStatus(KFSConstants.CurrencyCoinSources.DEPOSITS);

        moneyOutCurrency.setDocumentNumber(this.referenceFinancialDocumentNumber);
        moneyOutCurrency.setFinancialDocumentTypeCode(DETAIL_DOCUMENT_TYPE);
        moneyOutCurrency.setCashieringStatus(KFSConstants.CurrencyCoinSources.DEPOSITS);

        newItemInProcess.setCampusCode(this.campusCode);
    }

    /**
     * @see org.kuali.rice.krad.bo.BusinessObjectBase#toStringMapper()
     */

    protected LinkedHashMap toStringMapper_RICE20_REFACTORME() {
        LinkedHashMap pkMap = new LinkedHashMap();
        pkMap.put("campusCode", this.campusCode);
        pkMap.put("referenceFinancialDocumentNumber", this.referenceFinancialDocumentNumber);
        pkMap.put("transactionStarted", this.transactionStarted);
        return pkMap;
    }

    /**
     * Gets the checks attribute.
     *
     * @return Returns the checks.
     */
    public List getChecks() {
        return getMoneyInChecks();
    }

    /**
     * Sets the checks attribute value.
     *
     * @param checks The checks to set.
     */
    public void setChecks(List checks) {
        moneyInChecks = new ArrayList<Check>();
        for (Object o : checks) {
            moneyInChecks.add((Check) o);
        }
    }

    /**
     * Gets the number of checks, since Sun doesn't have a direct getter for collection size
     *
     * @return the number of checks
     */
    public int getCheckCount() {
        int count = 0;
        if (moneyInChecks != null) {
            count = moneyInChecks.size();
        }
        return count;
    }


    /**
     * Adds a new check to the list.
     *
     * @param check
     */
    public void addCheck(Check check) {
        check.setSequenceId(this.nextCheckSequenceId);

        this.moneyInChecks.add(check);

        this.nextCheckSequenceId = new Integer(this.nextCheckSequenceId.intValue() + 1);
    }

    /**
     * Retrieve a particular check at a given index in the list of checks.
     *
     * @param index
     * @return Check
     */
    public Check getCheck(int index) {
        while (this.moneyInChecks.size() <= index) {
            moneyInChecks.add(createNewCheck());
        }
        return (Check) moneyInChecks.get(index);
    }

    /**
     * @param checks
     * @return Map containing Checks from the given List, indexed by their sequenceId
     */
    private Map buildCheckMap(List checks) {
        Map checkMap = new HashMap();

        for (Iterator i = checks.iterator(); i.hasNext(); ) {
            Check check = (Check) i.next();
            Integer sequenceId = check.getSequenceId();

            Object oldCheck = checkMap.put(sequenceId, check);

            // verify that sequence numbers are unique...
            if (oldCheck != null) {
                throw new IllegalStateException("sequence id collision detected for sequence id " + sequenceId);
            }
        }

        return checkMap;
    }

    /**
     * This method removes a check from the list and updates the total appropriately.
     *
     * @param index
     */
    public void removeCheck(int index) {
        Check check = (Check) moneyInChecks.remove(index);
        KualiDecimal newTotalCheckAmount = getTotalCheckAmount().subtract(check.getAmount());
        // if the totalCheckAmount goes negative, bring back to zero.
        if (newTotalCheckAmount.isNegative()) {
            newTotalCheckAmount = KualiDecimal.ZERO;
        }
    }

    public KualiDecimal getTotalCheckAmount() {
        KualiDecimal result = KualiDecimal.ZERO;
        for (Check c : moneyInChecks) {
            if (c != null && c.getAmount() != null) {
                result = result.add(c.getAmount());
            }
        }
        return result;
    }

    /**
     * Gets the nextCheckSequenceId attribute.
     *
     * @return Returns the nextCheckSequenceId.
     */
    public Integer getNextCheckSequenceId() {
        return nextCheckSequenceId;
    }

    /**
     * Sets the nextCheckSequenceId attribute value.
     *
     * @param nextCheckSequenceId The nextCheckSequenceId to set.
     */
    public void setNextCheckSequenceId(Integer nextCheckSequenceId) {
        this.nextCheckSequenceId = nextCheckSequenceId;
    }

    public Check createNewCheck() {
        Check newCheck = new CheckBase();
        newCheck.setFinancialDocumentTypeCode(DETAIL_DOCUMENT_TYPE);
        newCheck.setCashieringStatus(KFSConstants.CheckSources.CASH_MANAGEMENT);
        return newCheck;
    }

    /**
     * This method calculates how much money has been paid back in all items in process
     *
     * @return the calculated amount
     */
    public KualiDecimal getPaidBackItemsInProcessAmount() {
        KualiDecimal amount = KualiDecimal.ZERO;
        if (this.openItemsInProcess != null) {
            for (CashieringItemInProcess itemInProcess : this.openItemsInProcess) {
                if (itemInProcess.getCurrentPayment() != null && itemInProcess.getCurrentPayment().isGreaterThan(KualiDecimal.ZERO)) {
                    amount = amount.add(itemInProcess.getCurrentPayment());
                }
            }
        }
        return amount;
    }

    /**
     * @return current List of baseline checks for use in update detection
     */
    public List getBaselineChecks() {
        return baselineChecks;
    }

    /**
     * Sets the current List of baseline checks to the given List
     *
     * @param baselineChecks
     */
    public void setBaselineChecks(List baselineChecks) {
        this.baselineChecks = baselineChecks;
    }

    /**
     * @param index
     * @return true if a baselineCheck with the given index exists
     */
    public boolean hasBaselineCheck(int index) {
        boolean has = false;

        if ((index >= 0) && (index <= baselineChecks.size())) {
            has = true;
        }

        return has;
    }

    /**
     * Implementation creates empty Checks as a side-effect, so that Struts' efforts to set fields of lines which haven't been
     * created will succeed rather than causing a NullPointerException.
     *
     * @param index
     * @return baseline Check at the given index
     */
    public Check getBaselineCheck(int index) {
        while (baselineChecks.size() <= index) {
            baselineChecks.add(this.createNewCheck());
        }
        return (Check) baselineChecks.get(index);
    }

    /**
     * This method calcuates how much money has come in to the "Money In" side of the transaction
     *
     * @return the amount calculated
     */
    public KualiDecimal getMoneyInTotal() {
        KualiDecimal result = KualiDecimal.ZERO;
        result = result.add(this.moneyInCurrency.getTotalAmount());
        result = result.add(this.moneyInCoin.getTotalAmount());
        result = result.add(this.getTotalCheckAmount());
        if (this.newItemInProcess.isPopulated()) {
            result = result.add(this.newItemInProcess.getItemAmount());
        }
        return result;
    }

    /**
     * This method calculates how much money has gone out through the "Money Out" side of the transaction
     *
     * @return the amount calculated
     */
    public KualiDecimal getMoneyOutTotal() {
        KualiDecimal result = KualiDecimal.ZERO;
        result = result.add(this.moneyOutCurrency.getTotalAmount());
        result = result.add(this.moneyOutCoin.getTotalAmount());
        result = result.add(this.getPaidBackItemsInProcessAmount());
        return result;
    }

    /**
     * @param checkTotal
     * @deprecated
     */
    public void setCheckTotal(KualiDecimal checkTotal) {
        this.checkTotal = checkTotal;
    }

}
