/*
 * 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.krad.uif.view;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.kuali.kfs.krad.service.DataObjectMetaDataService;
import org.kuali.kfs.krad.service.KRADServiceLocatorWeb;
import org.kuali.kfs.krad.uif.UifConstants;
import org.kuali.kfs.krad.uif.util.ObjectPropertyUtils;
import org.kuali.kfs.krad.uif.util.ViewModelUtils;
import org.kuali.kfs.krad.web.form.UifFormBase;

import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

/**
 * History class used to keep track of views visited so they can be displayed in the ui
 * as breadcrumbs - both as homeward path and history path interpretations
 */
public class History implements Serializable {
    private static final long serialVersionUID = -8279297694371557335L;
    private static final Logger LOG = Logger.getLogger(History.class);

    public static final String ENTRY_TOKEN = "$";
    public static final String VAR_TOKEN = ",";

    private boolean appendHomewardPath;
    private boolean appendPassedHistory;

    private HistoryEntry current;

    private List<HistoryEntry> homewardPath;
    private List<HistoryEntry> historyEntries;

    public History() {
        historyEntries = new ArrayList<HistoryEntry>();
    }

    /**
     * Gets the predetermined homeward path for this view's history.
     * This is set by the same property in the view's Breadcrumbs configuration.
     *
     * @return the homewardPath
     */
    public List<HistoryEntry> getHomewardPath() {
        return this.homewardPath;
    }

    /**
     * @param homewardPath the homewardPath to set
     */
    public void setHomewardPath(List<HistoryEntry> homewardPath) {
        this.homewardPath = homewardPath;
    }

    /**
     * Gets a list of the current HistoryEntries not including the current entry.
     * This list does not include the "&history=" query parameter on each HistoryEntry's
     * url variable.  For HistoryEntries that include history information to be passed to the
     * view they are retrieving, getGeneratedBreadcrumbs is used.
     *
     * @return the history
     */
    public List<HistoryEntry> getHistoryEntries() {
        return this.historyEntries;
    }

    /**
     * @param history the history to set
     */
    public void setHistoryEntries(List<HistoryEntry> history) {
        this.historyEntries = history;
    }

    /**
     * Gets the current view's HistoryEntry.
     * This does not include the "&history=" query parameter on its
     * url variable.  For the HistoryEntry that includes history information to be passed
     * on the url it is retrieving, getGeneratedCurrentBreadcrumb is used.
     *
     * @return the current
     */
    public HistoryEntry getCurrent() {
        return this.current;
    }

    /**
     * Sets the current HistoryEntry to the current view
     *
     * @param viewId
     * @param pageId
     * @param title
     * @param url
     * @param formKey
     */
    private void setCurrent(String viewId, String pageId, String title, String url, String formKey) {
        HistoryEntry entry = new HistoryEntry(viewId, pageId, title, url, formKey);
        current = entry;
    }

    /**
     * @param current the current to set
     */
    public void setCurrent(HistoryEntry current) {
        this.current = current;
    }

    /**
     * Takes in the encoded history query parameter string passed on the url and parses it to create
     * the list of historyEntries.  It will also append any homeward path if appendHomewardPath is true.  This
     * append will happen after the passedHistory entries are appended so it will not make sense to use both settings
     * in most cases.
     *
     * @param parameterString
     */
    public void buildHistoryFromParameterString(String parameterString) {
        if (StringUtils.isNotEmpty(parameterString)) {
            try {
                parameterString = URLDecoder.decode(parameterString, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                LOG.error("Error decoding history param", e);
            }

            historyEntries = new ArrayList<HistoryEntry>();
            if (appendPassedHistory) {
                String[] historyTokens = parameterString.split("\\" + ENTRY_TOKEN);
                for (String token : historyTokens) {
                    String[] params = token.split(VAR_TOKEN);
                    pushToHistory(params[0], params[1], params[2], params[3], params[4]);
                }
            }
        }

        if (appendHomewardPath) {
            historyEntries.addAll(homewardPath);
        }
    }

    /**
     * Gets the encoded and tokenized history parameter string that is representative of the HistoryEntries
     * currently in History and includes the current view's HistoryEntry.  This parameter should be appended on any
     * appropriate links which perform view swapping.
     *
     * @return
     */
    public String getHistoryParameterString() {
        String historyString = "";
        for (HistoryEntry e : historyEntries) {
            if (historyEntries.indexOf(e) == 0) {
                historyString = historyString + e.toParam();
            } else {
                historyString = historyString + ENTRY_TOKEN + e.toParam();
            }
        }

        // add current
        if (current != null) {
            if (historyString.equals("")) {
                historyString = historyString + current.toParam();
            } else {
                historyString = historyString + ENTRY_TOKEN + current.toParam();
            }
        }

        try {
            historyString = URLEncoder.encode(historyString, "UTF-8");
        } catch (Exception e) {
            LOG.error("Error encoding history param", e);
        }

        return historyString;
    }

    /**
     * Generates a list of HistoryEntries that can be used as breadcrumbs by the breadcrumb widget.  This
     * method appends the appropriate history information on the HistoryEntry url variables so when a view is requested
     * its history can be regenerated for use in its breadcrumbs.  It also sets the the passed showHome variable to
     * false to prevent showing the homeward path more than once (as it is passed through the history
     * variable backwards). This does not include the current HistoryEntry as a breadcrumb.
     *
     * @return
     */
    public List<HistoryEntry> getGeneratedBreadcrumbs() {
        List<HistoryEntry> breadcrumbs = new ArrayList<HistoryEntry>();
        for (int i = 0; i < historyEntries.size(); i++) {
            if (i == 0) {
                breadcrumbs.add(copyEntry(historyEntries.get(i)));
            } else {
                HistoryEntry breadcrumb = copyEntry(historyEntries.get(i));
                String historyParam = "";
                for (int j = 0; j < i; j++) {
                    historyParam = historyParam + ENTRY_TOKEN + historyEntries.get(j).toParam();
                }
                historyParam = historyParam.replaceFirst("\\" + ENTRY_TOKEN, "");
                try {
                    historyParam = URLEncoder.encode(historyParam, "UTF-8");
                } catch (Exception e) {
                    LOG.error("Error encoding history param", e);
                }

                String url = "";
                if (breadcrumb.getUrl().contains("?")) {
                    url = breadcrumb.getUrl() + "&" + UifConstants.UrlParams.HISTORY + "=" + historyParam;
                } else {
                    url = breadcrumb.getUrl() + "?" + UifConstants.UrlParams.HISTORY + "=" + historyParam;
                }

                breadcrumb.setUrl(url);
                breadcrumbs.add(breadcrumb);
            }
        }

        return breadcrumbs;
    }

    /**
     * Gets the current HistoryEntry in the breadcrumb format described in getGeneratedBreadcrumbs
     *
     * @return
     */
    public HistoryEntry getGeneratedCurrentBreadcrumb() {
        if (current == null) {
            return new HistoryEntry();
        }

        HistoryEntry breadcrumb = copyEntry(current);
        String historyParam = "";
        for (int j = 0; j < historyEntries.size(); j++) {
            historyParam = historyParam + ENTRY_TOKEN + historyEntries.get(j).toParam();
        }
        historyParam = historyParam.replaceFirst("\\" + ENTRY_TOKEN, "");

        try {
            historyParam = URLEncoder.encode(historyParam, "UTF-8");
        } catch (Exception e) {
            LOG.error("Error encoding history param", e);
        }

        String url = "";
        if (breadcrumb.getUrl().contains("?")) {
            url = breadcrumb.getUrl() + "&" + UifConstants.UrlParams.HISTORY + "=" + historyParam;
        } else {
            url = breadcrumb.getUrl() + "?" + UifConstants.UrlParams.HISTORY + "=" + historyParam;
        }
        breadcrumb.setUrl(url);

        return breadcrumb;
    }

    /**
     * Copies a HistoryEntry, for use during breadcrumb generation.
     *
     * @param e
     * @return
     */
    private HistoryEntry copyEntry(HistoryEntry e) {
        return new HistoryEntry(e.getViewId(), e.getPageId(), e.getTitle(), e.getUrl(), e.getFormKey());
    }

    /**
     * Pushes the information passed in to history.
     * Note: currently only used internally in the class - be cautious about its external use.
     *
     * @param viewId
     * @param pageId
     * @param title
     * @param url
     * @param formKey
     */
    public void pushToHistory(String viewId, String pageId, String title, String url, String formKey) {
        HistoryEntry entry = new HistoryEntry(viewId, pageId, title, url, formKey);
        historyEntries.add(entry);
    }

    /**
     * When this is set to true, the homeward path will be appended.
     * Note:  For most cases this should only be on during the first view load.
     * This setting is set automatically in most cases.
     *
     * @param appendHomewardPath the appendHomewardPath to set
     */
    public void setAppendHomewardPath(boolean appendHomewardPath) {
        this.appendHomewardPath = appendHomewardPath;
    }

    /**
     * @return the appendHomewardPath
     */
    public boolean isAppendHomewardPath() {
        return appendHomewardPath;
    }

    /**
     * Appends the passed history as each different view is shown.  This setting should be used when displaying
     * passed history is relevant to the user (ie inquiry/lookup chains).  This setting is set automatically in
     * most cases.
     *
     * @param appendPassedHistory the appendPassedHistory to set
     */
    public void setAppendPassedHistory(boolean appendPassedHistory) {
        this.appendPassedHistory = appendPassedHistory;
    }

    /**
     * @return the appendPassedHistory
     */
    public boolean isAppendPassedHistory() {
        return appendPassedHistory;
    }

    /**
     * Sets the current HistoryEntry using information from the form and the request.  This history parameter is
     * extracted out of the url inorder for a "clean" url to be used in history parameter and
     * breadcrumb generation, as passing history history through the nested urls is unnecessary.
     *
     * @param form
     * @param request
     */
    @SuppressWarnings("unchecked")
    public void setCurrent(UifFormBase form, HttpServletRequest request) {
        if (!request.getMethod().equals("POST")) {
            boolean showHomeValue = false;
            boolean pageIdValue = false;
            boolean formKeyValue = false;

            String queryString = "";
            String url = request.getRequestURL().toString();

            //remove history attribute
            Enumeration<String> params = request.getParameterNames();
            while (params.hasMoreElements()) {
                String key = params.nextElement();
                if (!key.equals(UifConstants.UrlParams.HISTORY)) {
                    for (String value : request.getParameterValues(key)) {
                        queryString = queryString + "&" + key + "=" + value;
                    }
                } else if (key.equals(UifConstants.UrlParams.PAGE_ID)) {
                    pageIdValue = true;
                } else if (key.equals(UifConstants.UrlParams.SHOW_HOME)) {
                    showHomeValue = true;
                } else if (key.equals(UifConstants.UrlParams.FORM_KEY)) {
                    formKeyValue = true;
                }
            }

            //add formKey and pageId to url
            if (StringUtils.isNotBlank(form.getFormKey()) && !formKeyValue) {
                queryString = queryString + "&" + UifConstants.UrlParams.FORM_KEY + "=" + form.getFormKey();
            }
            if (StringUtils.isNotBlank(form.getPageId()) && !pageIdValue) {
                queryString = queryString + "&" + UifConstants.UrlParams.PAGE_ID + "=" + form.getPageId();
            }
            if (!showHomeValue) {
                queryString = queryString + "&" + UifConstants.UrlParams.SHOW_HOME + "=false";
            }

            queryString = queryString.replaceFirst("&", "");

            if (StringUtils.isNotEmpty(queryString)) {
                url = url + "?" + queryString;
            }

            this.setCurrent(form.getViewId(), form.getPageId(), buildViewTitle(form), url, form.getFormKey());
        }
    }

    /**
     * Builds the title for the view to display in history (for example breadcrumbs)
     * <p>
     * <p>
     * Retrieves the viewLabelFieldPropertyName from the view if configured, otherwise attempts
     * to find the title attribute for the default data object. If view label property is found the
     * corresponding property value is retrieved and appended to the title for the view
     * </p>
     * <p>
     * TODO: Possibly move so it can be used for the actual view title, not just history
     *
     * @param form - form instance containing the view and view data
     * @return String title string to use
     */
    protected String buildViewTitle(UifFormBase form) {
        View view = form.getView();
        String title = view.getTitle();

        // may move this into view logic instead in the future if it is required for the view's title (not just breadcrumb)
        // if so remove this and just use getTitle - this logic would be in performFinalize instead
        String viewLabelPropertyName = view.getViewLabelFieldPropertyName();

        // if view label property name given, try to retrieve the title attribute for the main data object
        if (StringUtils.isBlank(viewLabelPropertyName)) {
            Class<?> dataObjectClass;
            if (StringUtils.isNotBlank(view.getDefaultBindingObjectPath())) {
                dataObjectClass = ObjectPropertyUtils.getPropertyType(form, view.getDefaultBindingObjectPath());
            } else {
                dataObjectClass = view.getFormClass();
            }

            DataObjectMetaDataService mds = KRADServiceLocatorWeb.getDataObjectMetaDataService();
            if (dataObjectClass != null) {
                viewLabelPropertyName = mds.getTitleAttribute(dataObjectClass);
            }
        }

        String viewLabelPropertyPath = "";
        if (StringUtils.isNotBlank(viewLabelPropertyName)) {
            // adjust binding prefix
            if (!viewLabelPropertyName.startsWith(UifConstants.NO_BIND_ADJUST_PREFIX)) {
                if (StringUtils.isNotBlank(view.getDefaultBindingObjectPath())) {
                    viewLabelPropertyPath = view.getDefaultBindingObjectPath() + "." + viewLabelPropertyName;
                }
            } else {
                viewLabelPropertyPath = StringUtils.removeStart(viewLabelPropertyName,
                    UifConstants.NO_BIND_ADJUST_PREFIX);
            }
        } else {
            // attempt to get title attribute
            Class<?> dataObjectClass;
            if (StringUtils.isNotBlank(view.getDefaultBindingObjectPath())) {
                dataObjectClass = ViewModelUtils.getObjectClassForMetadata(view, form,
                    view.getDefaultBindingObjectPath());
            } else {
                dataObjectClass = view.getFormClass();
            }

            DataObjectMetaDataService mds = KRADServiceLocatorWeb.getDataObjectMetaDataService();
            if (dataObjectClass != null) {
                String titleAttribute = mds.getTitleAttribute(dataObjectClass);
                if (StringUtils.isNotBlank(titleAttribute)) {
                    viewLabelPropertyPath = view.getDefaultBindingObjectPath() + "." + titleAttribute;
                }
            }
        }

        Object viewLabelPropertyValue = null;
        if (StringUtils.isNotBlank(viewLabelPropertyPath) && ObjectPropertyUtils.isReadableProperty(form, viewLabelPropertyPath)) {
            viewLabelPropertyValue = ObjectPropertyUtils.getPropertyValue(form, viewLabelPropertyPath);
        }

        String titleAppend = "";
        if (viewLabelPropertyValue != null) {
            titleAppend = viewLabelPropertyValue.toString();
        }

        if (StringUtils.isNotBlank(titleAppend) && view.getAppendOption() != null) {
            if (view.getAppendOption().equalsIgnoreCase(UifConstants.TitleAppendTypes.DASH)) {
                title = title + " - " + titleAppend;
            } else if (view.getAppendOption().equalsIgnoreCase(UifConstants.TitleAppendTypes.PARENTHESIS)) {
                title = title + "(" + titleAppend + ")";
            } else if (view.getAppendOption().equalsIgnoreCase(UifConstants.TitleAppendTypes.REPLACE)) {
                title = titleAppend;
            }
            //else it is none or blank so no title modification will be used
        }

        return title;
    }
}
