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

import org.apache.commons.lang.StringUtils;
import org.kuali.rice.core.api.CoreApiServiceLocator;
import org.kuali.rice.core.api.exception.RiceIllegalArgumentException;
import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
import org.kuali.rice.krad.uif.util.SessionTransient;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

/**
 * Manages Uif form objects for a session.
 *
 * @author Kuali Rice Team (rice.collab@kuali.org)
 */
public class UifFormManager implements Serializable {
    private static final long serialVersionUID = -6323378881342207080L;

    private int maxNumberOfSessionForms = 5;

    protected Vector accessedFormKeys;

    protected Map<String, UifFormBase> sessionForms;

    /**
     * Create a new form manager with an empty list of forms for the session.
     */
    public UifFormManager() {
        this.accessedFormKeys = new Vector();
        this.sessionForms = new HashMap<String, UifFormBase>();

        if (CoreApiServiceLocator.getKualiConfigurationService() != null) {
            String maxNumberOfSessionFormsConfig =
                    CoreApiServiceLocator.getKualiConfigurationService().getPropertyValueAsString(
                            "maxNumberOfSessionForms");
            if (StringUtils.isNotBlank(maxNumberOfSessionFormsConfig)) {
                maxNumberOfSessionForms = Integer.parseInt(maxNumberOfSessionFormsConfig);
            }
        }
    }

    /**
     * Add a form to the session.
     *
     * @param form to be added to the session
     */
    public synchronized void addSessionForm(UifFormBase form) {
        if (form == null || StringUtils.isBlank(form.getFormKey())) {
            throw new RiceIllegalArgumentException("Form or form key was null");
        }

        sessionForms.put(form.getFormKey(), form);

        // add form key to top of vector indicating it is most recent
        if (accessedFormKeys.contains(form.getFormKey())) {
            accessedFormKeys.removeElement(form.getFormKey());
        }
        accessedFormKeys.add(form.getFormKey());

        // check if we have too many forms and need to remove an old one
        if (sessionForms.size() > maxNumberOfSessionForms) {
            // get the oldest form we have
            String removeFormKey = (String) accessedFormKeys.get(0);
            if (sessionForms.containsKey(removeFormKey)) {
                sessionForms.remove(removeFormKey);
            }
            accessedFormKeys.removeElementAt(0);
        }
    }

    /**
     * Retrieve a form from the session.
     *
     * @param formKey of the form to retrieve from the session
     * @return UifFormBase
     */
    public UifFormBase getSessionForm(String formKey) {
        if (sessionForms.containsKey(formKey)) {
            return sessionForms.get(formKey);
        }

        return null;
    }

    /**
     * Removes the stored form data and the forms from the breadcrumb history from the session.
     *
     * @param form to be removed
     */
    public void removeSessionForm(UifFormBase form) {
        if (form == null || StringUtils.isBlank(form.getFormKey())) {
            return;
        }

        removeSessionFormByKey(form.getFormKey());
    }

    /**
     * Removes the stored form data and the forms from the breadcrumb history from the session.
     *
     * @param formKey of the form to be removed
     */
    public void removeFormWithHistoryFormsByKey(String formKey) {
        if (sessionForms.containsKey(formKey)) {
            removeSessionFormByKey(formKey);
        }
    }

    /**
     * Removes the stored form data from the session.
     *
     * @param formKey of the form to be removed
     */
    public void removeSessionFormByKey(String formKey) {
        if (accessedFormKeys.contains(formKey)) {
            accessedFormKeys.removeElement(formKey);
        }

        if (sessionForms.containsKey(formKey)) {
            sessionForms.remove(formKey);
        }
    }

    /**
     * Indicates whether the form manager has a session form with the given key.
     *
     * @param formKey key of the form in session to check for
     * @return true if the manager contains the session form, false if not
     */
    public boolean hasSessionForm(String formKey) {
        return sessionForms.containsKey(formKey);
    }

    /**
     * Retrieves the session form based on the formkey and updates the non session transient
     * variables on the request form from the session form.
     *
     * @param requestForm
     * @param formKey
     */
    public void updateFormWithSession(UifFormBase requestForm, String formKey) {
        UifFormBase sessionForm = sessionForms.get(formKey);
        if (sessionForm == null) {
            return;
        }

        if (!sessionForm.getClass().isAssignableFrom(requestForm.getClass())) {
            throw new RuntimeException(
                    "Session form mismatch, session form class not assignable from request form class");
        }

        List<Field> fields = new ArrayList<Field>();
        fields = getAllFields(fields, sessionForm.getClass(), UifFormBase.class);
        for (Field field : fields) {
            boolean copyValue = true;
            for (Annotation an : field.getAnnotations()) {
                if (an instanceof SessionTransient) {
                    copyValue = false;
                }
            }

            if (copyValue && ObjectPropertyUtils.isReadableProperty(sessionForm, field.getName()) && ObjectPropertyUtils
                    .isWritableProperty(sessionForm, field.getName())) {
                Object fieldValue = ObjectPropertyUtils.getPropertyValue(sessionForm, field.getName());
                ObjectPropertyUtils.setPropertyValue(requestForm, field.getName(), fieldValue);
            }
        }
    }

    /**
     * Removes the values that are marked @SessionTransient from the form.
     *
     * @param form the form from which the session transient values have been purged
     */
    public void purgeForm(UifFormBase form) {
        List<Field> fields = new ArrayList<Field>();
        fields = getAllFields(fields, form.getClass(), UifFormBase.class);
        for (Field field : fields) {
            boolean purgeValue = false;

            if (!field.getType().isPrimitive()) {
                for (Annotation an : field.getAnnotations()) {
                    if (an instanceof SessionTransient) {
                        purgeValue = true;
                    }
                }
            }

            if (purgeValue && ObjectPropertyUtils.isWritableProperty(form, field.getName())) {
                ObjectPropertyUtils.setPropertyValue(form, field.getName(), null);
            }
        }
    }

    private List<Field> getAllFields(List<Field> fields, Class<?> type, Class<?> stopAt) {
        for (Field field : type.getDeclaredFields()) {
            fields.add(field);
        }

        if (type.getSuperclass() != null && !type.getName().equals(stopAt.getName())) {
            fields = getAllFields(fields, type.getSuperclass(), stopAt);
        }

        return fields;
    }

    /**
     * Internal vector maintained to keep track of accessed form and the order in which they were accessed.
     *
     * <p>Used for the form clearing process. When forms are added to the manager their key is added to the top of
     * the vector. When a form needs to be cleared, the form identified by the key at the botton of this vector
     * is removed</p>
     *
     * @return Vector instance holding form key strings
     */
    protected Vector getAccessedFormKeys() {
        return accessedFormKeys;
    }

    /**
     * Maximum number of forms that can be stored at one time by the manager.
     *
     * @return int max number of forms
     */
    public int getMaxNumberOfSessionForms() {
        return maxNumberOfSessionForms;
    }

    /**
     * @see UifFormManager#getMaxNumberOfSessionForms()
     */
    public void setMaxNumberOfSessionForms(int maxNumberOfSessionForms) {
        this.maxNumberOfSessionForms = maxNumberOfSessionForms;
    }

}
