/*************************************************************************
 *
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 * 
 * Copyright 2000, 2010 Oracle and/or its affiliates.
 *
 * OpenOffice.org - a multi-platform office productivity suite
 *
 * This file is part of OpenOffice.org.
 *
 * OpenOffice.org is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 3
 * only, as published by the Free Software Foundation.
 *
 * OpenOffice.org 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 Lesser General Public License version 3 for more details
 * (a copy is included in the LICENSE file that accompanied this code).
 *
 * You should have received a copy of the GNU Lesser General Public License
 * version 3 along with OpenOffice.org.  If not, see
 * <http://www.openoffice.org/license.html>
 * for a copy of the LGPLv3 License.
 *
 ************************************************************************/
package com.sun.star.wizards.ui.event;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import com.sun.star.wizards.common.PropertyNames;

/**
 * @author rpiterman
 * DataAware objects are used to live-synchronize UI and DataModel/DataObject.
 * It is used as listener on UI events, to keep the DataObject up to date.
 * This class, as a base abstract class, sets a frame of functionality,
 * delegating the data Object get/set methods to a Value object,
 * and leaving the UI get/set methods abstract.
 * Note that event listenning is *not* a part of this model.
 * the updateData() or updateUI() methods should be porogramatically called.
 * in child classes, the updateData() will be binded to UI event calls.
 * <br><br>
 * This class holds references to a Data Object and a Value object.
 * The Value object "knows" how to get and set a value from the 
 * Data Object.
 */
public abstract class DataAware {

    /**
     * this is the data object.
     */
    protected Object dataObject;
    //protected Method setMethod;
    //protected Method getMethod;
    /**
     * A Value Object knows how to get/set a value
     * from/to the data object.
     */
    protected Value value;
    
    /**
     * creates a DataAware object for the given data object and Value object.
     * @param dataObject_
     * @param value_
     */
    protected DataAware(Object dataObject_, Value value_) {
        dataObject = dataObject_;
        value = value_;
        //getMethod = createGetMethod(dataPropName, dataObject);
        //setMethod = createSetMethod(dataPropName, dataObject, getMethod.getReturnType());
    }

    /**
     * returns the data object.
     * @return
     */
    public Object getDataObject() {
        return dataObject;
    }

    /**
     * sets a new data object. Optionally
     * update the UI.
     * @param obj the new data object.
     * @param updateUI if true updateUI() will be called.
     */
    public void setDataObject(Object obj, boolean updateUI) {

        if (obj != null && !value.isAssignable(obj.getClass())) 
            throw new ClassCastException("can not cast new DataObject to original Class");

        dataObject = obj;

        if (updateUI)
            updateUI();

    }

    /**
     * Sets the given value to the data object.
     * this method delegates the job to the
     * Value object, but can be overwritten if
     * another kind of Data is needed.
     * @param newValue the new value to set to the DataObject.
     */
    protected void setToData(Object newValue) {
        value.set(newValue,getDataObject());
    }

    /**
     * gets the current value from the data obejct.
     * this method delegates the job to 
     * the value object.
     * @return the current value of the data object.
     */
    protected Object getFromData() {
        return value.get(getDataObject());
    }
    
    /**
     * sets the given value to the UI control
     * @param newValue the value to set to the ui control.
     */
    protected abstract void setToUI(Object newValue);

    /**
     * gets the current value from the UI control.
     * @return the current value from the UI control.
     */
    protected abstract Object getFromUI();

    /**
     * updates the UI control according to the
     * current state of the data object.
     */
    public void updateUI() {
        Object data = getFromData();
        Object ui = getFromUI();
        if (!equals(data, ui))
            try {
                setToUI(data);
            } catch (Exception ex) {
                ex.printStackTrace();
                //TODO tell user...
            }
        enableControls(data);
    }

    /**
     * enables
     * @param currentValue
     */
    protected void enableControls(Object currentValue) {
    }

    /**
     * updates the DataObject according to
     * the current state of the UI control.
     */
    public void updateData() {
        Object data = getFromData();
        Object ui = getFromUI();
        if (!equals(data, ui))
            setToData(ui);
        enableControls(ui);
    }

    public interface Listener {
        public void eventPerformed(Object event);
    }

    /**
     * compares the two given objects.
     * This method is null safe and returns true also if both are null...
     * If both are arrays, treats them as array of short and compares them.
     * @param a first object to compare
     * @param b second object to compare.
     * @return true if both are null or both are equal.
     */
    protected boolean equals(Object a, Object b) {
        if (a == null && b == null)
            return true;
        if (a == null || b == null)
            return false;
        if (a.getClass().isArray()) {
            if (b.getClass().isArray())
                return Arrays.equals((short[]) a, (short[]) b);
            else
                return false;
        }
        return a.equals(b);
    }

    /**
     * given a collection containing DataAware objects,
     * calls updateUI() on each memebr of the collection.
     * @param dataAwares a collection containing DataAware objects.
     */
    public static void updateUI(Collection dataAwares) {
        for (Iterator i = dataAwares.iterator(); i.hasNext();)
             ((DataAware) i.next()).updateUI();
    }

    public static void updateData(Collection dataAwares) {
        for (Iterator i = dataAwares.iterator(); i.hasNext();)
             ((DataAware) i.next()).updateData();
    }

    /**
     * /**
     * Given a collection containing DataAware objects,
     * sets the given DataObject to each DataAware object
     * in the given collection
     * @param dataAwares a collection of DataAware objects.
     * @param dataObject new data object to set to the DataAware objects in the given collection.
     * @param updateUI if true, calls updateUI() on each DataAware object.
     */public static void setDataObject(Collection dataAwares, Object dataObject, boolean updateUI) {
        for (Iterator i = dataAwares.iterator(); i.hasNext();)
             ((DataAware) i.next()).setDataObject(dataObject, updateUI);
    }
    
    /**
     * Value objects read and write a value from and
     * to an object. Typically using reflection and JavaBeans properties 
     * or directly using memeber reflection API.
     * DataAware delegates the handling of the DataObject
     * to a Value object.
     * 2 implementations currently exist: PropertyValue,
     * using JavaBeans properties reflection, and DataAwareFields classes
     * which implement different memeber types.
     */
    public interface Value {
        /**
         * gets a value from the given object.
         * @param target the object to get the value from.
         * @return the value from the given object.
         */
        public Object get(Object target);
        /**
         * sets a value to the given object.
         * @param value the value to set to the object.
         * @param target the object to set the value to.
         */
        public void set(Object value, Object target);
        /**
         * checks if this Value object can handle
         * the given object type as a target.
         * @param type the type of a target to check
         * @return true if the given class is acceptible for
         * the Value object. False if not.
         */
        public boolean isAssignable(Class type);
    }
    
    /**
     * implementation of Value, handling JavaBeans properties through
     * reflection.
     * This Object gets and sets a value a specific
     * (JavaBean-style) property on a given object.
     * @author rp143992
     */
    public static class PropertyValue implements Value {
        /**
         * the get method of the JavaBean-style property
         */
        private Method getMethod;
        /**
         * the set method of the JavaBean-style property
         */
        private Method setMethod;

        /**
         * creates a PropertyValue for the property with
         * the given name, of the given JavaBean object.
         * @param propertyName the property to access. Must be a Cup letter (e.g. PropertyNames.PROPERTY_NAME for getName() and setName("..."). )
         * @param propertyOwner the object which "own" or "contains" the property.
         */
        public PropertyValue(String propertyName, Object propertyOwner) {
            getMethod = createGetMethod(propertyName, propertyOwner);
            setMethod = createSetMethod(propertyName, propertyOwner, getMethod.getReturnType());
        }
        
        /**
         * called from the constructor, and creates a get method reflection object
         * for the given property and object.
         * @param propName the property name0
         * @param obj the object which contains the property.
         * @return the get method reflection object.
         */
        private static Class[] EMPTY_ARRAY = new Class[0];

        protected Method createGetMethod(String propName, Object obj)
        {
            Method m = null;
            try
            { //try to get a "get" method.

                m = obj.getClass().getMethod("get" + propName, EMPTY_ARRAY);
            }
            catch (NoSuchMethodException ex1)
            {
                throw new IllegalArgumentException("get" + propName + "() method does not exist on " + obj.getClass().getName());
            }
            return m;
        }

        /* (non-Javadoc)
         * @see com.sun.star.wizards.ui.event.DataAware.Value#get(java.lang.Object)
         */
        public Object get(Object target) {
            try {
                return getMethod.invoke(target, EMPTY_ARRAY);
            } catch (IllegalAccessException ex1) {
                ex1.printStackTrace();
            } catch (InvocationTargetException ex2) {
                ex2.printStackTrace();
            } catch (NullPointerException npe) {
                if (getMethod.getReturnType().equals(String.class))
                    return PropertyNames.EMPTY_STRING;
                if (getMethod.getReturnType().equals(Short.class))
                    return new Short((short) 0);
                if (getMethod.getReturnType().equals(Integer.class))
                    return 0;
                if (getMethod.getReturnType().equals(short[].class))
                    return new short[0];
            }
            return null;
        }

        protected Method createSetMethod(String propName, Object obj, Class paramClass) {
            Method m = null;
            try {
                m = obj.getClass().getMethod("set" + propName, new Class[] { paramClass });
            } catch (NoSuchMethodException ex1) {
                throw new IllegalArgumentException("set" + propName + "(" + getMethod.getReturnType().getName() + ") method does not exist on " + obj.getClass().getName());
            }
            return m;
        }

        /* (non-Javadoc)
         * @see com.sun.star.wizards.ui.event.DataAware.Value#set(java.lang.Object, java.lang.Object)
         */
        public void set(Object value, Object target) {
            try {
                setMethod.invoke(target, value);
            } catch (IllegalAccessException ex1) {
                ex1.printStackTrace();
            } catch (InvocationTargetException ex2) {
                ex2.printStackTrace();
            }
        }

        /* (non-Javadoc)
         * @see com.sun.star.wizards.ui.event.DataAware.Value#isAssignable(java.lang.Class)
         */
        public boolean isAssignable(Class type) {
            return getMethod.getDeclaringClass().isAssignableFrom(type) &&
                setMethod.getDeclaringClass().isAssignableFrom(type);
        }
    }
}
