/*
 * Copyright (C) 2018 Andrew Gegg
 *
 *	This file is part of the Garden Notebook application
 *
 * The Garden Notebook application is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl.html>.
 */

/*
	Change log
    2.4.0   Pick up tab-out from editor/text field when entering new value
 */

package uk.co.gardennotebook;

import javafx.scene.control.skin.ComboBoxListViewSkin;
import java.util.Map;
import java.util.TreeMap;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.ListChangeListener;
import javafx.event.EventHandler;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.util.StringConverter;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.EntryMessage;

/**
 *	Support code to implement the various item Combo boxes used in the UI
*
*	Must be public to be loaded by FXML
 * 
 * @param <T>	The type of item shown in the ComboBox.  An item Bean.
*
*	@author	Andy Gegg
*	@version	2.4.0
*	@since	1.0
 */
abstract public class DiaryEntryCombo<T> extends ComboBox<T> 
{// NB must be public or fxml CANNOT see the 'mandatory' property
	
	private static final Logger LOGGER = LogManager.getLogger();

	private final TreeMap<String, T> conSet = new TreeMap<>();
	private final EventHandler<KeyEvent> keyHandlerEditable = (KeyEvent e) -> {
		this.show();
		KeyCode code = e.getCode();
		if (!(code.isLetterKey())) {
			return;
		}
		StringBuilder sb = new StringBuilder(this.getEditor().getText());
		sb.replace(this.getEditor().getSelection().getStart(), this.getEditor().getSelection().getEnd(), code.getName());
		scrollToBean(sb.toString());
	};
	private final EventHandler<KeyEvent> keyHandlerNonEditable = (KeyEvent e) -> {
		KeyCode code = e.getCode();
		scrollToBean(code.getName());
	};
	private final ListChangeListener<? super T> contentListener = chg -> {
		while (chg.next())
		{
			if (chg.wasAdded())
			{
				for (T item : chg.getAddedSubList())
				{
					conSet.putIfAbsent(this.getConverter().toString(item).toUpperCase(), item);
				}
			}
			else if (chg.wasRemoved())
			{
				for (T item : chg.getAddedSubList())
				{
					conSet.remove(this.getConverter().toString(item).toUpperCase());
				}
			}
		}
	};
	
	protected T currentValue;
	private final SimpleBooleanProperty mandatoryProperty = new SimpleBooleanProperty(this, "mandatory", false);

	DiaryEntryCombo()
	{
		super();
		this.getItems().addListener(contentListener);
		if (this.isEditable())
		{
			this.addEventHandler(KeyEvent.KEY_PRESSED, keyHandlerEditable);
		}
		else
		{
			this.addEventHandler(KeyEvent.KEY_PRESSED, keyHandlerNonEditable);
		}
		this.editableProperty().addListener((obs, old, newVal) -> {
			LOGGER.debug("editableProperty() listener: newVal: {}", newVal);
			this.removeEventHandler(KeyEvent.KEY_PRESSED, keyHandlerNonEditable);
			this.removeEventHandler(KeyEvent.KEY_PRESSED, keyHandlerEditable);
			if (newVal)
			{
				this.editorProperty().get().addEventHandler(KeyEvent.KEY_PRESSED, keyHandlerEditable);
				this.editorProperty().get().selectAll();
			}
			else
			{
				this.addEventHandler(KeyEvent.KEY_PRESSED, keyHandlerNonEditable);
			}
		});

        this.valueProperty().addListener((obs, old, newVal) -> {
            LOGGER.debug("valueProperty: listener: newVal: {}", newVal);
			currentValue = newVal;
		});

		this.setConverter(new StringConverter<T>(){
			@Override
			public T fromString(String string) {
				LOGGER.debug("converter: fromString: {}, mandatory: {}, currentValue: {}", string, mandatoryProperty.get(), currentValue);
				if (string == null || string.isEmpty())
				{
					if (!isMandatory()) return null;
					return currentValue;
				}
				if (conSet.containsKey(string.toUpperCase()))
				{
					LOGGER.debug(" converter: fromString: found: {}", conSet.get(string.toUpperCase()));
					return conSet.get(string.toUpperCase());
				}
				else
				{
					T added;
                    LOGGER.debug("converter: fromString: before call to newItem: string: {}", string);
					added = newItem(string);
                    LOGGER.debug("converter: fromString: after call to newItem: string: {}", string);
					conSet.put(string.toUpperCase(), added);
                    LOGGER.debug("converter: fromString: just before exit: added: {}", added);
					return added;
				}
			}

			@Override
			public String toString(T object) {
                LOGGER.debug("converter: toString: object: {}", object);
				return getName(object);
			}

		});
		this.onShownProperty().addListener(e -> {
            LOGGER.debug("DiaryEntryCombo: onShown");
			if ( this.getValue() != null)
			{
				scrollToBean(this.getConverter().toString(this.getValue()));
			}					
		});

        // 2.4.0
        this.getEditor().focusedProperty().addListener((obj, wasFocused, isFocused)->{
            if (wasFocused && !isFocused)
            {
                LOGGER.debug("in focus change - lost focus");
                this.setValue(this.getConverter().fromString( this.getEditor().getText() ));
            }
            
        });
}

	DiaryEntryCombo(T initialVal) 
	{
		this();
		this.setValue(initialVal);
	}

	private void scrollToBean(final String string)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("scrollToBean(): {}", string);
		if (conSet.isEmpty())
		{
			return;
		}
		if (!this.showingProperty().get())
		{
			LOGGER.debug("scrollToBean: says not showing");
			this.show();
		}
		T best;
		Map.Entry<String, T> ceil = conSet.ceilingEntry(string.toUpperCase());
		if (ceil == null)
		{
			LOGGER.debug("scrollToBean: off the end!");
			best = conSet.lastEntry().getValue();
		} 
		else
		{
			best = ceil.getValue();
		}
		LOGGER.debug("scrollToBean: best shot: {}", best);
		((ListView<T>)((ComboBoxListViewSkin) this.getSkin()).getPopupContent()).scrollTo(best);
	}
	
	abstract String getName(T item);
	abstract T newItem(String name);
	
	public final boolean isMandatory()
	{
		return mandatoryProperty.get();
	}
	public final void setMandatory(final boolean val)
	{
		mandatoryProperty.set(val);
	}
	public final BooleanProperty mandatoryProperty()
	{
		return mandatoryProperty;
	}

}
