/**
 * Copyright 2005-2018 The Kuali Foundation
 *
 * Licensed under the Educational Community 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.opensource.org/licenses/ecl2.php
 *
 * 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.kuali.rice.krad.uif.lifecycle.initialize;

import org.apache.commons.lang.StringUtils;
import org.kuali.rice.krad.datadictionary.AttributeDefinition;
import org.kuali.rice.krad.service.DataDictionaryService;
import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
import org.kuali.rice.krad.uif.UifConstants;
import org.kuali.rice.krad.uif.UifPropertyPaths;
import org.kuali.rice.krad.uif.component.BindingInfo;
import org.kuali.rice.krad.uif.field.DataField;
import org.kuali.rice.krad.uif.field.InputField;
import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle;
import org.kuali.rice.krad.uif.lifecycle.ViewLifecycleTaskBase;
import org.kuali.rice.krad.uif.util.ComponentFactory;
import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
import org.kuali.rice.krad.uif.view.ExpressionEvaluator;
import org.kuali.rice.krad.uif.view.View;
import org.kuali.rice.krad.util.KRADConstants;

import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

/**
 * Performs initialization on data fields based on attributes found in the data dictionary.
 * 
 * @author Kuali Rice Team (rice.collab@kuali.org)
 */
public class InitializeDataFieldFromDictionaryTask extends ViewLifecycleTaskBase<DataField> {

    /**
     * Constructor.
     * 
     * @param phase The initialize phase for the data field.
     */
    public InitializeDataFieldFromDictionaryTask() {
        super(DataField.class);
    }

    /**
     * Sets properties of the <code>InputField</code> (if blank) to the corresponding attribute
     * entry in the data dictionary
     *
     * {@inheritDoc}
     */
    @Override
    protected void performLifecycleTask() {
        DataField field = (DataField) getElementState().getElement();

        AttributeDefinition attributeDefinition = null;

        String dictionaryAttributeName = field.getDictionaryAttributeName();
        String dictionaryObjectEntry = field.getDictionaryObjectEntry();

        Map<String, String> propertyExpressions = field.getPropertyExpressions();

        if (dictionaryAttributeName == null && propertyExpressions.containsKey(UifPropertyPaths.DICTIONARY_ATTR_NAME)) {
            ExpressionEvaluator expressionEvaluator = ViewLifecycle.getExpressionEvaluator();
            dictionaryAttributeName = propertyExpressions.get(UifPropertyPaths.DICTIONARY_ATTR_NAME);

            if (dictionaryAttributeName.contains(UifConstants.DEFAULT_PATH_BIND_ADJUST_PREFIX)) {
                dictionaryAttributeName = dictionaryAttributeName.replace(UifConstants.DEFAULT_PATH_BIND_ADJUST_PREFIX,
                        "");
            } else if (dictionaryAttributeName.contains(UifConstants.LINE_PATH_BIND_ADJUST_PREFIX)) {
                // It is not possible to add both the collection and index to the context at this time.  Until
                // the dictionaryAttributeName expression can be properly evaluated, get the attribute definition for
                // the field by looking at the first item in the collection if an item exists.

                List<Object> collection = ObjectPropertyUtils.getPropertyValue(ViewLifecycle.getModel(),
                       field.getBindingInfo().getCollectionPath());

                if (!collection.isEmpty()) {
                    dictionaryAttributeName = dictionaryAttributeName.replace(UifConstants.LINE_PATH_BIND_ADJUST_PREFIX,
                            field.getBindingInfo().getCollectionPath() + "[0].");
                } else {
                    dictionaryAttributeName = null;
                }
            }

            dictionaryAttributeName = (String) expressionEvaluator.evaluateExpression(field.getContext(),
                    dictionaryAttributeName);
        }

        if (!(dictionaryAttributeName == null && propertyExpressions.containsKey(UifPropertyPaths.DICTIONARY_ATTR_NAME))) {

            // if entry given but not attribute name, use field name as attribute name
            if (StringUtils.isNotBlank(dictionaryObjectEntry) && StringUtils.isBlank(dictionaryAttributeName)) {
                dictionaryAttributeName = field.getPropertyName();
            }

            // if dictionary entry and attribute set, attempt to find definition
            if (StringUtils.isNotBlank(dictionaryAttributeName) && StringUtils.isNotBlank(dictionaryObjectEntry)) {
                attributeDefinition = KRADServiceLocatorWeb.getDataDictionaryService().getAttributeDefinition(
                        dictionaryObjectEntry, dictionaryAttributeName);
            }

            // if definition not found, recurse through path
            if (attributeDefinition == null) {
                BindingInfo fieldBindingInfo = field.getBindingInfo();
                String collectionPath = fieldBindingInfo.getCollectionPath();

                String propertyPath;
                if (StringUtils.isNotBlank(collectionPath)) {
                    StringBuilder propertyPathBuilder = new StringBuilder();

                    String bindingObjectPath = fieldBindingInfo.getBindingObjectPath();
                    if (StringUtils.isNotBlank(bindingObjectPath)) {
                        propertyPathBuilder.append(bindingObjectPath).append('.');
                    }

                    propertyPathBuilder.append(collectionPath).append('.');

                    String bindByNamePrefix = fieldBindingInfo.getBindByNamePrefix();
                    if (StringUtils.isNotBlank(bindByNamePrefix)) {

                        // fix for both collectionPath and bindByNamePrefix being set,
                        // wherein the bindByNamePrefix contains the collectionPath
                        if (!bindByNamePrefix.startsWith(collectionPath)) {
                            propertyPathBuilder.append(bindByNamePrefix).append('.');
                        }
                    }

                    propertyPathBuilder.append(fieldBindingInfo.getBindingName());
                    propertyPath = propertyPathBuilder.toString();
                } else {
                    propertyPath = field.getBindingInfo().getBindingPath();
                }

                attributeDefinition = findNestedDictionaryAttribute(propertyPath);
            }

            // if a definition was found, initialize field from definition
            if (attributeDefinition != null) {
                field.copyFromAttributeDefinition(attributeDefinition);
            }
        }

        // if control still null, assign default
        if (field instanceof InputField) {
            InputField inputField = (InputField) field;
            if (inputField.getControl() == null) {
                inputField.setControl(ComponentFactory.getTextControl());
            }
        }
    }

    /**
     * Determines the name of a data dictionary entry based on the portion of the path leading up to
     * the attribute name.
     * 
     * <p>
     * The property path passed in is checked first against
     * {@link View#getObjectPathToConcreteClassMapping()} for a full or partial match. If no match
     * is found then property type relative to the model involved in the current view lifecycle is
     * returned, where applicable.
     * </p>
     * 
     * @param dictionaryEntryPrefix Portion of a property path referring to the entry that has the
     *        attribute.
     * @return The name of the dictionary entry indicated by the property path.
     */
    private String getDictionaryEntryName(String dictionaryEntryPrefix) {
        if (StringUtils.isEmpty(dictionaryEntryPrefix)) {
            return dictionaryEntryPrefix;
        }
        
        Map<String, Class<?>> modelClasses = ViewLifecycle.getView().getObjectPathToConcreteClassMapping();
        Class<?> dictionaryModelClass = modelClasses.get(dictionaryEntryPrefix);

        // full match
        if (dictionaryModelClass != null) {
            return dictionaryModelClass.getName();
        }

        // in case of partial match, holds the class that matched and the
        // property so we can get by reflection
        Class<?> modelClass = null;
        String modelProperty = dictionaryEntryPrefix;

        int bestMatchLength = 0;
        int modelClassPathLength = dictionaryEntryPrefix.length();

        // check if property path matches one of the modelClass entries
        synchronized (modelClasses) {
            // synchronizing on modelClasses prevents ConcurrentModificationException during
            // asynchronous lifecycle processing
            for (Entry<String, Class<?>> modelClassEntry : modelClasses.entrySet()) {
                String path = modelClassEntry.getKey();
                int pathlen = path.length();

                if (dictionaryEntryPrefix.startsWith(path) && pathlen > bestMatchLength
                        && modelClassPathLength > pathlen && dictionaryEntryPrefix.charAt(pathlen) == '.') {
                    bestMatchLength = pathlen;
                    modelClass = modelClassEntry.getValue();
                    modelProperty = dictionaryEntryPrefix.substring(pathlen + 1);
                }
            }
        }

        if (modelClass != null) {
            // if a partial match was found, look up the property type based on matched model class
            dictionaryModelClass = ObjectPropertyUtils.getPropertyType(modelClass, modelProperty);
        }

        if (dictionaryModelClass == null) {
            // If no full or partial match, look up based on the model directly
            dictionaryModelClass = ObjectPropertyUtils.getPropertyType(ViewLifecycle.getModel(), dictionaryEntryPrefix);
        }

        return dictionaryModelClass == null ? null : dictionaryModelClass.getName();
    }

    /**
     * Recursively drills down the property path (if nested) to find an AttributeDefinition, the
     * first attribute definition found will be returned
     * 
     * <p>
     * e.g. suppose parentPath is 'document' and propertyPath is 'account.subAccount.name', first
     * the property type for document will be retrieved using the view metadata and used as the
     * dictionary entry, with the propertyPath as the dictionary attribute, if an attribute
     * definition exists it will be returned. Else, the first part of the property path is added to
     * the parent, making the parentPath 'document.account' and the propertyPath 'subAccount.name',
     * the method is then called again to perform the process with those parameters. The recursion
     * continues until an attribute field is found, or the propertyPath is no longer nested
     * </p>
     * 
     * @param propertyPath path of the property to use as dictionary attribute and to drill down on
     * @return AttributeDefinition if found, or Null
     */
    protected AttributeDefinition findNestedDictionaryAttribute(String propertyPath) {
        DataField field = (DataField) getElementState().getElement();

        String fieldBindingPrefix = null;
        String dictionaryAttributePath = propertyPath;

        if (field.getBindingInfo().isBindToMap()) {
            fieldBindingPrefix = "";
            if (!field.getBindingInfo().isBindToForm() && StringUtils.isNotBlank(
                    field.getBindingInfo().getBindingObjectPath())) {
                fieldBindingPrefix = field.getBindingInfo().getBindingObjectPath();
            }
            if (StringUtils.isNotBlank(field.getBindingInfo().getBindByNamePrefix())) {
                if (StringUtils.isNotBlank(fieldBindingPrefix)) {
                    fieldBindingPrefix += "." + field.getBindingInfo().getBindByNamePrefix();
                } else {
                    fieldBindingPrefix = field.getBindingInfo().getBindByNamePrefix();
                }
            }

            dictionaryAttributePath = field.getBindingInfo().getBindingName();
        }

        if (StringUtils.isEmpty(dictionaryAttributePath)) {
            return null;
        }

        if (StringUtils.startsWith(dictionaryAttributePath, KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX)) {
            dictionaryAttributePath = StringUtils.substringAfter(dictionaryAttributePath, KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX);
        }
        
        DataDictionaryService ddService = KRADServiceLocatorWeb.getDataDictionaryService();
        
        String dictionaryAttributeName = ObjectPropertyUtils.getCanonicalPath(dictionaryAttributePath);
        String dictionaryEntryPrefix = fieldBindingPrefix;
        
        AttributeDefinition attribute = null;
        String dictionaryEntryName = null;
        
        int i = dictionaryAttributeName.indexOf('.');
        while (attribute == null && i != -1) {
            
            if (dictionaryEntryPrefix != null) {
                dictionaryEntryName = getDictionaryEntryName(dictionaryEntryPrefix);
                dictionaryEntryPrefix += '.' + dictionaryAttributeName.substring(0, i);
            } else {
                dictionaryEntryName = null;
                dictionaryEntryPrefix = dictionaryAttributeName.substring(0, i);
            }

            if (dictionaryEntryName != null) {
                attribute = ddService.getAttributeDefinition(dictionaryEntryName, dictionaryAttributeName);
            }
            
            if (attribute == null) {
                dictionaryAttributeName = dictionaryAttributeName.substring(i+1);
                i = dictionaryAttributeName.indexOf('.');
            }
        }
        
        if (attribute == null && dictionaryEntryPrefix != null) {
            dictionaryEntryName = getDictionaryEntryName(dictionaryEntryPrefix);
            
            if (dictionaryEntryName != null) {
                attribute = ddService.getAttributeDefinition(dictionaryEntryName, dictionaryAttributeName);
            }
        }
        
        // if a definition was found, update the fields dictionary properties
        if (attribute != null) {
            field.setDictionaryObjectEntry(dictionaryEntryName);
            field.setDictionaryAttributeName(dictionaryAttributeName);
        }

        return attribute;
    }

}
