/*
 * Copyright 2004-2014 ICEsoft Technologies Canada Corp.
 *
 * Licensed under the Apache 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.apache.org/licenses/LICENSE-2.0
 *
 * 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 com.icesoft.faces.utils;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.faces.component.UIComponent;
import javax.faces.component.UIInput;
import javax.faces.el.ValueBinding;
import javax.faces.el.PropertyNotFoundException;
import javax.faces.context.FacesContext;
import javax.faces.application.FacesMessage;
import java.io.Serializable;

import com.icesoft.faces.util.CoreUtils;

/**
 * There are some UIComponent properties which we want to set based on
 *  both user interactions and from changing ValueBinding values. If
 *  we follow the typical pattern for regular component properties,
 *  then the local field value takes precedence over the ValueBinding,
 *  so once a user interaction has occured, then the ValueBinding won't
 *  work. What we'd instead prefer, it to follow the UIInput value
 *  property pattern, where newly set values are pushed into the
 *  ValueBinding, so that both means of changing the value work.
 * 
 * Most of the methods here are loose adaptations of UIInput's equivalents. 
 *  
 * @author mcollette
 */
public class UpdatableProperty implements Serializable {
    private static final Log log = LogFactory.getLog(UpdatableProperty.class);
    
    private String name;
    private Object savedValue;
    private boolean haveSavedValue;
    private Object submittedValue;
    private boolean setSubmittedInLocalValue; 
    private Object value;
    private boolean setLocalValueInValueBinding;
    
    public UpdatableProperty(String name) {
        this.name = name;
    }
    
    /**
     * Before a component does all the phases, and goes through the several
     *  steps of updating its value, we sometimes need to save away the
     *  original value, so we can refer to it unmolested
     */
    public void saveValue(FacesContext context, UIComponent comp) {
//System.out.println(getDesc(comp, "SET_SAVE"));
        savedValue = getValue(context, comp);
        haveSavedValue = true;
//System.out.println("  savedValue: " + savedValue);
    }
    
    /**
     * If we did save away the value, then just access it, but if we didn't
     *  save it away, then we'll want the most up-to-date value
     */
    public Object getSavedValue(FacesContext context, UIComponent comp) {
//System.out.println(getDesc(comp, "GET_SAVE"));
//System.out.println("  savedValue: " + savedValue);
//System.out.println("  haveSavedValue: " + haveSavedValue);
        if(haveSavedValue)
            return savedValue;
        else
            return getValue(context, comp);
    }
    
    public Object getSubmittedValue(UIComponent comp) {
//System.out.println(getDesc(comp, "GET_SUB"));
//System.out.println("  submittedValue: " + submittedValue);
        return submittedValue;
    }
    
    public void setSubmittedValue(UIComponent comp, Object newSubmittedValue) {
//System.out.println(getDesc(comp, "SET_SUB"));
        submittedValue = newSubmittedValue;
        setSubmittedInLocalValue = true;
//System.out.println("  submittedValue: " + submittedValue);
    }
    
    protected void clearSubmittedValue() {
        submittedValue = null;
        setSubmittedInLocalValue = false;
    }
    
    public Object getValue(FacesContext context, UIComponent comp) {
//System.out.println(getDesc(comp, "GET"));
//System.out.println("  value: " + value);
        if(value != null) {
            return value;
        }
        ValueBinding vb = comp.getValueBinding(name);
//System.out.println("  vb: " + vb);
        if(vb != null) {
            Object modelValue = vb.getValue(context);
//System.out.println("  modelValue: " + modelValue);
            return modelValue;
        }
        return null;
    }
    
    /**
     * This method be called as a setter, but not from the decode, since
     *  for things to happen in the right phases, you should call
     *  setSubmittedValue(-) in the component or renderer's decode(-) method,
     *  and allow the validate(-) method here to bubble the submittedValue  
     *  into value in the correct phase.
     */
    public void setValue(UIComponent comp, Object newValue) {
//System.out.println(getDesc(comp, "SET"));
        value = newValue;
        setLocalValueInValueBinding = true;
//System.out.println("  value: " + value);
//StackTraceElement[] ste = Thread.currentThread().getStackTrace();
//for(int i = 0; i < ste.length; i++)
//    System.out.println("     " + ste[i]);
    }
    
    protected void clearValue() {
        value = null;
        setLocalValueInValueBinding = false;
    }
    
    protected void discontinueSettingLocalValueInValueBinding() {
        setLocalValueInValueBinding = false;
    }
    
    public void validate(FacesContext context, UIComponent comp) {
//System.out.println(getDesc(comp, "VALIDATE"));
//System.out.println("  valid: " + isComponentValid(comp));
//System.out.println("  submittedValue: " + submittedValue);
//System.out.println("  setSubmittedInLocalValue: " + setSubmittedInLocalValue);
//System.out.println("  old value: " + value);
        if( !isComponentValid(comp) ) {
            return;
        }
        // Unlike UIInput.value treatment, these properties require
        //  no conversion nor validation
        if(setSubmittedInLocalValue) {
            setValue(comp, submittedValue);
            clearSubmittedValue();
        }
    }
    
    public void updateModel(FacesContext context, UIComponent comp) {
//System.out.println(getDesc(comp, "UPDATE"));
//System.out.println("  valid: " + isComponentValid(comp));
//System.out.println("  setLocalValueInValueBinding: " + setLocalValueInValueBinding);
        if( !isComponentValid(comp) || !setLocalValueInValueBinding) {
            return;
        }
//System.out.println("  value: " + value);
        ValueBinding vb = comp.getValueBinding(name);
//System.out.println("  vb: " + vb);
        if(vb == null)
            return;
        try {
            vb.setValue(context, value);
//System.out.println("  vb.setValue worked");
            clearValue();
        }
        catch(PropertyNotFoundException e) {
//System.out.println("PropertyNotFoundException: " + e);
            // It's ok for the application to not use a settable ValueBinding,
            //  since the UIComponent can keep the value. But they probably
            //  don't mean to, so we should log something
            log.info(e);
            // Don't keep trying to set it
            discontinueSettingLocalValueInValueBinding();
        }
        catch(Exception e) {
//System.out.println("Exception: " + e);
            String msgId = comp.getClass().getName() + '.' + name;
            Object label = MessageUtils.getComponentLabel(context, comp);
            FacesMessage msg = MessageUtils.getMessage(
                context, msgId, new Object[]{label, value});
            if(msg != null)
                context.addMessage(comp.getClientId(context), msg);
            setInvalid(comp);
        }
    }
    
    protected boolean isComponentValid(UIComponent comp) {
        if(comp instanceof UIInput)
            return ((UIInput)comp).isValid();
        return true;
    }
    
    protected void setInvalid(UIComponent comp) {
        if(comp instanceof UIInput)
            ((UIInput)comp).setValid(false);
    }
    
    private String getDesc(UIComponent comp, String meth) {
        StringBuffer sb = new StringBuffer(256);
        if(comp != null) {
            String className = comp.getClass().getName();
            int index = className.lastIndexOf('.');
            if(index >= 0)
                className = className.substring(index+1);
            sb.append(className);
            sb.append('.');
        }
        sb.append(meth);
        sb.append("  name: ");
        sb.append(name);
        if(comp != null) {
            sb.append("  clientId: ");
            sb.append(comp.getClientId(FacesContext.getCurrentInstance()));
        }
        return sb.toString();
    }
    
    public Object[] saveState(UIComponent comp) {
//System.out.println(getDesc(comp, "SAVE_STATE"));
//System.out.println("  name: " + name);
//System.out.println("  savedValue: " + savedValue);
//System.out.println("  haveSavedValue: " + haveSavedValue);
//System.out.println("  submittedValue: " + submittedValue);
//System.out.println("  setSubmittedInLocalValue: " + setSubmittedInLocalValue);
//System.out.println("  value: " + value);
//System.out.println("  setLocalValueInValueBinding: " + setLocalValueInValueBinding);
        Object[] state = new Object[] {
            name,
            savedValue,
            new Boolean(haveSavedValue),
            submittedValue,
            new Boolean(setSubmittedInLocalValue),
            value,
            new Boolean(setLocalValueInValueBinding)
        };
        return state;
    }
    
    public void restoreState(UIComponent comp, Object[] state) {
        name = (String) state[0];
        savedValue = state[1];
        haveSavedValue = ((Boolean) state[2]).booleanValue();
        submittedValue = state[3];
        setSubmittedInLocalValue = ((Boolean) state[4]).booleanValue();
        value = state[5];
        setLocalValueInValueBinding = ((Boolean) state[6]).booleanValue();
//System.out.println(getDesc(comp, "RES_STATE"));
//System.out.println("  name: " + name);
//System.out.println("  savedValue: " + savedValue);
//System.out.println("  haveSavedValue: " + haveSavedValue);
//System.out.println("  submittedValue: " + submittedValue);
//System.out.println("  setSubmittedInLocalValue: " + setSubmittedInLocalValue);
//System.out.println("  value: " + value);
//System.out.println("  setLocalValueInValueBinding: " + setLocalValueInValueBinding);
    }
    
    public boolean equals(Object obj) {
        if (!(obj instanceof UpdatableProperty)) {
            return false;
        }
        UpdatableProperty up = (UpdatableProperty) obj;
        if (haveSavedValue != up.haveSavedValue) {
            return false;
        }
        if (setSubmittedInLocalValue != up.setSubmittedInLocalValue) {
            return false;
        }
        if (setLocalValueInValueBinding != up.setLocalValueInValueBinding) {
            return false;
        }
        if (!CoreUtils.objectsEqual(name, up.name)) {
            return false;
        }
        if (!CoreUtils.objectsEqual(savedValue, up.savedValue)) {
            return false;
        }
        if (!CoreUtils.objectsEqual(submittedValue, up.submittedValue)) {
            return false;
        }
        if (!CoreUtils.objectsEqual(value, up.value)) {
            return false;
        }
        return true;
    }
}
