/*
 * BeanPropertyPathBinding.java
 *
 * Copyright (C) 2008-2009 Alessio Stalla
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * As a special exception, the copyright holders of this library give you
 * permission to link this library with independent modules to produce an
 * executable, regardless of the license terms of these independent
 * modules, and to copy and distribute the resulting executable under
 * terms of your choice, provided that you also meet, for each linked
 * independent module, the terms and conditions of the license of that
 * module.  An independent module is a module which is not derived from
 * or based on this library.  If you modify this library, you may extend
 * this exception to your version of the library, but you are not
 * obligated to do so.  If you do not wish to do so, delete this
 * exception statement from your version.
 */

package snow.binding;

import org.armedbear.lisp.JavaObject;
import org.armedbear.lisp.LispObject;
import java.beans.*;
import java.util.*;
import java.lang.reflect.*;
import com.jgoodies.binding.value.AbstractValueModel;
import com.jgoodies.binding.value.ValueModel;

public class BeanPropertyPathBinding
    extends AbstractValueModel
    implements PropertyChangeListener {

    private String propertyName;
    private Object object;
    private BeanPropertyPathBinding nextListener;
    private BeanPropertyPathBinding prevListener;
    private String[] nextPropertyPath;
    private Method reader;
    private Method writer;
    
    private static final Class[] addRemovePropertyChangeListenerSignature = new Class[] { PropertyChangeListener.class };

    public BeanPropertyPathBinding(Object o, String propertyPath) {
	this(o, propertyPath.split("\\."));
    }

    public BeanPropertyPathBinding(Object o, String[] propertyPath) {
	this(o, propertyPath, null);
    }

    protected BeanPropertyPathBinding(Object o, String[] propertyPath,
				      BeanPropertyPathBinding prevListener) {
	this.prevListener = prevListener;
	Class<?> oClass = o.getClass();
	object = o;
	propertyName = propertyPath[0];
	nextPropertyPath = new String[propertyPath.length - 1];
	System.arraycopy(propertyPath, 1, nextPropertyPath, 0, nextPropertyPath.length);
	try {
	    Method addPropertyChangeListener = oClass.getMethod("addPropertyChangeListener", addRemovePropertyChangeListenerSignature);
	    addPropertyChangeListener.invoke(o, this);
	} catch(Exception e) {
	    throw new RuntimeException(e);
	}
	PropertyDescriptor pd = getPropertyDescriptor(oClass, propertyName);
	if(pd == null) {
	    throw new RuntimeException("Property " + propertyName + " not found in " + oClass);
	}
	reader = pd.getReadMethod();
	writer = pd.getWriteMethod();
	if(nextPropertyPath.length > 0) {
	    Object subObj = getLocalValue();
	    if(subObj != null) {
		nextListener = new BeanPropertyPathBinding(subObj, nextPropertyPath, this);
		
	    }
	}
    }
    
    public void remove() {
	try {
	    Method removePropertyChangeListener = object.getClass().getMethod("removePropertyChangeListener", addRemovePropertyChangeListenerSignature);
	    removePropertyChangeListener.invoke(object, this);
	    if(nextListener != null) {
		nextListener.remove();
	    }
	} catch(Exception e) {
	    throw new RuntimeException(e);
	}
    }

    private static PropertyDescriptor getPropertyDescriptor(Class<?> c, String propertyName) {
	try {
	    BeanInfo info = Introspector.getBeanInfo(c);
	    for(PropertyDescriptor pd : info.getPropertyDescriptors()) {
		if(pd.getName().equals(propertyName)) {
		    return pd;
		}
	    }
	} catch(Exception e) {
	    e.printStackTrace();
	}
	return null;
    }    
    
    public void propertyChange(PropertyChangeEvent evt) {
	if(propertyName.equals(evt.getPropertyName())) {
	    if(nextListener != null) {
		nextListener.remove();
	    }
	    if(nextPropertyPath.length > 0) {
		Object subObj = evt.getNewValue();
		if(subObj != null) {
		    nextListener = new BeanPropertyPathBinding(subObj, nextPropertyPath, this);
		}
	    }
	    fireValueChange(evt);
	}
    }

    protected void fireValueChange(PropertyChangeEvent evt) {
	if(prevListener != null) {
	    prevListener.fireValueChange(evt);
	} else {
	    fireValueChange(evt.getOldValue(), evt.getNewValue(), false);
	}
    }

    private BeanPropertyPathBinding getTarget() {
	if(nextPropertyPath.length == 0) {
	    return this;
	} else if(nextListener != null) {
	    return nextListener.getTarget();
	} else {
	    return null;
	}
    }
	
    public Object getLocalValue() {
	try {
	    return reader.invoke(object);
	} catch(Exception e) {
	    throw new RuntimeException(e);
	}
    }

    @Override
    public Object getValue() {
	try {
	    BeanPropertyPathBinding target = getTarget();
	    if(target != null) {
		return target.getLocalValue();
	    } else {
		return null;
	    }
	} catch(Exception e) {
	    throw new RuntimeException(e);
	}
    }

    public void setLocalValue(Object value) {
	try {
	    writer.invoke(object, value);
	} catch(Exception e) {
	    throw new RuntimeException(e);
	}
    }


    @Override
    public void setValue(Object value) {
	try {
	    BeanPropertyPathBinding target = getTarget();
	    if(target != null) {
		target.setLocalValue(value);
	    } else {
		StringBuilder sb = new StringBuilder();
		assert(nextPropertyPath != null);
		for(String s : nextPropertyPath) {
		    if(sb.length() > 0) {
			sb.append('.');
		    }
		    sb.append(s);
		}
		throw new NullPointerException("Property " + sb + " not reachable.");
	    }
	} catch(Exception e) {
	    throw new RuntimeException(e);
	}
    }
	
}
