/*
 * 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.gl.businessobject.inquiry;

import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.gl.Constant;
import org.kuali.kfs.gl.businessobject.AccountBalance;
import org.kuali.kfs.gl.businessobject.lookup.BusinessObjectFieldConverter;
import org.kuali.kfs.kns.inquiry.KualiInquirableImpl;
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.kns.service.BusinessObjectDictionaryService;
import org.kuali.kfs.kns.service.DataDictionaryService;
import org.kuali.kfs.krad.datadictionary.AttributeDefinition;
import org.kuali.kfs.krad.datadictionary.DataDictionaryEntryBase;
import org.kuali.kfs.krad.service.PersistenceStructureService;
import org.kuali.kfs.krad.util.KRADConstants;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.krad.util.UrlFactory;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.rice.kew.api.doctype.DocumentType;
import org.kuali.rice.kew.api.doctype.DocumentTypeService;
import org.kuali.rice.kew.doctype.bo.DocumentTypeEBO;
import org.kuali.rice.krad.bo.BusinessObject;

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

/**
 * This class is the template class for the customized inqurable implementations used to generate balance inquiry screens.
 */
public abstract class AbstractGeneralLedgerInquirableImpl extends KualiInquirableImpl {
    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(AbstractGeneralLedgerInquirableImpl.class);

    private BusinessObject businessObject;

    /**
     * @see org.kuali.kfs.kns.inquiry.KualiInquirableImpl#getInquiryUrl(org.kuali.rice.krad.bo.BusinessObject, java.lang.String, boolean)
     */
    @Override
    public HtmlData getInquiryUrl(BusinessObject businessObject, String attributeName, boolean forceInquiry) {
        return this.getInquiryUrl(businessObject, attributeName);
    }

    /**
     * Helper method to build an inquiry url for a result field.
     *
     * @param businessObject the business object instance to build the urls for
     * @param attributeName  the attribute name which links to an inquirable
     * @return String url to inquiry
     */
    public HtmlData getInquiryUrl(BusinessObject businessObject, String attributeName) {
        this.setBusinessObject(businessObject);
        AnchorHtmlData inquiryHref = new AnchorHtmlData(KRADConstants.EMPTY_STRING, KRADConstants.EMPTY_STRING);
        BusinessObjectDictionaryService businessDictionary = SpringContext.getBean(BusinessObjectDictionaryService.class);
        PersistenceStructureService persistenceStructureService = SpringContext.getBean(PersistenceStructureService.class);

        String baseUrl = KFSConstants.INQUIRY_ACTION;
        Properties parameters = new Properties();
        parameters.put(KFSConstants.DISPATCH_REQUEST_PARAMETER, KFSConstants.START_METHOD);

        Object attributeValue = null;
        Class inquiryBusinessObjectClass = null;
        String attributeRefName = "";
        boolean isPkReference = false;

        Map userDefinedAttributeMap = getUserDefinedAttributeMap();
        boolean isUserDefinedAttribute = userDefinedAttributeMap == null ? false : userDefinedAttributeMap.containsKey(attributeName);

        // determine the type of the given attribute: user-defined, regular, nested-referenced or primitive reference
        if (isUserDefinedAttribute) {
            attributeName = getAttributeName(attributeName);
            inquiryBusinessObjectClass = getInquiryBusinessObjectClass(attributeName);
            isPkReference = true;
        } else if (attributeName.equals(businessDictionary.getTitleAttribute(businessObject.getClass()))) {
            inquiryBusinessObjectClass = businessObject.getClass();
            isPkReference = true;
        } else if (ObjectUtils.isNestedAttribute(attributeName)) {
            if (!"financialObject.financialObjectType.financialReportingSortCode".equals(attributeName)) {
                inquiryBusinessObjectClass = LookupUtils.getNestedReferenceClass(businessObject, attributeName);
            } else {
                return inquiryHref;
            }
        } else {
            Map primitiveReference = LookupUtils.getPrimitiveReference(businessObject, attributeName);
            if (primitiveReference != null && !primitiveReference.isEmpty()) {
                attributeRefName = (String) primitiveReference.keySet().iterator().next();
                inquiryBusinessObjectClass = (Class) primitiveReference.get(attributeRefName);
            }
            attributeValue = ObjectUtils.getPropertyValue(businessObject, attributeName);
            attributeValue = (attributeValue == null) ? "" : attributeValue.toString();
        }

        // process the business object class if the attribute name is not user-defined
        if (!isUserDefinedAttribute) {
            if (isExclusiveField(attributeName, attributeValue)) {
                return inquiryHref;
            }

            if (inquiryBusinessObjectClass == null || businessDictionary.isInquirable(inquiryBusinessObjectClass) == null || !businessDictionary.isInquirable(inquiryBusinessObjectClass).booleanValue()) {
                return inquiryHref;
            }
        }
        parameters.put(KFSConstants.BUSINESS_OBJECT_CLASS_ATTRIBUTE, inquiryBusinessObjectClass.getName());

        List keys = new ArrayList();
        Map<String, String> inquiryFields = new HashMap<String, String>();
        if (isUserDefinedAttribute) {
            baseUrl = getBaseUrl();
            keys = buildUserDefinedAttributeKeyList();

            parameters.put(KFSConstants.RETURN_LOCATION_PARAMETER, Constant.RETURN_LOCATION_VALUE);
            parameters.put(KFSConstants.GL_BALANCE_INQUIRY_FLAG, "true");
            parameters.put(KFSConstants.DISPATCH_REQUEST_PARAMETER, KFSConstants.SEARCH_METHOD);
            parameters.put(KFSConstants.DOC_FORM_KEY, "88888888");

            // add more customized parameters into the current parameter map
            addMoreParameters(parameters, attributeName);
        } else if (persistenceStructureService.isPersistable(inquiryBusinessObjectClass)) {
            keys = persistenceStructureService.listPrimaryKeyFieldNames(inquiryBusinessObjectClass);
        }

        // build key value url parameters used to retrieve the business object
        if (keys != null) {
            for (Iterator keyIterator = keys.iterator(); keyIterator.hasNext(); ) {
                String keyName = (String) keyIterator.next();

                // convert the key names based on their formats and types
                String keyConversion = keyName;
                if (ObjectUtils.isNestedAttribute(attributeName)) {
                    if (isUserDefinedAttribute) {
                        keyConversion = keyName;
                    } else {
                        keyConversion = ObjectUtils.getNestedAttributePrefix(attributeName) + "." + keyName;
                    }
                } else {
                    if (isPkReference) {
                        keyConversion = keyName;
                    } else {
                        keyConversion = persistenceStructureService.getForeignKeyFieldName(businessObject.getClass(), attributeRefName, keyName);
                    }
                }

                Object keyValue = ObjectUtils.getPropertyValue(businessObject, keyConversion);
                keyValue = (keyValue == null) ? "" : keyValue.toString();

                // convert the key value and name into the given ones
                Object tempKeyValue = this.getKeyValue(keyName, keyValue);
                keyValue = tempKeyValue == null ? keyValue : tempKeyValue;

                String tempKeyName = this.getKeyName(keyName);
                keyName = tempKeyName == null ? keyName : tempKeyName;

                // add the key-value pair into the parameter map
                if (keyName != null) {
                    parameters.put(keyName, keyValue);
                    inquiryFields.put(keyName, keyValue.toString());
                }
            }
        }

        // Hack to make this work. I don't know why it doesn't pick up the whole primary key for these. The last big change to
        // KualiInquirableImpl
        // broke all of this
        if (businessObject instanceof AccountBalance) {
            AccountBalance ab = (AccountBalance) businessObject;
            if ("financialObject.financialObjectLevel.financialConsolidationObject.finConsolidationObjectCode".equals(attributeName)) {
                parameters.put(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, ab.getChartOfAccountsCode());
            } else if ("financialObject.financialObjectLevel.financialObjectLevelCode".equals(attributeName)) {
                parameters.put(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, ab.getChartOfAccountsCode());
            }
        }

        return getHyperLink(inquiryBusinessObjectClass, inquiryFields, UrlFactory.parameterizeUrl(baseUrl, parameters));
    }

    /**
     * This method builds the inquiry url for user-defined attribute
     *
     * @return a List of attribute keys for the inquiry url
     */
    protected abstract List buildUserDefinedAttributeKeyList();

    /**
     * This method defines the user-defined attribute map
     *
     * @return the user-defined attribute map
     */
    protected abstract Map getUserDefinedAttributeMap();

    /**
     * This method finds the matching attribute name of given one
     *
     * @param attributeName the given attribute name
     * @return the attribute name from the given one
     */
    protected abstract String getAttributeName(String attributeName);

    /**
     * This method finds the matching the key value of the given one
     *
     * @param keyName  the given key name
     * @param keyValue the given key value
     * @return the key value from the given key value
     */
    protected abstract Object getKeyValue(String keyName, Object keyValue);

    /**
     * This method finds the matching the key name of the given one
     *
     * @param keyName the given key name
     * @return the key value from the given key name
     */
    protected abstract String getKeyName(String keyName);

    /**
     * This method defines the lookupable implementation attribute name
     *
     * @return the lookupable implementation attribute name
     */
    protected abstract String getLookupableImplAttributeName();

    /**
     * This method defines the base inquiry url
     *
     * @return the base inquiry url
     */
    protected abstract String getBaseUrl();

    /**
     * This method gets the class name of the inquiry business object for a given attribute.
     *
     * @return the class name of the inquiry business object for a given attribute
     */
    protected abstract Class getInquiryBusinessObjectClass(String attributeName);

    /**
     * This method adds more parameters into the curren parameter map
     *
     * @param parameter the current parameter map
     */
    protected abstract void addMoreParameters(Properties parameter, String attributeName);

    /**
     * This method determines whether the input name-value pair is exclusive from the processing
     *
     * @param keyName  the name of the name-value pair
     * @param keyValue the value of the name-value pair
     * @return true if the input key is in the exclusive list; otherwise, false
     */
    protected boolean isExclusiveField(Object keyName, Object keyValue) {

        if (keyName != null && keyValue != null) {
            String convertedKeyName = BusinessObjectFieldConverter.convertFromTransactionPropertyName(keyName.toString());

            if (convertedKeyName.equals(KFSPropertyConstants.SUB_ACCOUNT_NUMBER) && keyValue.equals(Constant.CONSOLIDATED_SUB_ACCOUNT_NUMBER)) {
                return true;
            } else if (convertedKeyName.equals(KFSPropertyConstants.SUB_OBJECT_CODE) && keyValue.equals(Constant.CONSOLIDATED_SUB_OBJECT_CODE)) {
                return true;
            } else if (convertedKeyName.equals(KFSPropertyConstants.OBJECT_TYPE_CODE) && keyValue.equals(Constant.CONSOLIDATED_OBJECT_TYPE_CODE)) {
                return true;
            }
            if (convertedKeyName.equals(KFSPropertyConstants.SUB_ACCOUNT_NUMBER) && keyValue.equals(KFSConstants.getDashSubAccountNumber())) {
                return true;
            } else if (convertedKeyName.equals(KFSPropertyConstants.SUB_OBJECT_CODE) && keyValue.equals(KFSConstants.getDashFinancialSubObjectCode())) {
                return true;
            } else if (convertedKeyName.equals(KFSPropertyConstants.PROJECT_CODE) && keyValue.equals(KFSConstants.getDashProjectCode())) {
                return true;
            }
        }
        return false;
    }

    /**
     * This method recovers the values of the given keys
     *
     * @param fieldValues unconsolidated values
     * @param keyName     a key name that may be in the fieldValues map
     * @param keyValue    a key value that may be in the fieldValues map
     * @return the original value for a previously consolidated value
     */
    protected String recoverFieldValueFromConsolidation(Map fieldValues, Object keyName, Object keyValue) {
        if (fieldValues == null || keyName == null || keyValue == null) {
            return Constant.EMPTY_STRING;
        }

        Map convertedFieldValues = BusinessObjectFieldConverter.convertFromTransactionFieldValues(fieldValues);
        String convertedKeyName = BusinessObjectFieldConverter.convertFromTransactionPropertyName(keyName.toString());

        if (convertedKeyName.equals(KFSPropertyConstants.SUB_ACCOUNT_NUMBER) && keyValue.equals(Constant.CONSOLIDATED_SUB_ACCOUNT_NUMBER)) {
            return this.getValueFromFieldValues(convertedFieldValues, keyName);
        } else if (convertedKeyName.equals(KFSPropertyConstants.SUB_OBJECT_CODE) && keyValue.equals(Constant.CONSOLIDATED_SUB_OBJECT_CODE)) {
            return this.getValueFromFieldValues(convertedFieldValues, keyName);
        } else if (convertedKeyName.equals(KFSPropertyConstants.OBJECT_TYPE_CODE) && keyValue.equals(Constant.CONSOLIDATED_OBJECT_TYPE_CODE)) {
            return this.getValueFromFieldValues(convertedFieldValues, keyName);
        }

        return Constant.EMPTY_STRING;
    }

    /**
     * Utility method to get the value of the given key from the field values
     *
     * @param fieldValues a Map of key values
     * @param keyName     the name of the key to retrieve the value from
     * @return the value for the key, or, if not found, an empty String
     */
    private String getValueFromFieldValues(Map fieldValues, Object keyName) {
        String keyValue = Constant.EMPTY_STRING;

        if (fieldValues.containsKey(keyName)) {
            keyValue = (String) fieldValues.get(keyName);
        }
        return keyValue;
    }

    /**
     * This takes a map of field values and then returns it without processing it, making this a sort
     * of identity method for Maps
     *
     * @param fieldValues field values to return to the user
     * @return the Map you sent in as a parameter
     */
    public Map getFieldValues(Map fieldValues) {
        return fieldValues;
    }

    /**
     * Given the nested name of an attribute in an object, returns the class that attribute will return
     *
     * @param businessObject the business object to find the propery class for
     * @param attributeName  the nested name of the attribute to find the class for
     * @return the class of the nested attribute
     */
    public Class getNestedInquiryBusinessObjectClass(BusinessObject businessObject, String attributeName) {
        // TODO: not finished
        Class inquiryBusinessObjectClass = null;
        String entryName = businessObject.getClass().getName();
        if (LOG.isDebugEnabled()) {
            LOG.debug("businessObject: " + entryName);
            LOG.debug("attributeName: " + attributeName);
        }
        DataDictionaryService dataDictionary = SpringContext.getBean(DataDictionaryService.class);
        AttributeDefinition attributeDefinition = null;

        if (StringUtils.isBlank(attributeName)) {
            throw new IllegalArgumentException("invalid (blank) attributeName");
        }

        DataDictionaryEntryBase entry = (DataDictionaryEntryBase) dataDictionary.getDataDictionary().getDictionaryObjectEntry(entryName);
        if (entry != null) {
            attributeDefinition = entry.getAttributeDefinition(attributeName);
            inquiryBusinessObjectClass = LookupUtils.getNestedReferenceClass(businessObject, attributeName);
        }

        // TODO: need to get class for which this property belongs
//        if (attributeDefinition instanceof AttributeReferenceDefinition) {
//            AttributeReferenceDefinition attributeReferenceDefinition = (AttributeReferenceDefinition) attributeDefinition;
//            LOG.debug("Source Classname = " + attributeReferenceDefinition.getSourceClassName());
//            LOG.debug("Source Attribute = " + attributeReferenceDefinition.getSourceAttributeName());
//
//            try {
//                inquiryBusinessObjectClass = Class.forName(attributeReferenceDefinition.getSourceClassName());
//            }
//            catch (Exception e) {
//                throw new IllegalArgumentException("fail to construct a Class");
//            }
//        }

        return inquiryBusinessObjectClass;
    }

    /**
     * Builds URL to document type inquiry based on a given document type code
     *
     * @param docTypeCode - document type code to inquiry on
     * @return {@link HtmlData} representing inquiry URL
     */
    protected HtmlData getDocTypeInquiryUrl(String docTypeCode) {
        DocumentType docTypeDTO = SpringContext.getBean(DocumentTypeService.class).getDocumentTypeByName(docTypeCode);
        if (docTypeDTO == null) {
            return new AnchorHtmlData();
        }
        String baseUrl = KFSConstants.INQUIRY_ACTION;

        Properties parameters = new Properties();
        parameters.put(KFSConstants.DISPATCH_REQUEST_PARAMETER, KFSConstants.START_METHOD);
        parameters.put(KFSConstants.BUSINESS_OBJECT_CLASS_ATTRIBUTE, DocumentTypeEBO.class.getName());
        parameters.put(KFSConstants.DOC_FORM_KEY, "88888888");
        parameters.put(KFSPropertyConstants.DOCUMENT_TYPE_ID, docTypeDTO.getId().toString());

        Map<String, String> inquiryFields = new HashMap<String, String>();
        inquiryFields.put(KFSPropertyConstants.DOCUMENT_TYPE_ID, docTypeDTO.getId().toString());

        return getHyperLink(DocumentTypeEBO.class, inquiryFields, UrlFactory.parameterizeUrl(baseUrl, parameters));
    }

    /**
     * Gets the businessObject attribute.
     *
     * @return Returns the businessObject.
     */
    public BusinessObject getBusinessObject() {
        return businessObject;
    }

    /**
     * Sets the businessObject attribute value.
     *
     * @param businessObject The businessObject to set.
     */
    public void setBusinessObject(BusinessObject businessObject) {
        this.businessObject = businessObject;
    }
}
