package javax.beans.binding.stateless;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * Represents a property composed of a chain of properties.
 *
 * <p>For example, supposed we had the properties {@code JButton#action} and
 * {@code Action#longDescription}. We could compose these two properties to
 * create a new property, the button's long description.
 *
 * @author jessewilson
 */
public class ChainedProperty<B,V> implements StatelessProperty<B,V> {

    private final List<StatelessProperty> propertyChain;

    public ChainedProperty(List<StatelessProperty> propertyChain) {
        this.propertyChain = new ArrayList<StatelessProperty>(propertyChain);
    }

    @SuppressWarnings("unchecked")
    public V get(B bean) {
        Object current = bean;
        for (StatelessProperty statelessProperty : propertyChain) {
            current = statelessProperty.get(current);

            // use a different marker value for cannot resolve?
            if (current == null) {
                return null;
            }
        }

        return (V)current;
    }

    @SuppressWarnings("unchecked")
    public void set(B bean, V value) {
        Object current = bean;
        for (Iterator<StatelessProperty> m = propertyChain.iterator(); m.hasNext(); ) {
            StatelessProperty statelessProperty = m.next();

            if (m.hasNext()) {
                current = statelessProperty.get(current);
                if (current == null) {
                    throw new IllegalStateException("Cannot set!");
                }
            } else {
                statelessProperty.set(current, value);

            }
        }
    }

    @SuppressWarnings("unchecked")
    public void addPropertyChangeListener(B bean, PropertyChangeListener p) {
        List<SubListener> listeners = buildListeners(bean, p);

        Object value = bean;
        for (SubListener subListener : listeners) {
            value = subListener.initializeBean(value);
            subListener.startListening();
        }
    }

    @SuppressWarnings("unchecked")
    public void removePropertyChangeListener(B bean, PropertyChangeListener p) {
        List<SubListener> listeners = buildListeners(bean, p);

        Object value = bean;
        for (SubListener subListener : listeners) {
            value = subListener.initializeBean(value);
            subListener.stopListening();
        }
    }

    @SuppressWarnings("unchecked")
    private List<SubListener> buildListeners(B bean, PropertyChangeListener p) {
        List<SubListener> listeners = new ArrayList<SubListener>();

        // build the chain in reverse order, this is so the chain's next link
        // can be final
        SubListener current = new LastSubListener(
                propertyChain.get(propertyChain.size() - 1), bean, getName(), p);
        listeners.add(0, current);

        for (int i = propertyChain.size() - 2; i >= 0; i--) {
            current = new ChainingSubListener(propertyChain.get(i), current);
            listeners.add(0, current);
        }
        return listeners;
    }


    public String getName() {
        StringBuilder chainName = new StringBuilder();
        for (Iterator<StatelessProperty> i = propertyChain.iterator(); i.hasNext(); ) {
            chainName.append(i.next().getName());
            if (i.hasNext()) {
                chainName.append(".");
            }
        }
        return chainName.toString();
    }

    /**
     * Listens to a single property in a chain.
     */
    public static abstract class SubListener<B,V> implements PropertyChangeListener {

        /** the property being observed */
        private B bean;
        private final StatelessProperty<B,V> property;

        /** the value is also the bean for the next in the chain */
        private V currentValue;

        public SubListener(StatelessProperty<B,V> property) {
            this.property = property;
        }

        /**
         * Install the observed object. It is still necessary to call
         * {@link #startListening} to make this observer active.
         *
         * @return the value of the property.
         */
        public V initializeBean(B bean) {
            if (this.bean != null || this.currentValue != null) {
                throw new IllegalStateException();
            }

            this.bean = bean;
            this.currentValue = property.get(bean);
            return currentValue;
        }

        /**
         * Replace the observed object.
         */
        protected void setBean(B bean) {
            if (this.bean == bean) {
                return;
            }

            stopListening();
            this.bean = bean;
            startListening();

            valueChanged();
        }

        public void startListening() {
            if (bean != null) {
                property.addPropertyChangeListener(bean, this);
            }
        }

        public void stopListening() {
            if (bean != null) {
                property.removePropertyChangeListener(bean, this);
            }
        }

        /**
         * Handle a change in the value of the observed property.
         */
        public void propertyChange(PropertyChangeEvent evt) {
            valueChanged();
        }

        /**
         * Respond to a change in this bean's value.
         */
        private void valueChanged() {
            V oldValue = currentValue;
            currentValue = bean != null
                    ? property.get(bean)
                    : null;
            fireValueChanged(oldValue, currentValue);
        }

        /**
         * Notify downstream objects or listeners about this bean's value
         * changing.
         */
        public abstract void fireValueChanged(V oldValue, V currentValue);

        public int hashCode() {
            return property.hashCode();
        }

        public boolean equals(Object obj) {
            return obj != null
                    && getClass() == obj.getClass()
                    && ((SubListener)obj).property.equals(property);
        }
    }

    /**
     * This sublistener just notifies the next sublistener in series when its
     * observed object is changed.
     */
    public static class ChainingSubListener<B,V> extends SubListener<B,V> {
        /** the next sublistener in series */
        private final SubListener<V,?> next;

        public ChainingSubListener(StatelessProperty<B,V> property, SubListener<V,?> next) {
            super(property);
            this.next = next;
        }

        public void fireValueChanged(V oldValue, V currentValue) {
            next.setBean(currentValue);
        }

        public int hashCode() {
            return super.hashCode() * 37 + next.hashCode();
        }

        public boolean equals(Object obj) {
            return super.equals(obj)
                    && ((ChainingSubListener)obj).next.equals(next);
        }
    }

    /**
     * This sublistener just notifies the property change listener when its
     * observed property is changed.
     */
    public static class LastSubListener<B,V> extends SubListener<B,V> {
        private final Object rootBean;
        private final String chainName;
        private final PropertyChangeListener delegate;

        public LastSubListener(StatelessProperty<B,V> property, Object rootBean,
                String chainName, PropertyChangeListener chainListener) {
            super(property);
            this.rootBean = rootBean;
            this.chainName = chainName;
            this.delegate = chainListener;
        }

        public void fireValueChanged(V oldValue, V currentValue) {
            // only notify if the value has actually changed
            if(oldValue == null ? currentValue != null : !oldValue.equals(currentValue)) {
                delegate.propertyChange(new PropertyChangeEvent(rootBean, chainName, oldValue, currentValue));
            }
        }

        public int hashCode() {
            return super.hashCode() * 37 + delegate.hashCode();
        }

        public boolean equals(Object obj) {
            return super.equals(obj)
                    && ((LastSubListener)obj).rootBean == rootBean 
                    && ((LastSubListener)obj).delegate == delegate;
        }
    }
}
