/*
 * 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.sys.web.struts;

import org.apache.commons.lang.StringUtils;
import org.apache.struts.upload.FormFile;
import org.kuali.kfs.coa.businessobject.Account;
import org.kuali.kfs.coa.businessobject.ObjectCode;
import org.kuali.kfs.coa.businessobject.SubAccount;
import org.kuali.kfs.coa.businessobject.SubObjectCode;
import org.kuali.kfs.coreservice.framework.parameter.ParameterService;
import org.kuali.kfs.kns.service.BusinessObjectDictionaryService;
import org.kuali.kfs.krad.exception.InfrastructureException;
import org.kuali.kfs.krad.util.KRADConstants;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.businessobject.AccountingLine;
import org.kuali.kfs.sys.businessobject.AccountingLineOverride;
import org.kuali.kfs.sys.businessobject.SourceAccountingLine;
import org.kuali.kfs.sys.businessobject.TargetAccountingLine;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.document.AccountingDocument;
import org.kuali.kfs.sys.document.web.struts.FinancialSystemTransactionalDocumentFormBase;
import org.kuali.kfs.sys.service.impl.KfsParameterConstants;
import org.kuali.rice.core.api.config.property.ConfigurationService;
import org.kuali.rice.core.web.format.CurrencyFormatter;

import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * This class is the base action form for all financial documents.
 */
public class KualiAccountingDocumentFormBase extends FinancialSystemTransactionalDocumentFormBase {
    protected static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(KualiAccountingDocumentFormBase.class);

    protected SourceAccountingLine newSourceLine;
    protected TargetAccountingLine newTargetLine;

    protected Map editableAccounts;
    protected Map forcedLookupOptionalFields;

    // TODO: FormFile isn't Serializable, so mark these fields need as transient or create a Serializable subclass of FormFile
    protected FormFile sourceFile;
    protected FormFile targetFile;
    protected boolean hideDetails = false;

    /**
     * This constructor sets up empty instances for the dependent objects...
     */
    public KualiAccountingDocumentFormBase() {
        super();

        // create an empty editableAccounts map, for safety's sake
        editableAccounts = new HashMap();
        forcedReadOnlyFields = new HashMap();
        forcedLookupOptionalFields = new HashMap();
    }


    /**
     * Overrides the parent to call super.populate and then to call the accounting lines populate method that is specific to loading
     * the two select lists on the page.
     *
     * @see org.kuali.rice.kns.web.struts.pojo.PojoForm#populate(javax.servlet.http.HttpServletRequest)
     */
    @Override
    public void populate(HttpServletRequest request) {
        super.populate(request);
        final String methodToCall = this.getMethodToCall();
        final Map parameterMap = request.getParameterMap();

        populateAccountingLinesForResponse(methodToCall, parameterMap);

        setDocTypeName(discoverDocumentTypeName());
    }

    /**
     * Populates the accounting lines which need to be updated to successfully complete a response to the request
     *
     * @param methodToCall the method to call in the action to complete this request transaction
     * @param parameterMap the map of parameters which came in with the transaction
     */
    protected void populateAccountingLinesForResponse(String methodToCall, Map parameterMap) {
        populateSourceAccountingLine(getNewSourceLine(), KFSPropertyConstants.NEW_SOURCE_LINE, parameterMap);
        populateTargetAccountingLine(getNewTargetLine(), KFSPropertyConstants.NEW_TARGET_LINE, parameterMap);

        // don't call populateAccountingLines if you are copying or errorCorrecting a document,
        // since you want the accountingLines in the copy to be "identical" to those in the original
        if (!StringUtils.equals(methodToCall, KFSConstants.COPY_METHOD) && !StringUtils.equals(methodToCall, KFSConstants.ERRORCORRECT_METHOD)) {
            populateAccountingLines(parameterMap);
        }
    }

    /**
     * This method iterates over all of the source lines and all of the target lines in a transactional document, and calls
     * prepareAccountingLineForValidationAndPersistence on each one. This is called because a user could have updated already
     * existing accounting lines that had blank values in composite key fields.
     *
     * @param parameterMap the map of parameters that were sent in with the request
     */
    protected void populateAccountingLines(Map parameterMap) {
        Iterator sourceLines = getFinancialDocument().getSourceAccountingLines().iterator();
        int count = 0;
        while (sourceLines.hasNext()) {
            SourceAccountingLine sourceLine = (SourceAccountingLine) sourceLines.next();
            populateSourceAccountingLine(sourceLine, KFSPropertyConstants.DOCUMENT + "." + KFSPropertyConstants.SOURCE_ACCOUNTING_LINE + "[" + count + "]", parameterMap);
            count += 1;
        }

        Iterator targetLines = getFinancialDocument().getTargetAccountingLines().iterator();
        count = 0;
        while (targetLines.hasNext()) {
            TargetAccountingLine targetLine = (TargetAccountingLine) targetLines.next();
            populateTargetAccountingLine(targetLine, KFSPropertyConstants.DOCUMENT + "." + KFSPropertyConstants.TARGET_ACCOUNTING_LINE + "[" + count + "]", parameterMap);
            count += 1;
        }
    }

    /**
     * Populates a source accounting line bo using values from the struts form. This is in place to make sure that all of the
     * composite key objects have the correct values in them. This should be overridden by children forms in the situation where
     * document level attributes need to be pushed down into the accounting lines.
     *
     * @param sourceLine
     * @param accountingLinePropertyName the property path from the form to the accounting line
     * @param parameterMap               the map of parameters that were sent in with the request
     */
    public void populateSourceAccountingLine(SourceAccountingLine sourceLine, String accountingLinePropertyName, Map parameterMap) {
        populateAccountingLine(sourceLine, accountingLinePropertyName, parameterMap);
    }

    /**
     * Populates a target accounting line bo using values from the struts form. This is in place to make sure that all of the
     * composite key objects have the correct values in them. This should be overridden by children forms in the situation where
     * document level attributes need to be pushed down into the accounting lines.
     *
     * @param targetLine
     * @param accountingLinePropertyName the property path from the form to the accounting line
     * @param parameterMap               the map of parameters that were sent in with the request
     */
    public void populateTargetAccountingLine(TargetAccountingLine targetLine, String accountingLinePropertyName, Map parameterMap) {
        populateAccountingLine(targetLine, accountingLinePropertyName, parameterMap);
    }

    /**
     * Populates the dependent fields of objects contained within the given accountingLine
     *
     * @param line
     * @param accountingLinePropertyName the property path from the form to the accounting line
     * @param parameterMap               the map of parameters that were sent in with the request
     */
    @SuppressWarnings("deprecation")
    protected void populateAccountingLine(AccountingLine line, String accountingLinePropertyName, Map parameterMap) {
        SpringContext.getBean(BusinessObjectDictionaryService.class).performForceUppercase(line);

        line.setDocumentNumber(getDocument().getDocumentNumber());

        if (ObjectUtils.isNull(line.getAccount())) {
            line.setAccount(new Account());
        }
        line.getAccount().setChartOfAccountsCode(line.getChartOfAccountsCode());

        if (ObjectUtils.isNull(line.getObjectCode())) {
            line.setObjectCode(new ObjectCode());
        }
        line.getObjectCode().setUniversityFiscalYear(getFinancialDocument().getPostingYear());
        line.getObjectCode().setChartOfAccountsCode(line.getChartOfAccountsCode());

        if (ObjectUtils.isNull(line.getSubAccount())) {
            line.setSubAccount(new SubAccount());
        }
        line.getSubAccount().setChartOfAccountsCode(line.getChartOfAccountsCode());
        line.getSubAccount().setAccountNumber(line.getAccountNumber());

        if (ObjectUtils.isNull(line.getSubObjectCode())) {
            line.setSubObjectCode(new SubObjectCode());
        }
        line.getSubObjectCode().setChartOfAccountsCode(line.getChartOfAccountsCode());
        line.getSubObjectCode().setAccountNumber(line.getAccountNumber());
        line.getSubObjectCode().setFinancialObjectCode(line.getFinancialObjectCode());
        line.getSubObjectCode().setUniversityFiscalYear(getFinancialDocument().getPostingYear());

        repopulateOverrides(line, accountingLinePropertyName, parameterMap);

        AccountingLineOverride.populateFromInput(line);
    }

    /**
     * This repopulates the override values from the request
     *
     * @param line                       the line to repopulate override values for
     * @param accountingLinePropertyName the property path from the form to the accounting line
     * @param parameterMap               the map of parameters that were sent in with the request
     */
    protected void repopulateOverrides(AccountingLine line, String accountingLinePropertyName, Map parameterMap) {
        AccountingLineOverride.determineNeededOverrides(getFinancialDocument(), line);
        if (line.getAccountExpiredOverrideNeeded()) {
            if (LOG.isDebugEnabled()) {
                StringUtils.join(parameterMap.keySet(), "\n");
            }
            if (parameterMap.containsKey(accountingLinePropertyName + ".accountExpiredOverride.present")) {
                line.setAccountExpiredOverride(parameterMap.containsKey(accountingLinePropertyName + ".accountExpiredOverride"));
            }
        } else {
            line.setAccountExpiredOverride(false);
        }
        if (line.isObjectBudgetOverrideNeeded()) {
            if (parameterMap.containsKey(accountingLinePropertyName + ".objectBudgetOverride.present")) {
                line.setObjectBudgetOverride(parameterMap.containsKey(accountingLinePropertyName + ".objectBudgetOverride"));
            }
        } else {
            line.setObjectBudgetOverride(false);
        }
    }

    /**
     * This method retrieves an instance of the form.
     *
     * @return
     */
    public AccountingDocument getFinancialDocument() {
        return (AccountingDocument) getDocument();
    }

    /**
     * @return Returns the newTargetLine.
     */
    public TargetAccountingLine getNewTargetLine() {
        if (newTargetLine == null) {
            newTargetLine = createNewTargetAccountingLine(getFinancialDocument());
        }
        return newTargetLine;
    }

    /**
     * @param newExpenseLine The newTargetLine to set.
     */
    public void setNewTargetLine(TargetAccountingLine newExpenseLine) {
        this.newTargetLine = newExpenseLine;
    }

    /**
     * @return Returns the newSourceLine.
     */
    public SourceAccountingLine getNewSourceLine() {
        if (newSourceLine == null) {
            newSourceLine = createNewSourceAccountingLine(getFinancialDocument());
        }
        return newSourceLine;
    }

    /**
     * @param newIncomeLine The newSourceLine to set.
     */
    public void setNewSourceLine(SourceAccountingLine newIncomeLine) {
        this.newSourceLine = newIncomeLine;
    }

    /**
     * @return Returns the sourceFile.
     */
    public FormFile getSourceFile() {
        return sourceFile;
    }

    /**
     * @param sourceFile The sourceFile to set.
     */
    public void setSourceFile(FormFile sourceFile) {
        this.sourceFile = sourceFile;
    }

    /**
     * @return Returns the targetFile.
     */
    public FormFile getTargetFile() {
        return targetFile;
    }

    /**
     * @param targetFile The targetFile to set.
     */
    public void setTargetFile(FormFile targetFile) {
        this.targetFile = targetFile;
    }


    /**
     * @return current Map of editableAccounts
     */
    public Map getEditableAccounts() {
        return editableAccounts;
    }

    /**
     * @param editableAccounts the account Map to set
     */
    public void setEditableAccounts(Map editableAccounts) {
        this.editableAccounts = editableAccounts;
    }

    /**
     * @return hideDetails attribute
     */
    public boolean isHideDetails() {
        return hideDetails;
    }

    /**
     * @return hideDetails attribute
     * @see #isHideDetails()
     */
    public boolean getHideDetails() {
        return isHideDetails();
    }

    /**
     * @param hideDetails
     */
    public void setHideDetails(boolean hideDetails) {
        this.hideDetails = hideDetails;
    }

    /**
     * Retrieves the source accounting lines total in a currency format with commas.
     *
     * @return String
     */
    public String getCurrencyFormattedSourceTotal() {
        return (String) new CurrencyFormatter().format(getFinancialDocument().getSourceTotal());
    }

    /**
     * Retrieves the source accounting lines total in a currency format with commas.
     *
     * @return String
     */
    public String getCurrencyFormattedTargetTotal() {
        return (String) new CurrencyFormatter().format(getFinancialDocument().getTargetTotal());
    }

    /**
     * @return the URL to the accounting line import instructions
     */
    public String getAccountingLineImportInstructionsUrl() {
        return SpringContext.getBean(ConfigurationService.class).getPropertyValueAsString(KFSConstants.EXTERNALIZABLE_HELP_URL_KEY) + SpringContext.getBean(ParameterService.class).getParameterValueAsString(KfsParameterConstants.FINANCIAL_SYSTEM_DOCUMENT.class, KFSConstants.FinancialApcParms.ACCOUNTING_LINE_IMPORT_HELP);
    }

    /**
     * @param financialDocument
     * @return a new source accounting line for the document
     */
    protected SourceAccountingLine createNewSourceAccountingLine(AccountingDocument financialDocument) {
        if (financialDocument == null) {
            throw new IllegalArgumentException("invalid (null) document");
        }
        try {
            return (SourceAccountingLine) financialDocument.getSourceAccountingLineClass().newInstance();
        } catch (Exception e) {
            throw new InfrastructureException("unable to create a new source accounting line", e);
        }
    }

    /**
     * @param financialDocument
     * @return a new target accounting line for the documet
     */
    protected TargetAccountingLine createNewTargetAccountingLine(AccountingDocument financialDocument) {
        if (financialDocument == null) {
            throw new IllegalArgumentException("invalid (null) document");
        }
        try {
            return (TargetAccountingLine) financialDocument.getTargetAccountingLineClass().newInstance();
        } catch (Exception e) {
            throw new InfrastructureException("unable to create a new target accounting line", e);
        }
    }

    /**
     * This method takes a generic list, hopefully with some AccountingLine objects in it, and returns a list of AccountingLine
     * objects, because Java generics are just so wonderful.
     *
     * @param lines a list of objects
     * @return a list of the accounting lines that were in the lines parameter
     */
    protected List<AccountingLine> harvestAccountingLines(List lines) {
        List<AccountingLine> accountingLines = new ArrayList<AccountingLine>();
        for (Object o : lines) {
            if (o instanceof AccountingLine) {
                accountingLines.add((AccountingLine) o);
            }
        }
        return accountingLines;
    }

    /**
     * A <code>{@link Map}</code> of names of optional accounting line fields that require a quickfinder.
     *
     * @return a Map of fields
     */
    public void setForcedLookupOptionalFields(Map fieldMap) {
        forcedLookupOptionalFields = fieldMap;
    }

    /**
     * A <code>{@link Map}</code> of names of optional accounting line fields that require a quickfinder.
     *
     * @return a Map of fields
     */
    public Map getForcedLookupOptionalFields() {
        return forcedLookupOptionalFields;
    }

    /**
     * Adds the accounting line file size to the list of max file sizes.
     *
     * @see org.kuali.rice.kns.web.struts.form.pojo.PojoFormBase#customInitMaxUploadSizes()
     */
    @Override
    protected void customInitMaxUploadSizes() {
        super.customInitMaxUploadSizes();
        addMaxUploadSize(SpringContext.getBean(ParameterService.class).getParameterValueAsString(KfsParameterConstants.FINANCIAL_SYSTEM_DOCUMENT.class, KFSConstants.ACCOUNTING_LINE_IMPORT_MAX_FILE_SIZE_PARM_NM));
    }

    /**
     * @see org.kuali.rice.kns.web.struts.form.KualiDocumentFormBase#shouldMethodToCallParameterBeUsed(java.lang.String, java.lang.String, javax.servlet.http.HttpServletRequest)
     */
    @Override
    public boolean shouldMethodToCallParameterBeUsed(String methodToCallParameterName, String methodToCallParameterValue, HttpServletRequest request) {
        if (StringUtils.equals(methodToCallParameterName, KRADConstants.DISPATCH_REQUEST_PARAMETER)) {
            if (this.getExcludedmethodToCall().contains(methodToCallParameterValue)) {
                return true;
            }
        }
        return super.shouldMethodToCallParameterBeUsed(methodToCallParameterName, methodToCallParameterValue, request);
    }

    /**
     * get the names of the methods to call that can be excluded from the "be used" check.
     *
     * @return the names of the methods to call that can be excluded from the "be used" check
     */
    protected List<String> getExcludedmethodToCall() {
        return new ArrayList<String>();
    }
}
