/**
 * 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.widget;

import java.io.Serializable;
import java.util.List;

import org.apache.commons.lang.StringUtils;
import org.kuali.rice.krad.datadictionary.parse.BeanTag;
import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
import org.kuali.rice.krad.uif.component.BindingInfo;
import org.kuali.rice.krad.uif.component.MethodInvokerConfig;
import org.kuali.rice.krad.uif.field.AttributeQuery;
import org.kuali.rice.krad.uif.field.InputField;
import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle;
import org.kuali.rice.krad.uif.util.LifecycleElement;
import org.kuali.rice.krad.uif.util.ScriptUtils;
import org.kuali.rice.krad.uif.view.View;

/**
 * Widget that provides dynamic select options to the user as they are entering the value (also known
 * as auto-complete).
 *
 * <p>Widget is backed by an {@link org.kuali.rice.krad.uif.field.AttributeQuery} that provides the configuration
 * for executing a query server side that will retrieve the valid option values.</p>
 *
 * @author Kuali Rice Team (rice.collab@kuali.org)
 */
@BeanTag(name = "suggest", parent = "Uif-Suggest")
public class Suggest extends WidgetBase {
    private static final long serialVersionUID = 7373706855319347225L;

    private AttributeQuery suggestQuery;

    private String valuePropertyName;
    private String labelPropertyName;
    private List<String> additionalPropertiesToReturn;

    private boolean returnFullQueryObject;

    private boolean retrieveAllSuggestions;
    private List<Object> suggestOptions;

    private String suggestOptionsJsString;

    public Suggest() {
        super();
    }

    /**
     * The following updates are done here:
     *
     * <ul>
     * <li>Invoke expression evaluation on the suggestQuery</li>
     * </ul>
     *
     * {@inheritDoc}
     */
    public void performApplyModel(Object model, LifecycleElement parent) {
        super.performApplyModel(model, parent);

        if (suggestQuery != null) {
            ViewLifecycle.getExpressionEvaluator().evaluateExpressionsOnConfigurable(ViewLifecycle.getView(),
                    suggestQuery, getContext());
        }
    }

    /**
     * The following actions are performed:
     *
     * <ul>
     * <li>Adjusts the query field mappings on the query based on the binding configuration of the field</li>
     * <li>TODO: determine query if render is true and query is not set</li>
     * </ul>
     *
     * {@inheritDoc}
     */
    @Override
    public void performFinalize(Object model, LifecycleElement parent) {
        super.performFinalize(model, parent);

        // check for necessary configuration
        if (!isSuggestConfigured()) {
            setRender(false);
        }

        if (!isRender()) {
            return;
        }

        if (retrieveAllSuggestions) {
            if (suggestOptions == null || suggestOptions.isEmpty()) {
                // execute query method to retrieve up front suggestions
                if (suggestQuery.hasConfiguredMethod()) {
                    retrieveSuggestOptions(ViewLifecycle.getView());
                }
            } else {
                suggestOptionsJsString = ScriptUtils.translateValue(suggestOptions);
            }
        } else {
            // adjust from side on query field mapping to match parent fields path
            InputField field = (InputField) parent;

            BindingInfo bindingInfo = field.getBindingInfo();
            suggestQuery.updateQueryFieldMapping(bindingInfo);

            if (suggestQuery != null) {
                suggestQuery.defaultQueryTarget(ViewLifecycle.getHelper());
            }
        }
    }

    /**
     * Indicates whether the suggest widget has the necessary configuration to render
     *
     * @return true if the necessary configuration is present, false if not
     */
    public boolean isSuggestConfigured() {
        if (StringUtils.isNotBlank(valuePropertyName) || suggestQuery.hasConfiguredMethod() ||
                (suggestOptions != null && !suggestOptions.isEmpty())) {
            return true;
        }

        return false;
    }

    /**
     * Invokes the configured query method and sets the returned method value as the suggest options or
     * suggest options JS string
     *
     * @param view view instance the suggest belongs to, used to get the view helper service if needed
     */
    protected void retrieveSuggestOptions(View view) {
        String queryMethodToCall = suggestQuery.getQueryMethodToCall();
        MethodInvokerConfig queryMethodInvoker = suggestQuery.getQueryMethodInvokerConfig();

        if (queryMethodInvoker == null) {
            queryMethodInvoker = new MethodInvokerConfig();
        }

        // if method not set on invoker, use queryMethodToCall, note staticMethod could be set(don't know since
        // there is not a getter), if so it will override the target method in prepare
        if (StringUtils.isBlank(queryMethodInvoker.getTargetMethod())) {
            queryMethodInvoker.setTargetMethod(queryMethodToCall);
        }

        // if target class or object not set, use view helper service
        if ((queryMethodInvoker.getTargetClass() == null) && (queryMethodInvoker.getTargetObject() == null)) {
            queryMethodInvoker.setTargetObject(view.getViewHelperService());
        }

        try {
            queryMethodInvoker.prepare();

            Object methodResult = queryMethodInvoker.invoke();
            if (methodResult instanceof String) {
                suggestOptionsJsString = (String) methodResult;
            } else if (methodResult instanceof List) {
                suggestOptions = (List<Object>) methodResult;
                suggestOptionsJsString = ScriptUtils.translateValue(suggestOptions);
            } else {
                throw new RuntimeException("Suggest query method did not return List<String> for suggestions");
            }
        } catch (Exception e) {
            throw new RuntimeException("Unable to invoke query method: " + queryMethodInvoker.getTargetMethod(), e);
        }
    }

    /**
     * Returns object containing post data to store for the suggest request.
     *
     * @return suggest post data instance
     */
    public SuggestPostData getPostData() {
        return new SuggestPostData(this);
    }

    /**
     * Attribute query instance the will be executed to provide
     * the suggest options
     *
     * @return AttributeQuery
     */
    @BeanTagAttribute(type = BeanTagAttribute.AttributeType.DIRECTORBYTYPE)
    public AttributeQuery getSuggestQuery() {
        return suggestQuery;
    }

    /**
     * Setter for the suggest attribute query
     *
     * @param suggestQuery
     */
    public void setSuggestQuery(AttributeQuery suggestQuery) {
        this.suggestQuery = suggestQuery;
    }

    /**
     * Name of the property on the query result object that provides
     * the options for the suggest, values from this field will be
     * collected and sent back on the result to provide as suggest options.
     *
     * <p>If a labelPropertyName is also set,
     * the property specified by it will be used as the label the user selects (the suggestion), but the value will
     * be the value retrieved by this property.  If only one of labelPropertyName or valuePropertyName is set,
     * the property's value on the object will be used for both the value inserted on selection and the suggestion
     * text (most default cases only a valuePropertyName would be set).</p>
     *
     * @return source property name
     */
    @BeanTagAttribute
    public String getValuePropertyName() {
        return valuePropertyName;
    }

    /**
     * Setter for the value property name
     *
     * @param valuePropertyName
     */
    public void setValuePropertyName(String valuePropertyName) {
        this.valuePropertyName = valuePropertyName;
    }

    /**
     * Name of the property on the query result object that provides the label for the suggestion.
     *
     * <p>This should
     * be set when the label that the user selects is different from the value that is inserted when a user selects a
     * suggestion. If only one of labelPropertyName or valuePropertyName is set,
     * the property's value on the object will be used for both the value inserted on selection and the suggestion
     * text (most default cases only a valuePropertyName would be set).</p>
     *
     * @return labelPropertyName representing the property to use for the suggestion label of the item
     */
    @BeanTagAttribute
    public String getLabelPropertyName() {
        return labelPropertyName;
    }

    /**
     * Set the labelPropertyName
     *
     * @param labelPropertyName
     */
    public void setLabelPropertyName(String labelPropertyName) {
        this.labelPropertyName = labelPropertyName;
    }

    /**
     * List of additional properties to return in the result objects to the plugin's success callback.
     *
     * <p>In most cases, this should not be set.  The main use case
     * of setting this list is to use additional properties in the select function on the plugin's options, so
     * it is only recommended that this property be set when doing heavy customization to the select function.
     * This list is not used if the full result object is already being returned.</p>
     *
     * @return the list of additional properties to send back
     */
    @BeanTagAttribute
    public List<String> getAdditionalPropertiesToReturn() {
        return additionalPropertiesToReturn;
    }

    /**
     * Set the list of additional properties to return to the plugin success callback results
     *
     * @param additionalPropertiesToReturn
     */
    public void setAdditionalPropertiesToReturn(List<String> additionalPropertiesToReturn) {
        this.additionalPropertiesToReturn = additionalPropertiesToReturn;
    }

    /**
     * When set to true the results of a query method will be sent back as-is (in translated form) with all properties
     * intact.
     *
     * <p>
     * Note this is not supported for highly complex objects (ie, most auto-query objects - will throw exception).
     * Intended usage of this flag is with custom query methods which return simple data objects.
     * The query method can return a list of Strings which will be used for the suggestions, a list of objects
     * with 'label' and 'value' properties, or a custom object.  In the case of using a customObject
     * labelPropertyName or valuePropertyName MUST be specified (or both) OR the custom object must contain a
     * property named "label" or "value" (or both) for the suggestions to appear.  In cases where this is not used,
     * the data sent back represents a slim subset of the properties on the object.
     * </p>
     *
     * @return true if the query method results should be used as the suggestions, false to assume
     * objects are returned and suggestions are formed using the source property name
     */
    @BeanTagAttribute
    public boolean isReturnFullQueryObject() {
        return returnFullQueryObject;
    }

    /**
     * Setter for the for returning the full object of the query
     *
     * @param returnFullQueryObject
     */
    public void setReturnFullQueryObject(boolean returnFullQueryObject) {
        this.returnFullQueryObject = returnFullQueryObject;
    }

    /**
     * Indicates whether all suggest options should be retrieved up front and provide to the suggest
     * widget as options locally
     *
     * <p>
     * Use this for a small list of options to improve performance. The query will be performed on the client
     * to filter the provider options based on the users input instead of doing a query each time
     * </p>
     *
     * <p>
     * When a query method is configured and this option set to true the method will be invoked to set the
     * options. The query method should not take any arguments and should return the suggestion options
     * List or the JS String as a result. If a query method is not configured the suggest options can be
     * set through configuration or a view helper method (for example a component finalize method)
     * </p>
     *
     * @return true to provide the suggest options initially, false to use ajax retrieval based on the
     * user's input
     */
    @BeanTagAttribute
    public boolean isRetrieveAllSuggestions() {
        return retrieveAllSuggestions;
    }

    /**
     * Setter for the retrieve all suggestions indicator
     *
     * @param retrieveAllSuggestions
     */
    public void setRetrieveAllSuggestions(boolean retrieveAllSuggestions) {
        this.retrieveAllSuggestions = retrieveAllSuggestions;
    }

    /**
     * When {@link #isRetrieveAllSuggestions()} is true, this list provides the full list of suggestions
     *
     * <p>
     * If a query method is configured that method will be invoked to populate this list, otherwise the
     * list should be populated through configuration or the view helper
     * </p>
     *
     * <p>
     * The suggest options can either be a list of Strings, in which case the strings will be the suggested
     * values. Or a list of objects. If the object does not have 'label' and 'value' properties, a custom render
     * and select method must be provided
     * </p>
     *
     * @return list of suggest options
     */
    @BeanTagAttribute
    public List<Object> getSuggestOptions() {
        return suggestOptions;
    }

    /**
     * Setter for the list of suggest options
     *
     * @param suggestOptions
     */
    public void setSuggestOptions(List<Object> suggestOptions) {
        this.suggestOptions = suggestOptions;
    }

    /**
     * Returns the suggest options as a JS String (set by the framework from method invocation)
     *
     * @return suggest options JS string
     */
    public String getSuggestOptionsJsString() {
        if (StringUtils.isNotBlank(suggestOptionsJsString)) {
            return this.suggestOptionsJsString;
        }

        return "null";
    }

    /**
     * Sets suggest options javascript string
     *
     * @param suggestOptionsJsString
     */
    public void setSuggestOptionsJsString(String suggestOptionsJsString) {
        this.suggestOptionsJsString = suggestOptionsJsString;
    }

    /**
     * Holds post data for the suggest component.
     */
    public static class SuggestPostData implements Serializable {
        private static final long serialVersionUID = 997780560864981128L;

        private String id;

        private AttributeQuery suggestQuery;

        private String valuePropertyName;
        private String labelPropertyName;
        private List<String> additionalPropertiesToReturn;
        private boolean returnFullQueryObject;
        private boolean retrieveAllSuggestions;

        /**
         * Constructor taking suggest widget to pull post data from.
         *
         * @param suggest component instance to pull data
         */
        public SuggestPostData(Suggest suggest) {
            this.id = suggest.getId();
            this.suggestQuery = suggest.getSuggestQuery();
            this.valuePropertyName = suggest.getValuePropertyName();
            this.labelPropertyName = suggest.getLabelPropertyName();
            this.additionalPropertiesToReturn = suggest.getAdditionalPropertiesToReturn();
            this.returnFullQueryObject = suggest.isReturnFullQueryObject();
            this.retrieveAllSuggestions = suggest.isRetrieveAllSuggestions();
        }

        /**
         * @see org.kuali.rice.krad.uif.util.LifecycleElement#getId()
         */
        public String getId() {
            return id;
        }

        /**
         * @see Suggest#getSuggestQuery()
         */
        public AttributeQuery getSuggestQuery() {
            return suggestQuery;
        }

        /**
         * @see Suggest#getValuePropertyName()
         */
        public String getValuePropertyName() {
            return valuePropertyName;
        }

        /**
         * @see Suggest#getLabelPropertyName()
         */
        public String getLabelPropertyName() {
            return labelPropertyName;
        }

        /**
         * @see Suggest#getAdditionalPropertiesToReturn()
         */
        public List<String> getAdditionalPropertiesToReturn() {
            return additionalPropertiesToReturn;
        }

        /**
         * @see Suggest#isReturnFullQueryObject()
         */
        public boolean isReturnFullQueryObject() {
            return returnFullQueryObject;
        }

        /**
         * @see Suggest#isRetrieveAllSuggestions()
         */
        public boolean isRetrieveAllSuggestions() {
            return retrieveAllSuggestions;
        }
    }
}
