/*
 * Copyright (C) 2018, 2019, 2021-2023 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/>.
 */

/*
	Change log
	2.0.1   Show correct Detail value for non-plant products
    2.5.0   Handle Sales
    3.0.0	Support Journal
	3.0.3	Add Location as the Detail column where appropriate.
	3.0.5	Switch to a 'factory' implementation to simplify the code
	3.2.0	Add from(INotebookBean) to facilitate Storyline
 */

package uk.co.gardennotebook;

import javafx.beans.property.*;
import uk.co.gardennotebook.fxbean.AfflictionEventBean;
import uk.co.gardennotebook.fxbean.CommentBean;
import uk.co.gardennotebook.fxbean.GroundworkBean;
import uk.co.gardennotebook.fxbean.HusbandryBean;
import uk.co.gardennotebook.fxbean.INotebookBean;
import uk.co.gardennotebook.fxbean.JournalBean;
import uk.co.gardennotebook.fxbean.PurchaseBean;
import uk.co.gardennotebook.fxbean.PurchaseItemBean;
import uk.co.gardennotebook.fxbean.SaleBean;
import uk.co.gardennotebook.fxbean.SaleItemBean;
import uk.co.gardennotebook.fxbean.WeatherBean;
import uk.co.gardennotebook.fxbean.WildlifeBean;
import uk.co.gardennotebook.spi.GNDBException;
import uk.co.gardennotebook.spi.IAfflictionEvent;
import uk.co.gardennotebook.spi.IGroundwork;
import uk.co.gardennotebook.spi.IHusbandry;
import uk.co.gardennotebook.spi.IJournal;
import uk.co.gardennotebook.spi.INotebookEntry;
import uk.co.gardennotebook.spi.IPurchase;
import uk.co.gardennotebook.spi.IPurchaseItem;
import uk.co.gardennotebook.spi.ISale;
import uk.co.gardennotebook.spi.ISaleItem;
import uk.co.gardennotebook.spi.IWeather;
import uk.co.gardennotebook.spi.IWildlife;
import uk.co.gardennotebook.spi.NotebookEntryType;
import uk.co.gardennotebook.util.StoryLineTree;
import java.time.LocalDate;
import javafx.beans.binding.StringBinding;
import javafx.collections.ObservableList;

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

/**
 *	An aggregate and facade on the various item beans which may appear in a diary
 *	Used to show the Diary
 * 
 * @author Andy Gegg
*	@version	3.2.0
*	@since	1.0
 */
abstract sealed class DiaryBean implements Comparable<DiaryBean>
		permits DiaryBeanAfflictionEvent,
				DiaryBeanGroundwork,
				DiaryBeanHusbandry,
				DiaryBeanJournal,
				DiaryBeanPurchase,
				DiaryBeanPurchaseItem,
				DiaryBeanSale,
				DiaryBeanSaleItem,
				DiaryBeanWeather,
				DiaryBeanWildlife
{
	
	private static final Logger LOGGER = LogManager.getLogger();

	static DiaryBean from(IAfflictionEvent paramVal)
	{
		return new DiaryBeanAfflictionEvent(new AfflictionEventBean(paramVal));
	}

	static DiaryBean from(AfflictionEventBean afflictionEventBean)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("from(): AfflictionEventBean:{}", afflictionEventBean);

		return new DiaryBeanAfflictionEvent(afflictionEventBean);
	}

	static DiaryBean from(IGroundwork groundwork)
	{
		return new DiaryBeanGroundwork(new GroundworkBean(groundwork));
	}
	static DiaryBean from(GroundworkBean groundworkBean)
	{
		return new DiaryBeanGroundwork(groundworkBean);
	}

	static DiaryBean from(IHusbandry husbandry)
	{
		return new DiaryBeanHusbandry(new HusbandryBean(husbandry));
	}
	static DiaryBean from(HusbandryBean husbandryBean)
	{
		return new DiaryBeanHusbandry(husbandryBean);
	}

	static DiaryBean from(IPurchase purchase)
	{
		return new DiaryBeanPurchase(new PurchaseBean(purchase));
	}
	static DiaryBean from(PurchaseBean purchaseBean)
	{
		return new DiaryBeanPurchase(purchaseBean);
	}

	static DiaryBean from(IPurchaseItem purchaseItem)
	{
		return new DiaryBeanPurchaseItem(new PurchaseItemBean(purchaseItem));
	}
	static DiaryBean from(PurchaseItemBean purchaseItemBean)
	{
		return new DiaryBeanPurchaseItem(purchaseItemBean);
	}

	static DiaryBean from(ISale saleItem)
	{
		return new DiaryBeanSale(new SaleBean(saleItem));
	}
	static DiaryBean from(SaleBean saleBean)
	{
		return new DiaryBeanSale(saleBean);
	}

	static DiaryBean from(ISaleItem saleItem)
	{
		return new DiaryBeanSaleItem(new SaleItemBean(saleItem));
	}
	static DiaryBean from(SaleItemBean saleItemBean)
	{
		return new DiaryBeanSaleItem(saleItemBean);
	}

	static DiaryBean from(IWeather weather)
	{
		return new DiaryBeanWeather(new WeatherBean(weather));
	}
	static DiaryBean from(WeatherBean weather)
	{
		return new DiaryBeanWeather(weather);
	}

	static DiaryBean from(IWildlife wildlife)
	{
		return new DiaryBeanWildlife(new WildlifeBean(wildlife));
	}
	static DiaryBean from(WildlifeBean wildlife)
	{
		return new DiaryBeanWildlife(wildlife);
	}

	static DiaryBean from(IJournal journal)
	{
		return new DiaryBeanJournal(new JournalBean(journal));
	}
	static DiaryBean from(JournalBean journal)
	{
		return new DiaryBeanJournal(journal);
	}

	static DiaryBean from(INotebookBean bean)
	{
		return switch (bean.getType())
		{
			case AFFLICTIONEVENT -> from((AfflictionEventBean) bean);
			case GROUNDWORK -> from((GroundworkBean) bean);
			case HUSBANDRY -> from((HusbandryBean) bean);
			case JOURNAL -> from((JournalBean) bean);
			case PURCHASE -> from((PurchaseBean) bean);
			case PURCHASEITEM -> from((PurchaseItemBean) bean);
			case SALE -> from((SaleBean) bean);
			case SALEITEM -> from((SaleItemBean) bean);
			case WEATHER -> from ((WeatherBean) bean);
			case WILDLIFE -> from ((WildlifeBean) bean);
			default -> throw new RuntimeException("unknown DIaryBEan type: "+ bean.getType());
		};
	}

	abstract INotebookBean getItem();

	abstract INotebookEntry getBaseItem();

	abstract Integer getKey();

	abstract NotebookEntryType getItemType();

	abstract boolean canDelete() throws GNDBException;

	boolean hasAncestor() throws GNDBException
	{
		return getItem().hasAncestor();
	}

	StoryLineTree<DiaryBean> getAncestors() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("getAncestors()");
		
		StoryLineTree<? extends INotebookBean> items = getItem().getAncestors();
		LOGGER.debug("getAncestors(): items: {}", items);
		if (items == null)
		{
			return StoryLineTree.emptyTree();
		}
		StoryLineTree<DiaryBean> beanTree = items.copyTree(bean -> {
			LOGGER.debug("copying: {}", bean);
			return switch (bean.getType())
					{
						case PURCHASEITEM -> DiaryBean.from((PurchaseItemBean) bean);
						case HUSBANDRY -> DiaryBean.from((HusbandryBean) bean);
						case AFFLICTIONEVENT -> DiaryBean.from((AfflictionEventBean) bean);
						case GROUNDWORK -> DiaryBean.from((GroundworkBean) bean);
						case SALEITEM -> DiaryBean.from((SaleItemBean) bean);
						default -> null;
					};
			});
		
		return LOGGER.traceExit("beanTree: {}", beanTree);
	}

	boolean hasDescendant() throws GNDBException
	{
		return getItem().hasDescendant();
	}

	StoryLineTree<DiaryBean> getDescendants() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("getDescendants()");
		
		StoryLineTree<? extends INotebookBean> items = getItem().getDescendants();
		LOGGER.debug("getDescendants(): items: {}", items);
		if (items == null)
		{
			return StoryLineTree.emptyTree();
		}
		StoryLineTree<DiaryBean> beanTree = items.<DiaryBean>copyTree(bean -> {
			LOGGER.debug("copying: {}", bean);
			return switch (bean.getType())
					{
						case PURCHASEITEM -> DiaryBean.from((PurchaseItemBean) bean);
						case HUSBANDRY -> DiaryBean.from((HusbandryBean) bean);
						case AFFLICTIONEVENT -> DiaryBean.from((AfflictionEventBean) bean);
						case GROUNDWORK -> DiaryBean.from((GroundworkBean) bean);
						case SALEITEM -> DiaryBean.from((SaleItemBean) bean);
						default -> null;
					};
			});
		
		return LOGGER.traceExit("beanTree: {}", beanTree);
	}

	/**
	 * The date of this diary entry
	 * 
	 * @return	the (value of) the dateProperty
	 */
	abstract LocalDate getDate();

	abstract void setDate(LocalDate date);

	abstract ObjectProperty<LocalDate> dateProperty();

	/**
	 * The title for this Diary entry - usually the activity
	 * 
	 * @return	the (value of) the mainTitleProperty
	 */
	abstract StringBinding getMainTitleText();

	abstract ObjectProperty mainTitleProperty();

	/**
	 * The first detail for this Diary entry - usually the plant species
	 * 
	 * @return	the (value of) the subTitleProperty
	 */
	abstract StringBinding getSubTitleText();

	abstract Property subTitleProperty();

	/**
	 * The second detail for this Diary entry - usually the plant variety
	 * 
	 * @return	the (value of) the detail2Property
	 */
	abstract StringBinding getVarietyText();

	abstract Property varietyProperty();

	/**
	 * The third detail for this Diary entry - a purchase item category
	 * 
	 * @return	the (value of) the detail3Property
	 */
	abstract StringBinding getDetailText();

	abstract Property detailProperty();

	abstract ObservableList<CommentBean> getComments();

	abstract SimpleObjectProperty<ObservableList<CommentBean>> commentProperty();

	@Override
	/**
	 * Implement the comparison for sorting.
	 * Items are first sorted in date order.
	 * if same date, then by item type
	 *      -   weather then husbandry, then affliction,
	 * 		-	then groundwork, then wildlife,
	 * 		-	then purchases and items within purchases,
	 * 		-	then sales and sale items within sales
	 * 		-	and journal entries
	 * 	Items of same type on same day are sorted by some criterion (e.g. key) to give a stable ordering.
	 */
	abstract public int compareTo(DiaryBean o);
//	public int compareTo(DiaryBean o)
//	{
//		EntryMessage log4jEntryMsg = LOGGER.traceEntry("compareTo(): this: {}. other: {}", this.toString(), o.toString());
////		System.out.println("IN DIARYBEAN COMPARATOR");
//		if (!this.dateProperty.get().equals(o.dateProperty.get()))
//		{//sort on date as primary key
//			return this.dateProperty.get().compareTo(o.dateProperty.get());
//		}
//		if (this.nodeType != o.nodeType)
//		{/*if same date, sort by item type - weather then husbandry, then affliction,
//		*	then groundwork, then wildlife,
//		*	then purchases and items within purchases,
//		*	and sales and sale items within sales
//		*	and journal entries
//		*/
//			if (this.nodeType == NotebookEntryType.WEATHER) return -1;
//			if (o.nodeType == NotebookEntryType.WEATHER) return +1;
//			if (this.nodeType == NotebookEntryType.HUSBANDRY) return -1;
//			if (o.nodeType == NotebookEntryType.HUSBANDRY) return +1;
//			if (this.nodeType == NotebookEntryType.AFFLICTIONEVENT) return -1;
//			if (o.nodeType == NotebookEntryType.AFFLICTIONEVENT) return +1;
//			if (this.nodeType == NotebookEntryType.GROUNDWORK) return -1;
//			if (o.nodeType == NotebookEntryType.GROUNDWORK) return +1;
//			if (this.nodeType == NotebookEntryType.WILDLIFE) return -1;
//			if (o.nodeType == NotebookEntryType.WILDLIFE) return +1;
//			if (o.nodeType == NotebookEntryType.JOURNAL) return +1;
//
//			if (this.nodeType == NotebookEntryType.PURCHASE)
//			{//sort purchases before sales
//				if (o.nodeType == NotebookEntryType.SALE || o.nodeType == NotebookEntryType.SALEITEM) return -1;
//				// the compare-to item must be a PurchaseItem
//				// compare the 2 Purchase ids so that PurchaseItems sort together with their Purchase
//				if (o.nodeType != NotebookEntryType.PURCHASEITEM) throw new IllegalArgumentException("Comparing DiaryBeans: this: "+this.nodeType+", other: "+o.nodeType);
////					LOGGER.debug("compareTo(): comparing purchase (this) id: {} to purchase item (other) id: {}", this.purchase.getKey(), o.purchaseItem.getPurchase().getKey());
//				if (this.purchase.getKey().equals(o.purchaseItem.getPurchase().getKey()))
//				{// sort PU before PI if the SAME Purchase id
//					return -1;
//				}
//				// otherwise sort purchases (PU and PIs) by Purchase id, so they come together
//				return this.purchase.getKey().compareTo(o.purchaseItem.getPurchase().getKey());
//			}
//			if (this.nodeType == NotebookEntryType.PURCHASEITEM)
//			{//sort purchases before sales
//				if (o.nodeType == NotebookEntryType.SALE || o.nodeType == NotebookEntryType.SALEITEM) return -1;
//				// the compare-to item must be a Purchase
//				// compare the 2 Purchase ids so that PurchaseItems sort together with their Purchase
//				if (o.nodeType != NotebookEntryType.PURCHASE) throw new IllegalArgumentException("Comparing DiaryBeans: this: "+this.nodeType+", other: "+o.nodeType);
////					LOGGER.debug("compareTo(): comparing purchase item (this) id: {} to purchase (other) id: {}", this.purchaseItem.getPurchase().getKey(), o.purchase.getKey());
//				if (this.purchaseItem.getPurchase().getKey().equals(o.purchase.getKey()))
//				{// sort PU before PI if the SAME Purchase id
//					return +1;
//				}
//				// otherwise sort purchases (PU and PIs) by Purchase id, so they come together
//				return this.purchaseItem.getPurchase().getKey().compareTo(o.purchase.getKey());
//			}
////				LOGGER.debug("sweep up PU and PI - how do I get here?");
//
//			// this (i.e. the item I am) can only be Sale or SaleItem
//			if (this.nodeType == NotebookEntryType.SALE)
//			{// so the compare-to is a sale item
//				if (o.nodeType != NotebookEntryType.SALEITEM) throw new IllegalArgumentException("Comparing DiaryBeans: this: "+this.nodeType+", other: "+o.nodeType);
//				// the compare-to item must be a SaleItem
//				// compare the 2 Sale ids so that SaleItems sort together with their Sale
//				if (this.sale.getKey().equals(o.saleItem.getSale().getKey()))
//				{// sort SA before SI if the SAME Purchase id
//					return -1;
//				}
//				return this.sale.getKey().compareTo(o.saleItem.getSale().getKey());
//			}
//			if (this.nodeType == NotebookEntryType.SALEITEM)
//			{
//				if (this.nodeType != NotebookEntryType.SALEITEM) throw new IllegalArgumentException("Comparing DiaryBeans: expecting PI, got: "+this.nodeType+", other: "+o.nodeType);
//				if (o.nodeType == NotebookEntryType.SALE)
//					return this.saleItem.getSale().getKey().compareTo(o.sale.getKey());
//				if (o.nodeType != NotebookEntryType.SALEITEM) throw new IllegalArgumentException("Comparing DiaryBeans: this: "+this.nodeType+", other: "+o.nodeType);
//				if (this.saleItem.getSale().getKey().equals(o.sale.getKey()))
//				{// sort SA before SI if the SAME Purchase id
//					return +1;
//				}
//				return this.saleItem.getSale().getKey().compareTo(o.sale.getKey());
//			}
//		}
//		else
//		{//same date and type
//			if (this.nodeType == NotebookEntryType.PURCHASE)
//			{/*
//			*	for purchases, sort on purchase id so that if there are
//			*	multiple purchases on the same day, the items remain properly attached!
//			*/
//				return this.purchase.getKey().compareTo(o.purchase.getKey());
//			}
//			if (this.nodeType == NotebookEntryType.PURCHASEITEM)
//			{/*
//			*	for purchase items, sort on purchase id so that if there are
//			*	multiple purchases on the same day, the items remain properly attached!
//			*/
//				return this.purchaseItem.getPurchase().getKey().compareTo(o.purchaseItem.getPurchase().getKey());
//			}
//			if (this.nodeType == NotebookEntryType.SALE)
//			{/*
//			*	for sale items, sort on sale id so that if there are
//			*	multiple sales on the same day, the items remain properly attached!
//			*/
//				return this.sale.getKey().compareTo(o.sale.getKey());
//			}
//			if (this.nodeType == NotebookEntryType.SALEITEM)
//			{/*
//			*	for sale items, sort on sale id so that if there are
//			*	multiple sales on the same day, the items remain properly attached!
//			*/
//				return this.saleItem.getSale().getKey().compareTo(o.saleItem.getSale().getKey());
//			}
//			//as a last resort, the first added item goes first
//			return this.itemKey.compareTo(o.itemKey);
//		}
//		return 0;
//	}
	
//	@Override
//	public boolean equals(Object o)
//	{
//		if (this == o) return true;
//		if (!(o instanceof DiaryBean oo)) return false;
//		if (oo.nodeType != this.nodeType) return false;
//		return (oo.itemKey).equals(this.itemKey);
//	}
//
//	@Override
//	public int hashCode()
//	{
//		int hash = 7;
//		hash = 17 * hash + Objects.hashCode(this.nodeType);
//		hash = 17 * hash + Objects.hashCode(this.itemKey);
//		return hash;
//	}
	
//	protected abstract static class TextValueBinding extends StringBinding
//	{
//		private final Property boundObj;
//
//		TextValueBinding(Property text)
//		{
//			super();
//			this.boundObj = text;
//			if (text == null) return;
//			super.bind(text);
//		}
//
//		Observable getBoundObj()
//		{
//			return this.boundObj;
//		}
//
//		@Override
//		public void dispose()
//		{
//			super.unbind(boundObj);
//			super.dispose();
//		}
//	}
//
//	private static class NullTextValueBinding extends StringBinding
//	{
//		private Property boundObj;
//
//		NullTextValueBinding()
//		{
//			super();
//		}
//
//		Observable getBoundObj()
//		{
//			return this.boundObj;
//		}
//
//		@Override
//		protected String computeValue() {
//			return "";
//		}
//
//		@Override
//		public void dispose()
//		{
//			super.dispose();
//		}
//	}
	
	
}
