/*
 * 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.module.ec.document.web.struts;

import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.coa.businessobject.Account;
import org.kuali.kfs.coa.businessobject.Chart;
import org.kuali.kfs.coa.businessobject.ObjectCode;
import org.kuali.kfs.coa.businessobject.SubAccount;
import org.kuali.kfs.coa.service.AccountService;
import org.kuali.kfs.integration.cg.ContractsAndGrantsModuleService;
import org.kuali.kfs.kns.inquiry.Inquirable;
import org.kuali.kfs.kns.lookup.HtmlData;
import org.kuali.kfs.kns.lookup.HtmlData.AnchorHtmlData;
import org.kuali.kfs.kns.lookup.LookupUtils;
import org.kuali.kfs.krad.bo.DataObjectRelationship;
import org.kuali.kfs.krad.service.PersistenceStructureService;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.module.ec.EffortConstants;
import org.kuali.kfs.module.ec.EffortPropertyConstants;
import org.kuali.kfs.module.ec.businessobject.EffortCertificationDetail;
import org.kuali.kfs.module.ec.businessobject.EffortCertificationDetailLineOverride;
import org.kuali.kfs.module.ec.businessobject.inquiry.EffortLedgerBalanceInquirableImpl;
import org.kuali.kfs.module.ec.businessobject.inquiry.EffortPositionDataDetailsInquirableImpl;
import org.kuali.kfs.module.ec.document.EffortCertificationDocument;
import org.kuali.kfs.module.ec.util.PayrollAmountHolder;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.document.web.struts.FinancialSystemTransactionalDocumentFormBase;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.kim.api.identity.Person;
import org.kuali.rice.krad.bo.BusinessObject;

import javax.servlet.http.HttpServletRequest;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

/**
 * Action form for Effort Certification Document.
 */
public class EffortCertificationForm extends FinancialSystemTransactionalDocumentFormBase {
    protected static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(EffortCertificationForm.class);

    protected EffortCertificationDetail newDetailLine;

    /**
     * Constructs a EffortCertificationForm.java.
     */
    public EffortCertificationForm() {
        super();

        this.setNewDetailLine(this.createNewDetailLine());
    }

    @Override
    protected String getDefaultDocumentTypeName() {
        return EffortConstants.EffortDocumentTypes.EFFORT_CERTIFICATION_DOCUMENT;
    }

    /**
     * initialize a new detail line
     *
     * @return the initialized detail line
     */
    public EffortCertificationDetail createNewDetailLine() {
        EffortCertificationDetail detailLine = new EffortCertificationDetail();
        detailLine.setEffortCertificationUpdatedOverallPercent(null);
        detailLine.setEffortCertificationPayrollAmount(null);
        detailLine.setSubAccountNumber(null);

        return detailLine;
    }

    /**
     * @return new detail line
     */
    public EffortCertificationDetail getNewDetailLine() {
        return newDetailLine;
    }

    /**
     * Sets the new detail line
     *
     * @param newDetailLine
     */
    public void setNewDetailLine(EffortCertificationDetail newDetailLine) {
        this.newDetailLine = newDetailLine;
    }

    /**
     * Gets the effortCertificationDocument attribute.
     *
     * @return Returns the effortCertificationDocument.
     */
    public EffortCertificationDocument getEffortCertificationDocument() {
        return (EffortCertificationDocument) this.getDocument();
    }

    /**
     * @see org.kuali.rice.kns.web.struts.form.KualiDocumentFormBase#populate(javax.servlet.http.HttpServletRequest)
     */
    @Override
    public void populate(HttpServletRequest request) {
        super.populate(request);

        for (EffortCertificationDetail detailLine : this.getDetailLines()) {
            EffortCertificationDetailLineOverride.populateFromInput(detailLine);
        }
    }

    /**
     * Gets the detailLines attribute.
     *
     * @return Returns the detailLines.
     */
    public List<EffortCertificationDetail> getDetailLines() {
        EffortCertificationDocument effortCertificationDocument = (EffortCertificationDocument) this.getDocument();
        return effortCertificationDocument.getEffortCertificationDetailLines();
    }

    /**
     * get the relationship metadata for the detail line fields
     *
     * @return the relationship metadata for the detail line fields
     */
    public Map<String, DataObjectRelationship> getRelationshipMetadata() {
        LOG.debug("getRelationshipMetadata() start");

        PersistenceStructureService persistenceStructureService = SpringContext.getBean(PersistenceStructureService.class);

        Map<String, DataObjectRelationship> relationshipMetadata = new HashMap<String, DataObjectRelationship>();
        for (String attributeName : this.getInquirableFieldNames()) {
            Map<String, Class<? extends BusinessObject>> primitiveReference = LookupUtils.getPrimitiveReference(newDetailLine, attributeName);

            if (primitiveReference != null && !primitiveReference.isEmpty()) {
                DataObjectRelationship primitiveRelationship = this.getPrimitiveDataObjectRelationship(persistenceStructureService.getRelationshipMetadata(newDetailLine.getClass(), attributeName));
                relationshipMetadata.put(attributeName, primitiveRelationship);
            }
        }

        return relationshipMetadata;
    }

    /**
     * Gets the inquiryUrl attribute.
     *
     * @return Returns the inquiryUrl for the detail lines in the document.
     */
    public List<Map<String, HtmlData>> getDetailLineFieldInquiryUrl() {
        LOG.debug("getDetailLineFieldInquiryUrl() start");

        return this.getDetailLineFieldInquiryUrl(this.getDetailLines());
    }

    /**
     * Gets the fieldInfo attribute.
     *
     * @return Returns the fieldInfo.
     */
    public List<Map<String, String>> getFieldInfo() {
        LOG.debug("getFieldInfo() start");

        return this.getFieldInfo(this.getDetailLines());
    }

    /**
     * pick up the primitive relationship for an attribute from a set of relationships. Generally, the primitive relationship is
     * that has the minimum number of primary keys.
     *
     * @param relationshipMetadata the relationship metadata that contains the primitive relationship
     * @return the primitive relationship for an attribute from a set of relationships.
     */
    protected DataObjectRelationship getPrimitiveDataObjectRelationship(Map<String, DataObjectRelationship> relationshipMetadata) {
        int minCountOfKeys = Integer.MAX_VALUE;
        DataObjectRelationship primitiveRelationship = null;

        for (String attribute : relationshipMetadata.keySet()) {
            DataObjectRelationship currentRelationship = relationshipMetadata.get(attribute);

            Map<String, String> parentToChildReferences = currentRelationship.getParentToChildReferences();
            if (parentToChildReferences.size() < minCountOfKeys) {
                minCountOfKeys = parentToChildReferences.size();
                primitiveRelationship = currentRelationship;
            }
        }
        return primitiveRelationship;
    }

    /**
     * Gets the inquiryUrl attribute.
     *
     * @return Returns the inquiryUrl for the detail lines in the document.
     */
    protected List<Map<String, HtmlData>> getDetailLineFieldInquiryUrl(List<EffortCertificationDetail> detailLines) {
        LOG.debug("getDetailLineFieldInquiryUrl(List<EffortCertificationDetail>) start");

        Map<String, String> noninquirableFieldValues = this.getNoninquirableFieldValues();
        Inquirable inquirable = this.getInquirable();

        List<Map<String, HtmlData>> inquiryURL = new ArrayList<Map<String, HtmlData>>();
        for (EffortCertificationDetail detailLine : detailLines) {
            detailLine.refreshNonUpdateableReferences();

            Map<String, HtmlData> inquiryURLForAttribute = new HashMap<String, HtmlData>();
            for (String attributeName : this.getInquirableFieldNames()) {
                // exclude the non inquirable field values
                Object attributeValue = ObjectUtils.getPropertyValue(detailLine, attributeName);
                String noninquirableFieldValue = noninquirableFieldValues.get(attributeName);
                if (noninquirableFieldValue != null && noninquirableFieldValue.equals(attributeValue)) {
                    continue;
                }

                HtmlData inquiryHref;
                if (this.getCustomizedInquirableFieldNames().contains(attributeName)) {
                    inquiryHref = this.getCustomizedInquiryUrl(detailLine, attributeName);
                } else {
                    inquiryHref = inquirable.getInquiryUrl(detailLine, attributeName, false);
                }

                inquiryURLForAttribute.put(attributeName, inquiryHref);
            }

            inquiryURL.add(inquiryURLForAttribute);
        }

        return inquiryURL;
    }

    /**
     * get the inquiry URL for the specified attribute
     *
     * @param detailLine    the detail line containing the given attribute
     * @param attributeName the specified attribute name
     * @return the inquiry URL for the specified attribute
     */
    protected HtmlData getCustomizedInquiryUrl(EffortCertificationDetail detailLine, String attributeName) {
        if (StringUtils.equals(attributeName, KFSPropertyConstants.POSITION_NUMBER)) {
            AnchorHtmlData inquiryHref = (AnchorHtmlData) getEffortPositionDataDetailsInquirableImpl().getInquiryUrl(detailLine, attributeName);
            inquiryHref.setHref(this.getCompleteURL(inquiryHref.getHref()));
            return inquiryHref;
        }

        AnchorHtmlData inquiryHref = (AnchorHtmlData) getInquirable().getInquiryUrl(detailLine, attributeName, false);
        inquiryHref.setHref(this.getCompleteURL(inquiryHref.getHref()));

        return inquiryHref;
    }

    /**
     * Gets the inquirableFieldNames attribute.
     *
     * @return Returns the inquirableFieldNames.
     */
    public Map<String, String> getNoninquirableFieldValues() {
        Map<String, String> inquirableFieldNames = new HashMap<String, String>();
        inquirableFieldNames.put(KFSPropertyConstants.SUB_ACCOUNT_NUMBER, KFSConstants.getDashSubAccountNumber());
        inquirableFieldNames.put(KFSPropertyConstants.FINANCIAL_OBJECT_CODE, KFSConstants.getDashFinancialObjectCode());
        inquirableFieldNames.put(KFSPropertyConstants.POSITION_NUMBER, EffortConstants.DASH_POSITION_NUMBER);

        inquirableFieldNames.put(EffortPropertyConstants.SOURCE_CHART_OF_ACCOUNTS_CODE, EffortConstants.DASH_CHART_OF_ACCOUNTS_CODE);
        inquirableFieldNames.put(EffortPropertyConstants.SOURCE_ACCOUNT_NUMBER, EffortConstants.DASH_ACCOUNT_NUMBER);
        inquirableFieldNames.put(EffortPropertyConstants.COST_SHARE_SOURCE_SUB_ACCOUNT_NUMBER, KFSConstants.getDashSubAccountNumber());

        return inquirableFieldNames;
    }

    /**
     * Gets the inquirableFieldNames attribute.
     *
     * @return Returns the inquirableFieldNames.
     */
    public List<String> getInquirableFieldNames() {
        List<String> inquirableFieldNames = new ArrayList<String>();
        inquirableFieldNames.add(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE);
        inquirableFieldNames.add(KFSPropertyConstants.ACCOUNT_NUMBER);
        inquirableFieldNames.add(KFSPropertyConstants.SUB_ACCOUNT_NUMBER);
        inquirableFieldNames.add(KFSPropertyConstants.FINANCIAL_OBJECT_CODE);
        inquirableFieldNames.add(KFSPropertyConstants.POSITION_NUMBER);

        inquirableFieldNames.add(EffortPropertyConstants.SOURCE_CHART_OF_ACCOUNTS_CODE);
        inquirableFieldNames.add(EffortPropertyConstants.SOURCE_ACCOUNT_NUMBER);
        inquirableFieldNames.add(EffortPropertyConstants.COST_SHARE_SOURCE_SUB_ACCOUNT_NUMBER);

        inquirableFieldNames.add(EffortPropertyConstants.EFFORT_CERTIFICATION_ORIGINAL_PAYROLL_AMOUNT);
        inquirableFieldNames.add(EffortPropertyConstants.EFFORT_CERTIFICATION_PAYROLL_AMOUNT);

        return inquirableFieldNames;
    }

    /**
     * get the inquirable field names that need to be handled specially
     *
     * @return the inquirable field names that need to be handled specially
     */
    public List<String> getCustomizedInquirableFieldNames() {
        List<String> inquirableFieldNames = new ArrayList<String>();

        inquirableFieldNames.add(KFSPropertyConstants.POSITION_NUMBER);
        inquirableFieldNames.add(EffortPropertyConstants.EFFORT_CERTIFICATION_ORIGINAL_PAYROLL_AMOUNT);
        inquirableFieldNames.add(EffortPropertyConstants.EFFORT_CERTIFICATION_PAYROLL_AMOUNT);

        return inquirableFieldNames;
    }

    /**
     * append the extract query string into the given base URL
     *
     * @param baseURL the given base URL. If the parameter is blank, the base URL won't be changed
     * @return the complete URL built from the given base URL and extra query strings
     */
    protected String getCompleteURL(String baseURL) {
        if (StringUtils.isBlank(baseURL)) {
            return baseURL;
        }

        String completeURL = baseURL;
        EffortCertificationDocument document = (EffortCertificationDocument) this.getDocument();
        Properties properties = new Properties();

        properties.put(KFSPropertyConstants.EMPLID, document.getEmplid());
        properties.put(EffortPropertyConstants.EFFORT_CERTIFICATION_REPORT_NUMBER, document.getEffortCertificationReportNumber());
        properties.put(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, document.getUniversityFiscalYear());

        StringBuilder queryString = new StringBuilder();
        for (Object key : properties.keySet()) {
            queryString.append("&").append(key).append("=").append(properties.get(key));
        }

        return completeURL.concat(queryString.toString());
    }


    /**
     * Gets the fieldInfo attribute.
     *
     * @return Returns the fieldInfo.
     */
    protected Map<String, String> getFieldInfo(EffortCertificationDetail detailLine) {
        LOG.info("getFieldInfo(List<EffortCertificationDetail>) start");

        //Map<String, String> fieldInfo = new HashMap<String, String>();
        EffortCertificationDocument document = (EffortCertificationDocument) this.getDocument();
        KualiDecimal totalOriginalPayrollAmount = document.getTotalOriginalPayrollAmount();


        detailLine.refreshNonUpdateableReferences();

        Map<String, String> fieldInfoForAttribute = new HashMap<String, String>();

        fieldInfoForAttribute.put(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, ObjectUtils.isNotNull(detailLine.getChartOfAccounts()) ? detailLine.getChartOfAccounts().getFinChartOfAccountDescription() : "");

        String accountInfo = buildAccountInfo(detailLine.getAccount());
        fieldInfoForAttribute.put(KFSPropertyConstants.ACCOUNT_NUMBER, accountInfo);

        SubAccount subAccount = detailLine.getSubAccount();
        if (ObjectUtils.isNotNull(subAccount)) {
            fieldInfoForAttribute.put(KFSPropertyConstants.SUB_ACCOUNT_NUMBER, subAccount.getSubAccountName());
        }

        ObjectCode objectCode = detailLine.getFinancialObject();
        if (ObjectUtils.isNotNull(objectCode)) {
            fieldInfoForAttribute.put(KFSPropertyConstants.FINANCIAL_OBJECT_CODE, objectCode.getFinancialObjectCodeName());
        }

        Account sourceAccount = detailLine.getSourceAccount();
        if (ObjectUtils.isNotNull(sourceAccount)) {
            fieldInfoForAttribute.put(EffortPropertyConstants.SOURCE_ACCOUNT_NUMBER, sourceAccount.getAccountName());
        }

        Chart sourceChart = detailLine.getSourceChartOfAccounts();
        if (ObjectUtils.isNotNull(sourceChart)) {
            fieldInfoForAttribute.put(EffortPropertyConstants.SOURCE_CHART_OF_ACCOUNTS_CODE, sourceChart.getFinChartOfAccountDescription());
        }

        KualiDecimal originalPayrollAmount = detailLine.getEffortCertificationOriginalPayrollAmount();
        String actualOriginalPercent = PayrollAmountHolder.recalculateEffortPercentAsString(totalOriginalPayrollAmount, originalPayrollAmount);
        fieldInfoForAttribute.put(EffortPropertyConstants.EFFORT_CERTIFICATION_CALCULATED_OVERALL_PERCENT, actualOriginalPercent);

        //fieldInfo.add(fieldInfoForAttribute);


        return fieldInfoForAttribute;
    }

    /**
     * Gets the fieldInfo attribute.
     *
     * @return Returns the fieldInfo.
     */
    public Map<String, String> getDetailLineFieldInfo() {
        LOG.info("getSummarizedDetailLineFieldInfo() start");

        return this.getFieldInfo(this.getNewDetailLine());
    }

    /**
     * Gets the fieldInfo attribute.
     *
     * @return Returns the fieldInfo.
     */
    protected List<Map<String, String>> getFieldInfo(List<EffortCertificationDetail> detailLines) {
        LOG.debug("getFieldInfo(List<EffortCertificationDetail>) start");

        List<Map<String, String>> fieldInfo = new ArrayList<Map<String, String>>();
        EffortCertificationDocument document = (EffortCertificationDocument) this.getDocument();
        KualiDecimal totalOriginalPayrollAmount = document.getTotalOriginalPayrollAmount();

        for (EffortCertificationDetail detailLine : detailLines) {
            detailLine.refreshNonUpdateableReferences();

            Map<String, String> fieldInfoForAttribute = new HashMap<String, String>();

            fieldInfoForAttribute.put(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, detailLine.getChartOfAccounts().getFinChartOfAccountDescription());

            String accountInfo = buildAccountInfo(detailLine.getAccount());
            fieldInfoForAttribute.put(KFSPropertyConstants.ACCOUNT_NUMBER, accountInfo);

            SubAccount subAccount = detailLine.getSubAccount();
            if (ObjectUtils.isNotNull(subAccount)) {
                fieldInfoForAttribute.put(KFSPropertyConstants.SUB_ACCOUNT_NUMBER, subAccount.getSubAccountName());
            }

            ObjectCode objectCode = detailLine.getFinancialObject();
            if (ObjectUtils.isNotNull(objectCode)) {
                fieldInfoForAttribute.put(KFSPropertyConstants.FINANCIAL_OBJECT_CODE, objectCode.getFinancialObjectCodeName());
            }

            Account sourceAccount = detailLine.getSourceAccount();
            if (ObjectUtils.isNotNull(sourceAccount)) {
                fieldInfoForAttribute.put(EffortPropertyConstants.SOURCE_ACCOUNT_NUMBER, sourceAccount.getAccountName());
            }

            Chart sourceChart = detailLine.getSourceChartOfAccounts();
            if (ObjectUtils.isNotNull(sourceChart)) {
                fieldInfoForAttribute.put(EffortPropertyConstants.SOURCE_CHART_OF_ACCOUNTS_CODE, sourceChart.getFinChartOfAccountDescription());
            }

            KualiDecimal originalPayrollAmount = detailLine.getEffortCertificationOriginalPayrollAmount();
            String actualOriginalPercent = PayrollAmountHolder.recalculateEffortPercentAsString(totalOriginalPayrollAmount, originalPayrollAmount);
            fieldInfoForAttribute.put(EffortPropertyConstants.EFFORT_CERTIFICATION_CALCULATED_OVERALL_PERCENT, actualOriginalPercent);

            fieldInfo.add(fieldInfoForAttribute);
        }

        return fieldInfo;
    }


    /**
     * get the inquirable implmentation
     *
     * @return the inquirable implmentation
     */
    protected Inquirable getInquirable() {
        return new EffortLedgerBalanceInquirableImpl();
    }

    /**
     * get the EffortPositionDataDetailsInquirableImpl implmentation
     *
     * @return the EffortPositionDataDetailsInquirableImpl implmentation
     */
    protected EffortPositionDataDetailsInquirableImpl getEffortPositionDataDetailsInquirableImpl() {
        return new EffortPositionDataDetailsInquirableImpl();
    }

    /**
     * build the descriptive information of the given account. The information includes account name and project director's name if
     * any
     *
     * @param chartOfAccountsCode the given chart of accounts code
     * @param accountNumber       the given account number
     * @return the descriptive information of the given account
     */
    public static String buildAccountInfo(Account account) {
        if (ObjectUtils.isNull(account)) {
            return KFSConstants.EMPTY_STRING;
        }

        String projectDirectorName = KFSConstants.EMPTY_STRING;

        try {
            ContractsAndGrantsModuleService contractsAndGrantsModuleService = SpringContext.getBean(ContractsAndGrantsModuleService.class);
            Person projectDirector = contractsAndGrantsModuleService.getProjectDirectorForAccount(account);

            projectDirectorName = projectDirector != null ? MessageFormat.format("  ({0})", projectDirector.getName()) : KFSConstants.EMPTY_STRING;
        } catch (Exception e) {
            LOG.error("Cannot find a project director for the account:" + account);
        }

        return MessageFormat.format("{0}{1}", account.getAccountName(), projectDirectorName);
    }

    /**
     * load the descriptive information of the given account. This method is used by DWR.
     *
     * @param chartOfAccountsCode the given chart of accounts code
     * @param accountNumber       the given account number
     * @return the descriptive information of the given account
     */
    public static String loadAccountInfo(String chartOfAccountsCode, String accountNumber) {
        Account account = SpringContext.getBean(AccountService.class).getByPrimaryIdWithCaching(chartOfAccountsCode, accountNumber);

        return buildAccountInfo(account);
    }
}
