package org.javafxdata.control.cell;

import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TableCell;
import javafx.util.Callback;

/**
 * <p>A class containing a {@link TableCell} implementation that draws a {@link CheckBox}
 * node inside the cell, optionally with a label to indicate what the checkbox represents.
 * 
 * <img src="http://www.javafxdata.org/screenshots/CheckBoxCell-TableView.PNG" 
 * style="float: right; padding: 0px 0px 0px 3px;"/>
 * 
 * <p>By default, the CheckBoxTableCell is rendered with a CheckBox centred in the TableColumn. If a 
 * label is required, it is necessary to provide a non-null toString argument to the 
 * {@link #CheckBoxTableCell(Callback, Callback)} constructor.
 * 
 * <p>To construct an instance of this class, it is necessary to provide a {@link Callback} that, given an 
 * object of type T, will return an {@code ObservableProperty<Boolean>} that represents whether
 * the given item is selected or not. This ObservableValue will be bound bidirectionally (meaning that the 
 * CheckBox in the cell will set/unset this property based on user interactions, and the CheckBox will reflect
 * the state of the ObservableValue, if it changes externally).
 * 
 * @param <T> The type of the elements contained within the TableColumn.
 * @author <a href="http://www.jonathangiles.net">Jonathan Giles</a>
 */
public class CheckBoxTableCell<S,T> extends TableCell<S,T> {
    private final CheckBox checkBox;
    
    private final boolean showLabel;
    
    private final Callback<T, String> toString;
    
    private final Callback<Integer, ObservableValue<Boolean>> getSelectedProperty;
    private ObservableValue<Boolean> booleanProperty;
    
    /**
     * Creates a default CheckBoxTableCell.
     */
    public CheckBoxTableCell() {
        this(null, null);
    }
    
    /**
     * Creates a default CheckBoxTableCell with a custom {@link Callback} to retrieve an
     * ObservableValue for a given cell index.
     * 
     * @param getSelectedProperty A {@link Callback} that will return an {@link ObservableValue} 
     *      given an index from the TableColumn.
     */
    public CheckBoxTableCell(Callback<Integer, ObservableValue<Boolean>> getSelectedProperty) {
        this(getSelectedProperty, null);
    }

    /**
     * Creates a CheckBoxTableCell with a custom string converter.
     * 
     * @param getSelectedProperty A {@link Callback} that will return a {@link ObservableValue} 
     *      given an index from the TableColumn.
     * @param toString A Callback that, give an object of type T, will return a String that can be used 
     *      to represent the object visually.
     */
    public CheckBoxTableCell(Callback<Integer, ObservableValue<Boolean>> getSelectedProperty, Callback<T, String> toString) {
        // we let getSelectedProperty be null here, as we can always defer to the
        // TableColumn
        this.getSelectedProperty = getSelectedProperty;
        this.toString = toString;
        this.showLabel = toString != null;
        
        this.checkBox = new CheckBox();
//        this.checkBox.setAlignment(Pos.TOP_CENTER);
        
        setAlignment(Pos.CENTER);
        setGraphic(checkBox);
        
        if (showLabel) {
            this.checkBox.setAlignment(Pos.CENTER_LEFT);
        }
    }
    
    /** {@inheritDoc} */
    @Override public void updateItem(T item, boolean empty) {
        super.updateItem(item, empty);
        
        if (empty) {
            setText(null);
            setGraphic(null);
        } else {
            if (showLabel) {
                setText(toString.call(item));
            }
            setGraphic(checkBox);
            
            if (booleanProperty instanceof BooleanProperty) {
                checkBox.selectedProperty().unbindBidirectional((BooleanProperty)booleanProperty);
            }
            ObservableValue obsValue = getSelectedProperty();
            if (obsValue instanceof BooleanProperty) {
                booleanProperty = obsValue;
                checkBox.selectedProperty().bindBidirectional((BooleanProperty)booleanProperty);
            }
            
            checkBox.disableProperty().bind(Bindings.not(
                    getTableView().editableProperty().and(
                    getTableColumn().editableProperty()).and(
                    editableProperty())
                ));
        }
    }
    
    private ObservableValue getSelectedProperty() {
        return getSelectedProperty != null ?
                getSelectedProperty.call(getIndex()) :
                getTableColumn().getCellObservableValue(getIndex());
    }
}