package org.javafxdata.control;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.Event;
import javafx.event.EventType;
import javafx.scene.Node;
import javafx.scene.control.TreeItem;
import org.javafxdata.control.cell.CheckBoxCellFactory;
import org.javafxdata.control.cell.CheckBoxTreeCell;

/**
 * Simple TreeItem subclass that adds support for being in a selected, unselected
 * or indeterminate state. This is useful when used in conjunction with a TreeView
 * which has a {@link CheckBoxCellFactory} installed.
 *
 * @see CheckBoxTreeCell
 * @see CheckBoxCellFactory
 * @author <a href="http://www.jonathangiles.net">Jonathan Giles</a>
 */
// TODO the TreeModificationEvent doesn't actually bubble in the same way as
// TreeItem - it just looks that way as the 'bubbling' is done via changing the
// state of all parent items.
public class CheckBoxTreeItem<T> extends TreeItem<T> {
    
    /**
     * An EventType used when the CheckBoxTreeItem selection / indeterminate
     * state changes.
     * 
     * @param <T> The type of the value contained within the TreeItem.
     */
    @SuppressWarnings("unchecked")
    public static <T> EventType<TreeModificationEvent<T>> checkBoxSelectionChangedEvent() {
        return (EventType<TreeModificationEvent<T>>) CHECK_BOX_SELECTION_CHANGED_EVENT;
    }
    private static final EventType<?> CHECK_BOX_SELECTION_CHANGED_EVENT
            = new EventType<TreeModificationEvent<Object>>("checkBoxSelectionChangedEvent");

    /***************************************************************************
     *                                                                         *
     * Constructors                                                            *
     *                                                                         *
     **************************************************************************/
    
    /**
     * Creates a CheckBoxTreeItem with the value property set to the provided 
     * object.
     * 
     * @param value The object to be stored as the value of this TreeItem.
     */
    public CheckBoxTreeItem(T value) {
        this(value, null, false);
    }

    /**
     * Creates a CheckBoxTreeItem with the value property set to the provided 
     * object, and the graphic set to the provided Node.
     * 
     * @param value The object to be stored as the value of this CheckBoxTreeItem.
     * @param graphic The Node to show in the TreeView next to this CheckBoxTreeItem.
     */
    public CheckBoxTreeItem(T value, Node graphic) {
        this(value, graphic, false);
    }
    
    /**
     * Creates a CheckBoxTreeItem with the value property set to the provided 
     * object, the graphic set to the provided Node, and the initial state
     * of the {@link #selectedProperty()} set to the provided boolean value.
     * 
     * @param value The object to be stored as the value of this CheckBoxTreeItem.
     * @param graphic The Node to show in the TreeView next to this CheckBoxTreeItem.
     * @param selected The initial value of the 
     *            {@link #selectedProperty() selected} property.
     */
    public CheckBoxTreeItem(T value, Node graphic, boolean selected) {
        this(value, graphic, selected, false);
    }

    /**
     * Creates a CheckBoxTreeItem with the value property set to the provided 
     * object, the graphic set to the provided Node, the initial state
     * of the {@link #selectedProperty()} set to the provided boolean value, and
     * the initial state of the {@link #independentProperty() independent} 
     * property to the provided boolean value.
     * 
     * @param value The object to be stored as the value of this CheckBoxTreeItem.
     * @param graphic The Node to show in the TreeView next to this CheckBoxTreeItem.
     * @param selected The initial value of the 
     *            {@link #selectedProperty() selected} property.
     * @param independent The initial value of the 
     *            {@link #independentProperty() independent} property
     */
    public CheckBoxTreeItem(T value, Node graphic, boolean selected, boolean independent) {
        super(value, graphic);
        setSelected(selected);
        setIndependent(independent);
        
        selectedProperty().addListener(stateChangeListener);
        indeterminateProperty().addListener(stateChangeListener);
    }

    
    
    /***************************************************************************
     *                                                                         *
     * Callbacks                                                               *
     *                                                                         *
     **************************************************************************/   
    private final ChangeListener<Boolean> stateChangeListener = new ChangeListener<Boolean>() {
        @Override public void changed(ObservableValue<? extends Boolean> ov, Boolean oldVal, Boolean newVal) {
            updateState();
        }
    };
    
    
    /***************************************************************************
     *                                                                         *
     * Properties                                                              *
     *                                                                         *
     **************************************************************************/ 
    
    // --- Selected
    private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected", false) {
        @Override protected void invalidated() {
            super.invalidated();
            fireEvent(CheckBoxTreeItem.this, true);
        }
    };
    /** Sets the selected state of this CheckBoxTreeItem. */
    public final void setSelected(Boolean value) { selectedProperty().setValue(value); }
    /** Returns the selected state of this CheckBoxTreeItem. */
    public final Boolean isSelected() { return selected == null ? false : selected.getValue(); }
    /** A {@link BooleanProperty} used to represent the selected state of this CheckBoxTreeItem. */
    public final BooleanProperty selectedProperty() { return selected; }
    
    
    // --- Indeterminate
    private final BooleanProperty indeterminate = new SimpleBooleanProperty(this, "indeterminate", false) {
        @Override protected void invalidated() {
            super.invalidated();
            fireEvent(CheckBoxTreeItem.this, false);
        }
    };
    /** Sets the indeterminate state of this CheckBoxTreeItem. */
    public final void setIndeterminate(Boolean value) { indeterminateProperty().setValue(value); }
    /** Returns the indeterminate state of this CheckBoxTreeItem. */
    public final Boolean isIndeterminate() { return indeterminate == null ? false : indeterminate.getValue(); }
    /** A {@link BooleanProperty} used to represent the indeterminate state of this CheckBoxTreeItem. */
    public final BooleanProperty indeterminateProperty() { return indeterminate; }
    
    
    // --- Independent
    /** 
     * A {@link BooleanProperty} used to represent the independent state of this CheckBoxTreeItem. 
     * The independent state is used to represent whether changes to a single
     * CheckBoxTreeItem should influence the state of its parent and children.
     * 
     * <p>By default, the independent property is false, which means that when
     * a CheckBoxTreeItem has state changes to the selected or indeterminate
     * properties, the state of related CheckBoxTreeItems will possibly be changed.
     * If the independent property is set to true, the state of related CheckBoxTreeItems
     * will <b>never</b> change.
     */
    public final BooleanProperty independentProperty() { return independent; }
    private final BooleanProperty independent = new SimpleBooleanProperty(this, "independent", false);
    public final void setIndependent(Boolean value) { independentProperty().setValue(value); }
    public final Boolean isIndependent() { return independent == null ? false : independent.getValue(); }
    
    
    
    /***************************************************************************
     *                                                                         *
     * Private Implementation                                                  *
     *                                                                         *
     **************************************************************************/
    
    private static boolean updateLock = false;
    
    private void updateState() {
        if (isIndependent()) return;
        
        boolean firstLock = ! updateLock;
        
        // toggle parent (recursively up to root)
        updateLock = true;
        updateUpwards();
        
        if (firstLock) updateLock = false;
        
        // toggle children
        if (updateLock) return;
        updateDownwards();
    }

    private void updateUpwards() {
        if (! (getParent() instanceof CheckBoxTreeItem)) return;
        
        CheckBoxTreeItem<?> parent = (CheckBoxTreeItem<?>) getParent();
        int selectCount = 0;
        int indeterminateCount = 0;
        for (TreeItem<?> child : parent.getChildren()) {
            if (! (child instanceof CheckBoxTreeItem)) continue;
            
            CheckBoxTreeItem<?> cbti = (CheckBoxTreeItem<?>) child;
            
            selectCount += cbti.isSelected() && ! cbti.isIndeterminate() ? 1 : 0;
            indeterminateCount += cbti.isIndeterminate() ? 1 : 0;
        }
        
        if (selectCount == parent.getChildren().size()) {
            parent.setSelected(true);
            parent.setIndeterminate(false);
        } else if (selectCount == 0 && indeterminateCount == 0) {
            parent.setSelected(false);
            parent.setIndeterminate(false);
        } else {
            parent.setIndeterminate(true);
        }
    }
    
    private void updateDownwards() {
        // If this node is not a leaf, we also put all
        // children into the same state as this branch
        if (! isLeaf()) {
            for (TreeItem<T> child : getChildren()) {
                if (child instanceof CheckBoxTreeItem) {
                    CheckBoxTreeItem<?> cbti = ((CheckBoxTreeItem<?>) child);
                    cbti.setSelected(isSelected());
                }
            }
        }
    }
    
    private void fireEvent(CheckBoxTreeItem item, boolean selectionChanged) {
        Event evt = new CheckBoxTreeItem.TreeModificationEvent(CHECK_BOX_SELECTION_CHANGED_EVENT, item, selectionChanged);
        Event.fireEvent(this, evt);
    }
    
    
    /**
     * A TreeModificationEvent class that works in a similar vein to the 
     * {@link javafx.scene.control.TreeItem.TreeModificationEvent} class, in that
     * this event will bubble up the CheckBoxTreeItem hierarchy, until the parent
     * node is null.
     * 
     * @param <T> The type of the value contained within the
     *      {@link CheckBoxTreeItem#valueProperty() value} property.
     */
    public static class TreeModificationEvent<T> extends Event {

        private final CheckBoxTreeItem<T> treeItem;
        private final boolean selectionChanged;

        /**
         * Creates a default TreeModificationEvent instance to represent the 
         * change in selection/indeterminate states for the given CheckBoxTreeItem
         * instance.
         */
        public TreeModificationEvent(EventType<? extends Event> eventType, CheckBoxTreeItem<T> treeItem, boolean selectionChanged) {
            super(eventType);
            this.treeItem = treeItem;
            this.selectionChanged = selectionChanged;
        }
        
        /** 
         * Returns the CheckBoxTreeItem that this event occurred upon.
         * @return The CheckBoxTreeItem that this event occurred upon.
         */
        public CheckBoxTreeItem<T> getTreeItem() {
            return treeItem;
        }
        
        /**
         * Indicates the the reason for this event is that the selection on the
         * CheckBoxTreeItem changed (as opposed to it becoming indeterminate).
         */
        public boolean wasSelectionChanged() {
            return selectionChanged;
        }
        
        /**
         * Indicates the the reason for this event is that the indeterminate
         * state on the CheckBoxTreeItem changed (as opposed to it becoming 
         * selected or unselected).
         */
        public boolean wasIndeterminateChanged() {
            return ! selectionChanged;
        }
    }
}