/**
 * Created 2016 by Jordan Martinez
 *
 * The author dedicates this to the public domain
 */

package org.fxmisc.livedirs.demo.checkbox;

import javafx.beans.InvalidationListener;
import javafx.scene.Node;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import org.reactfx.Subscription;
import org.reactfx.value.Var;

import java.util.function.BiConsumer;
import java.util.function.Function;

public class CheckBoxTreeCell<C extends CheckBoxContent> extends TreeCell<C> {

    private final Recursive<BiConsumer<TreeItem<C>, CheckBoxContent.State>> UPDATE_DOWNWARDS = new Recursive<>();
    {
        UPDATE_DOWNWARDS.f = (item, state) -> {
            item.getValue().setState(state);
            item.getChildren().forEach(child -> UPDATE_DOWNWARDS.f.accept(child, state));
        };
    }

    private final CheckBox checkBox = new CheckBox();
    private final Function<C, String> stringConverter;
    private Var<CheckBoxContent.State> state;
    private Var<Boolean> select;
    private Subscription intermediateState;

    private final InvalidationListener stateInvalidations = (obs) -> {
        TreeItem<C> treeItem = getTreeItem();
        if (treeItem != null) {
            final TreeItem<C> parentItem = treeItem.getParent();

            // do upward call first
            if (parentItem != null) {
                CheckBoxContent value = parentItem.getValue();

                if (value != null && !value.isLocked()) {
                    CheckBoxContent.State[] childrenStates = parentItem.getChildren()
                            .stream().map(v -> v.getValue().getState())
                            .distinct()
                            .toArray(CheckBoxContent.State[]::new);

                    /*
                        Due to `distinct()`,
                            if length > 1,
                                then children were 2+ of the 3 CheckBoxContent.State enum values
                                Thus, set to UNDEFINED
                            else
                                then children were all UNCHECKED or CHECKED.
                                Thus, set the current value to that State
                     */
                    value.setState(childrenStates.length > 1
                            ? CheckBoxContent.State.UNDEFINED
                            : childrenStates[0]
                    );
                }
            }

            // then do downward call
            C itemVal = treeItem.getValue();
            // when children's invalidation listeners are called, skip this item's update as it
            //  was the one the initiated the call.
            itemVal.lock();
            CheckBoxContent.State state = itemVal.getState();
            if (state != CheckBoxContent.State.UNDEFINED) {
                treeItem.getChildren().forEach(child -> UPDATE_DOWNWARDS.f.accept(child, state));
            }
            // once finished, unlock so updates via one of its children will propogate through the tree
            itemVal.unlock();
        }
    };

    public CheckBoxTreeCell() {
        this((content) -> content.getPath().toString());
    }

    public CheckBoxTreeCell(Function<C, String> stringConverter) {
        super();
        this.stringConverter = stringConverter;
    }

    @Override
    protected void updateItem(C item, boolean empty) {
        super.updateItem(item, empty);

        if (empty || item == null) {
            setText(null);
            checkBox.setGraphic(null);
            setGraphic(null);
        } else {
            // update the text
            setText(stringConverter.apply(getItem()));

            // update the graphic
            TreeItem<C> treeItem = getTreeItem();
            Node graphic = treeItem.getGraphic();
            checkBox.setGraphic( graphic != null ? graphic : null);
            setGraphic(checkBox);

            // unbind properties
            if (state != null) {
                checkBox.selectedProperty().unbindBidirectional(select);
                intermediateState.unsubscribe();
                state.removeListener(stateInvalidations);
            }

            // rebind properties
            state = treeItem.getValue().stateProperty();
            select = state.mapBidirectional(
                    s -> s == CheckBoxContent.State.CHECKED,
                    val -> val ? CheckBoxContent.State.CHECKED : CheckBoxContent.State.UNCHECKED
            );
            checkBox.selectedProperty().bindBidirectional(select);

            // using checkBox.intermediateProperty().bind(state.map(s -> s == UNDEFINED)); results in a
            // RunTimeException: a bounded property cannot be set
            // So, get around it by feeding state values into it.
            intermediateState = state.values()
                    .map(s -> s == CheckBoxContent.State.UNDEFINED)
                    .feedTo(checkBox.indeterminateProperty());

            state.addListener(stateInvalidations);
        }
    }
}