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

import org.kuali.kfs.krad.uif.view.ViewIndex;
import org.kuali.kfs.krad.uif.view.ViewModel;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.BeansException;
import org.springframework.beans.InvalidPropertyException;
import org.springframework.beans.PropertyValue;

import java.beans.PropertyDescriptor;
import java.beans.PropertyEditor;
import java.util.HashSet;
import java.util.Set;

/**
 * Class is a top level BeanWrapper for a UIF View Model
 * <p>
 * <p>
 * Registers custom property editors configured on the field associated with the property name for which
 * we are getting or setting a value. In addition determines if the field requires encryption and if so applies
 * the {@link UifEncryptionPropertyEditorWrapper}
 * </p>
 */
public class UifViewBeanWrapper extends BeanWrapperImpl {
    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(UifViewBeanWrapper.class);

    // this is a handle to the target object so we don't have to cast so often
    private ViewModel model;

    // this stores all properties this wrapper has already checked
    // with the view so the service isn't called again
    private Set<String> processedProperties;

    public UifViewBeanWrapper(ViewModel model) {
        super(model);

        this.model = model;
        this.processedProperties = new HashSet<String>();
    }

    /**
     * Attempts to find a corresponding data field for the given property name in the current view or previous view,
     * then if the field has a property editor configured it is registered with the property editor registry to use
     * for this property
     *
     * @param propertyName - name of the property to find field and editor for
     */
    protected void registerEditorFromView(String propertyName) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Attempting to find property editor for property '" + propertyName + "'");
        }

        // check if we already processed this property for this BeanWrapper instance
        if (processedProperties.contains(propertyName)) {
            return;
        }

        // when rendering the page, we will use the view that was just built, for post
        // we need to use the posted view (not the newly initialized view)
        ViewIndex viewIndex = null;
        if (model.getView() != null) {
            viewIndex = model.getView().getViewIndex();
        } else if (model.getPostedView() != null) {
            viewIndex = model.getPostedView().getViewIndex();
        }

        // if view index instance not established we cannot determine property editors
        if (viewIndex == null) {
            return;
        }

        PropertyEditor propertyEditor = null;
        boolean requiresEncryption = false;

        if (viewIndex.getFieldPropertyEditors().containsKey(propertyName)) {
            propertyEditor = viewIndex.getFieldPropertyEditors().get(propertyName);
        } else if (viewIndex.getSecureFieldPropertyEditors().containsKey(propertyName)) {
            propertyEditor = viewIndex.getSecureFieldPropertyEditors().get(propertyName);
            requiresEncryption = true;
        }

        if (propertyEditor != null) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Registering custom editor for property path '" + propertyName
                    + "' and property editor class '" + propertyEditor.getClass().getName() + "'");
            }

            if (requiresEncryption) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Enabling encryption for custom editor '" + propertyName +
                        "' and property editor class '" + propertyEditor.getClass().getName() + "'");
                }
                this.registerCustomEditor(null, propertyName, new UifEncryptionPropertyEditorWrapper(propertyEditor));
            } else {
                this.registerCustomEditor(null, propertyName, propertyEditor);
            }
        } else if (requiresEncryption) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("No custom formatter for property path '" + propertyName
                    + "' but property does require encryption");
            }

            this.registerCustomEditor(null, propertyName, new UifEncryptionPropertyEditorWrapper(
                findEditorForPropertyName(propertyName)));
        }

        processedProperties.add(propertyName);
    }

    protected PropertyEditor findEditorForPropertyName(String propertyName) {
        Class<?> clazz = getPropertyType(propertyName);
        if (LOG.isDebugEnabled()) {
            LOG.debug("Attempting retrieval of property editor using class '"
                + clazz
                + "' and property path '"
                + propertyName
                + "'");
        }

        PropertyEditor editor = findCustomEditor(clazz, propertyName);
        if (editor == null) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("No custom property editor found using class '"
                    + clazz
                    + "' and property path '"
                    + propertyName
                    + "'. Attempting to find default property editor class.");
            }
            editor = getDefaultEditor(clazz);
        }

        return editor;
    }

    @Override
    public Class<?> getPropertyType(String propertyName) throws BeansException {
        try {
            PropertyDescriptor pd = getPropertyDescriptorInternal(propertyName);
            if (pd != null) {
                return pd.getPropertyType();
            }

            // Maybe an indexed/mapped property...
            Object value = super.getPropertyValue(propertyName);
            if (value != null) {
                return value.getClass();
            }

            // Check to see if there is a custom editor,
            // which might give an indication on the desired target type.
            Class<?> editorType = guessPropertyTypeFromEditors(propertyName);
            if (editorType != null) {
                return editorType;
            }
        } catch (InvalidPropertyException ex) {
            // Consider as not determinable.
        }

        return null;
    }

    @Override
    public Object getPropertyValue(String propertyName) throws BeansException {
        registerEditorFromView(propertyName);
        return super.getPropertyValue(propertyName);
    }

    @Override
    public void setPropertyValue(PropertyValue pv) throws BeansException {
        registerEditorFromView(pv.getName());
        super.setPropertyValue(pv);
    }

    @Override
    public void setPropertyValue(String propertyName, Object value) throws BeansException {
        registerEditorFromView(propertyName);
        super.setPropertyValue(propertyName, value);
    }

    @Override
    public void setWrappedInstance(Object object, String nestedPath, Object rootObject) {
        //TODO clear cache?
        model = (ViewModel) object;
        super.setWrappedInstance(object, nestedPath, rootObject);
    }

    @Override
    public void setWrappedInstance(Object object) {
        //TODO clear cache?
        model = (ViewModel) object;
        super.setWrappedInstance(object);
    }
}
