/*
 * Copyright (C) 2020 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.8.0   First version.
 */

package uk.co.gardennotebook.fxbean;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.EntryMessage;
import uk.co.gardennotebook.spi.GNDBException;
import uk.co.gardennotebook.spi.IProduct;

/**
	*	A facade on the ProductBean allowing free edit of the various fields.
    * 
    *   There are two legal possibilities:
    *       -   everything is null - initial state for a new 'parent' or a ShoppingList entry with no product
    *       -   a ProductCategory and a ProductName or PlantSpecies is set
	*
	*	@author	Andy Gegg
	*	@version	2.8.0
	*	@since	2.8.0
 */
final class ProxyProductBean {
    
	private static final Logger LOGGER = LogManager.getLogger();

    //  This holds the most recently saved Product
    //  Initially, whatever was passed to the constructor
    //  Subsequently, if explicitSave is FALSE, the last saved Product
    //  If explicitSave is TRUE, will be reset on Save (which is probably the last thing it ever does)
    //  It is used to initialise the local values and to check for changed product
    private ProductBean latestProduct;

    // The semantics of explicitSave, etc, are a bit different here from other *Beans
    // All the savable field values are held in the various Properties and NOT applied
    // to an extant Product - a new Product is created as needed - possibly even when NOT needed!
	private boolean explicitSave = false;
//	private IProductBuilder explicitBuilder = null;
	private final SimpleBooleanProperty saveRequiredProperty = new SimpleBooleanProperty(this, "saveRequired", explicitSave);
    // We can do this here (unlike in the other *Beans) because we do NOT set explicitSave in the constructor
	private final ChangeListener<Boolean> saveRequiredListener = this::onSaveRequiredChange;

    private final SimpleObjectProperty<ProductCategoryBean> productCategoryProperty = new SimpleObjectProperty<>(this, "productCategory", null);
	private final ChangeListener<ProductCategoryBean> productCategoryListener = this::onProductCategoryChange;
	private final ReadOnlyBooleanWrapper hasProductCategoryProperty = new ReadOnlyBooleanWrapper(this, "hasProductCategory", false);

    private final SimpleObjectProperty<ProductBrandBean> productBrandProperty = new SimpleObjectProperty<>(this, "productBrand", null);
	private final ChangeListener<ProductBrandBean> productBrandListener = this::onProductBrandChange;
	private final ReadOnlyBooleanWrapper hasProductBrandProperty = new ReadOnlyBooleanWrapper(this, "hasProductBrand", false);
    
    // remember, these can be PlantSpecies/Variety or just a name
    private final SimpleStringProperty nameProperty = new SimpleStringProperty(this, "name", "");
	private final ChangeListener<String> nameListener = this::onNameChange;
    private final SimpleStringProperty nameDetailProperty = new SimpleStringProperty(this, "nameDetail", "");
	private final ChangeListener<String> nameDetailListener = this::onNameDetailChange;
    
	private final SimpleObjectProperty<PlantSpeciesBean> plantSpeciesProperty = new SimpleObjectProperty<>(this, "plantSpecies", null);
	private final ChangeListener<PlantSpeciesBean> plantSpeciesListener = this::onPlantSpeciesChange;
	private final ReadOnlyBooleanWrapper hasPlantSpeciesProperty = new ReadOnlyBooleanWrapper(this, "hasPlantSpecies", false);
	private final SimpleObjectProperty<PlantVarietyBean> plantVarietyProperty = new SimpleObjectProperty<>(this, "plantVariety", null);
	private final ChangeListener<PlantVarietyBean> plantVarietyListener = this::onPlantVarietyChange;
	private final ReadOnlyBooleanWrapper hasPlantVarietyProperty = new ReadOnlyBooleanWrapper(this, "hasPlantVariety", false);
    
    // signal that a new Product has been set
    private final ReadOnlyBooleanWrapper hasValidProductProperty = new ReadOnlyBooleanWrapper(this, "hasValidProductProperty", false);
    private final ReadOnlyBooleanWrapper productChangedProperty = new ReadOnlyBooleanWrapper(this, "productChangedProperty", false);
    private final SimpleObjectProperty<ProductBean> productProperty = new SimpleObjectProperty<>(this, "productProperty", null);
	private final ChangeListener<ProductBean> productListener = this::onProductChange;
    
    ProxyProductBean()
    {
        this(null);
    }
    
    ProxyProductBean(final IProduct initialValue)
    {

        latestProduct = new ProductBean(initialValue);
        
        setValues(latestProduct);
    }
    
	public boolean isSaveRequired()
	{
		return explicitSave;
	}
	public void setSaveRequired(boolean reqd)
	{
		saveRequiredProperty.set(reqd);
	}
	public BooleanProperty saveRequiredProperty()
	{
		return saveRequiredProperty;
	}
	private void onSaveRequiredChange(ObservableValue<? extends Boolean> obs, boolean  old, boolean  nval)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("onSaveRequiredChange(): old={}, new={}", old, nval);

        if (nval && !explicitSave)
        {
            explicitSave = true;
        }
        if (!nval && explicitSave && (latestProduct != null))
        {
            explicitSave = false;
        }
    }


	public boolean hasValidProduct()
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ProxyProductBean: hasValidProduct()");
        return LOGGER.traceExit(log4jEntryMsg, hasValidProductProperty().getValue());
	}
	public ReadOnlyBooleanProperty hasValidProductProperty()
	{
 		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ProxyProductBean: hasValidProductPROPERTY()");
        hasValidProductProperty.set(isProductSpecSufficient());
		return LOGGER.traceExit(hasValidProductProperty.getReadOnlyProperty());
	}
    
	public boolean hasProductChanged()
	{
		return productChangedProperty().getValue();
	}
	/**
	*	Use this to check if the Product parent of the Product this Bean wraps is present
	*
	*	@return	true if this Product is linked to a ProductBrand
	*/
	public ReadOnlyBooleanProperty productChangedProperty()
	{
		return productChangedProperty.getReadOnlyProperty();
	}
    
    public ProductBean getProduct()
    {
        return productProperty.getValue();
    }
    
    public void setProduct(final ProductBean bean)
    {
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("ProxyProduct: setProduct(): bean: {}", bean);
        productProperty.setValue(bean);
    }
    
    public ObjectProperty<ProductBean> productProperty()
    {
        return productProperty;
    }

	/**
	*	Handle changes to the (proxy) Product value
	*
	*/
	private void onProductChange(ObservableValue<? extends ProductBean> obs, ProductBean old, ProductBean nval)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("onProductChange(): old: {}, new: {}", old, nval);
        setValues(nval);
    }

	public boolean hasProductCategory()
	{
		return hasProductCategoryProperty().getValue();
	}
	/**
	*	Use this to check if the ProductBrand parent of the Product this Bean wraps is present
	*
	*	@return	true if this Product is linked to a ProductBrand
	*/
	public ReadOnlyBooleanProperty hasProductCategoryProperty()
	{
		return hasProductCategoryProperty.getReadOnlyProperty();
	}
    
    public ProductCategoryBean getProductCategory()
    {
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("getProductCategory()");
        return productCategoryProperty.getValue();
    }
    
    public void setProductCategory(final ProductCategoryBean bean)
    {
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("setProductCategory(): bean: {}", bean);
        productCategoryProperty.setValue(bean);
    }
    
    public ObjectProperty<ProductCategoryBean> productCategoryProperty()
    {
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("productCategoryProperty()");
        return productCategoryProperty;
    }

	/**
	*	Handle changes to the (proxy) ProductCategory value.
	*
	*/
	private void onProductCategoryChange(ObservableValue<? extends ProductCategoryBean> obs, ProductCategoryBean old, ProductCategoryBean nval)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("onProductCategoryChange(): old: {}, new: {}", old, nval);
		if (nval == null)
		{
LOGGER.debug("onProductCategoryChange(): nval is null");
            setDefaults();
//            setProduct(null);
			return;
		}
        
		if (nval.sameAs(old))
		{
LOGGER.debug("onProductCategoryChange(): nval is sameAs old");
			return;
		}

        hasProductCategoryProperty.set(true);
        
        if (old != null && old.isPlantLike() && nval.isPlantLike())
        {// leave any PlantSpecies/Variety values and set up a new Product
            try {
                resetProduct();
            } catch (GNDBException ex) {
                LOGGER.error("onProductCategoryChange(): db error: {}", ex);
            }
           return;
        }
        
        if (nval.isPlantLike())
        {// old was NOT plantlike
            setPlantSpecies(null);
        }
        else
        {
            setName("");
            setNameDetail("");
        }
        
		LOGGER.traceExit(log4jEntryMsg);
	}

	public boolean hasProductBrand()
	{
		return hasProductBrandProperty().getValue();
	}
	/**
	*	Use this to check if the ProductBrand parent of the Product this Bean wraps is present
	*
	*	@return	true if this Product is linked to a ProductBrand
	*/
	public ReadOnlyBooleanProperty hasProductBrandProperty()
	{
		return hasProductBrandProperty.getReadOnlyProperty();
	}
    
    public ProductBrandBean getProductBrand()
    {
        return productBrandProperty.getValue();
    }
    
    public void setProductBrand(final ProductBrandBean bean)
    {
        productBrandProperty.setValue(bean);
    }
    
    public ObjectProperty<ProductBrandBean> productBrandProperty()
    {
        return productBrandProperty;
    }

	/**
	*	Handle changes to the (proxy) ProductBrand value
	*
	*/
	private void onProductBrandChange(ObservableValue<? extends ProductBrandBean> obs, ProductBrandBean old, ProductBrandBean nval)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("onProductBrandChange(): old={}, new={}", old, nval);
        
		if (nval != null && nval.sameAs(old))
		{
LOGGER.debug("onProductBrandChange(): nval is sameAs old");
			return;
		}
		hasProductBrandProperty.set(nval != null);
        
        try {
            resetProduct();
        } catch (GNDBException ex) {
            LOGGER.error("onProductBrandChange(): db error: {}", ex);
        }

    }
    

    public boolean hasPlantSpecies()
	{
		return hasPlantSpeciesProperty().getValue();
	}
	/**
	*	Use this to check if the PlantSpecies parent of the Product this Bean wraps is present
	*
	*	@return	true if this Product is linked to a PlantSpecies
	*/
	public ReadOnlyBooleanProperty hasPlantSpeciesProperty()
	{
		return hasPlantSpeciesProperty.getReadOnlyProperty();
	}

    public PlantSpeciesBean getPlantSpecies()
    {
        return plantSpeciesProperty.getValue();
    }
    
    public void setPlantSpecies(final PlantSpeciesBean bean)
    {
        plantSpeciesProperty.setValue(bean);
    }
    
    public ObjectProperty<PlantSpeciesBean> plantSpeciesProperty()
    {
        return plantSpeciesProperty;
    }

	/**
	*	Handle changes to the (proxy) ProductBrand value
	*
	*/
	private void onPlantSpeciesChange(ObservableValue<? extends PlantSpeciesBean> obs, PlantSpeciesBean old, PlantSpeciesBean nval)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("onPlantSpeciesChange(): old: {}, new: {}", old, nval);
        
        if (nval == null)
        {
LOGGER.debug("onPlantSpeciesChange(): nval is null");
            hasPlantSpeciesProperty.set(false);
            setPlantVariety(null);
LOGGER.debug("onPlantSpeciesChange(): old Name value: {}", nameProperty.get());
            setName("");
            setNameDetail("");

            productChangedProperty.setValue(false);

            return;
        }
        
        if (nval.sameAs(old))
        {
            return;
        }
        hasPlantSpeciesProperty.set(true);
        
        setPlantVariety(null);
        setName(nval.getCommonName());
        setNameDetail("");
        
        try {
            resetProduct();
        } catch (GNDBException ex) {
            LOGGER.error("onPlantSpeciesChange(): db error: {}", ex);
        }

        LOGGER.traceExit(log4jEntryMsg);
    }
    

	public boolean hasPlantVariety()
	{
		return hasPlantVarietyProperty().getValue();
	}
	/**
	*	Use this to check if the PlantVariety parent of the Product this Bean wraps is present
	*
	*	@return	true if this Product is linked to a PlantVariety
	*/
	public ReadOnlyBooleanProperty hasPlantVarietyProperty()
	{
		return hasPlantVarietyProperty.getReadOnlyProperty();
	}

    public PlantVarietyBean getPlantVariety()
    {
        return plantVarietyProperty.getValue();
    }
    
    public void setPlantVariety(final PlantVarietyBean bean)
    {
        plantVarietyProperty.setValue(bean);
    }
    
    public ObjectProperty<PlantVarietyBean> plantVarietyProperty()
    {
        return plantVarietyProperty;
    }

	/**
	*	Handle changes to the (proxy) ProductBrand value
	*
	*/
	private void onPlantVarietyChange(ObservableValue<? extends PlantVarietyBean> obs, PlantVarietyBean old, PlantVarietyBean nval)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("onPlantVarietyChange(): old={}, new={}", old, nval);
        
        if (nval == null)
        {
            plantVarietyProperty.setValue(null);
            hasPlantVarietyProperty.set(false);
            setNameDetail("");
            try {
                resetProduct();
            } catch (GNDBException ex) {
                LOGGER.error("onPlantVarietyChange(): db error: {}", ex);
            }
            return;
        }
        
        if (nval.sameAs(old))
        {
            return;
        }
        
        hasPlantVarietyProperty.set(true);
        setNameDetail(nval.getCommonName());

        try {
            resetProduct();
        } catch (GNDBException ex) {
            LOGGER.error("onPlantVarietyChange(): db error: {}", ex);
        }

        LOGGER.traceExit(log4jEntryMsg);
    }
    

    public String getName()
	{
		return nameProperty.get();
	}
	public void setName(final String newVal)
	{
		nameProperty.set(newVal);
	}
	/**
	*	Wraps the Name value of the Product
	*
	*	@return	a writable property wrapping the name attribute
	*/
	public StringProperty nameProperty()
	{
		return nameProperty;
	}

	/**
	*	Handle changes to the Name value
	*
	*/
	private void onNameChange(ObservableValue<? extends String> obs, String old, String nval)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("onNameChange(): old={}, new={}", old, nval);
        
        if (getProductCategory() != null && getProductCategory().isPlantLike())
            return; // caused by onPlantSpeciesChanged setting the name value

        if (nval == null)
            setName("");
        try {
            resetProduct();
        } catch (GNDBException ex) {
            LOGGER.error("onNameChange(): db error: {}", ex);
        }

        LOGGER.traceExit(log4jEntryMsg);
	}
    

    public String getNameDetail()
	{
		return nameDetailProperty.get();
	}
	public void setNameDetail(final String newVal)
	{
		nameDetailProperty.set(newVal);
	}
	/**
	*	Wraps the Name value of the Product
	*
	*	@return	a writable property wrapping the name attribute
	*/
	public StringProperty nameDetailProperty()
	{
		return nameDetailProperty;
	}

	/**
	*	Handle changes to the Name value
	*
	*/
	private void onNameDetailChange(ObservableValue<? extends String> obs, String old, String nval)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("onNameDetailChange(): old={}, new={}", old, nval);
        
        if (getProductCategory() != null && getProductCategory().isPlantLike())
            return; // caused by onPlantVarietyChanged setting the name value
        
        try {
            resetProduct();
        } catch (GNDBException ex) {
            LOGGER.error("onNameDetailChange(): db error: {}", ex);
        }

        LOGGER.traceExit(log4jEntryMsg);
	}
    
    public void save() throws GNDBException
    {
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("save()");
		if (!explicitSave) return;
        
        if (!isProductSpecSufficient()) return;
        
        createProduct();
    }
    
	private void addListeners()
	{
        LOGGER.debug("addListeners()");
        productProperty.addListener(productListener);
		productCategoryProperty.addListener(productCategoryListener);
		plantSpeciesProperty.addListener(plantSpeciesListener);
		plantVarietyProperty.addListener(plantVarietyListener);
		productBrandProperty.addListener(productBrandListener);
		nameProperty.addListener(nameListener);
		nameDetailProperty.addListener(nameDetailListener);
	}
	private void removeListeners()
	{
        LOGGER.debug("removeListeners()");
        productProperty.removeListener(productListener);
		productCategoryProperty.removeListener(productCategoryListener);
		plantSpeciesProperty.removeListener(plantSpeciesListener);
		plantVarietyProperty.removeListener(plantVarietyListener);
		productBrandProperty.removeListener(productBrandListener);
		nameProperty.removeListener(nameListener);
		nameDetailProperty.removeListener(nameDetailListener);
	}
    
    private void setValues(ProductBean bean)
    {// NB may be called Before listeners are set up
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("setValues(): bean: {}", bean);
        
        if (bean == null)
        {
            setDefaults();
            return;
        }
        
        removeListeners();
        setGivenValues(bean);
        addListeners();
    }
    
    private void setGivenValues(ProductBean bean)
    {// NB may be called Before listeners are set up
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("setGivenValues(): bean: {}", bean);
        
        if (bean == null /*|| bean.getValue().isEmpty()*/)
        {
            setDefaultValues();
            return;
        }
        
        setProductCategory(bean.getProductCategory());
        hasProductCategoryProperty.set(bean.getProductCategory().get().isPresent());
        if (bean.hasProductBrand())
        {
            setProductBrand(bean.getProductBrand());
            hasProductBrandProperty.set(true);
        }
        else
        {
            setProductBrand(null);
            hasProductBrandProperty.set(false);
        }
        if (bean.hasPlantSpecies())
        {
            hasPlantSpeciesProperty.set(true);
            setPlantSpecies(bean.getPlantSpecies());
            if (bean.hasPlantVariety())
            {
                hasPlantVarietyProperty.set(true);
                setPlantVariety(bean.getPlantVariety());
            }
        }
        else
        {
            hasPlantSpeciesProperty.set(false);
            setPlantSpecies(null);
            hasPlantVarietyProperty.set(false);
            setPlantVariety(null);
        }
        setName(bean.getName());
        setNameDetail(bean.getNameDetail_1());
        
        latestProduct = bean;
    }
    
    private void setDefaults()
    {
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("setDefaults()");

        removeListeners();
        setDefaultValues();
        addListeners();
    }

    private void setDefaultValues()
    {
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("setDefaultValues()");

        productBrandProperty.setValue(null);
        hasProductBrandProperty.set(false);
        hasProductCategoryProperty.set(false);
        plantSpeciesProperty.setValue(null);
        hasPlantSpeciesProperty.set(false);
        plantVarietyProperty.setValue(null);
        hasPlantVarietyProperty.set(false);
        nameProperty.set("");
        nameDetailProperty.set("");

        productChangedProperty.setValue(false);
        latestProduct = null;
    }

    /*    
    *   Check if we have sufficient conditions to check for a Product
    */
    private boolean isProductSpecSufficient()
    {
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("isProductSpecSufficient()");

        LOGGER.debug("isProductSpecSufficient(): productCat: {}", productCategoryProperty.getValue());
        if (productCategoryProperty.getValue() == null)
            return false;

        if (productCategoryProperty.getValue().isPlantLike())
        {
            LOGGER.debug("isProductSpecSufficient(): hasPlantSpecies: {}", hasPlantSpecies());
            if (!hasPlantSpecies())
                return false;
        }
        else if (nameProperty.getValue() == null || nameProperty.getValue().isBlank())
        {
            return LOGGER.traceExit(log4jEntryMsg, false);
        }
        return LOGGER.traceExit(log4jEntryMsg, true);
    }
    
    private void resetProduct() throws GNDBException
    {
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("resetProduct()");
        
        if (!isProductSpecSufficient())
            return;
        LOGGER.debug("resetProduct(): product is OK");
        
        ObservableList<ProductBean> products = FXCollections.emptyObservableList();
        if (getProductCategory().isPlantLike())
        {
            products = ProductBean.fetchAll(getProductCategory(), getProductBrand(), getPlantSpecies(), getPlantVariety());
        }
        else
        {
            products = ProductBean.fetchAll(getProductCategory(), getProductBrand(), getName(), getNameDetail());
        }
        LOGGER.debug("resetProduct(): products: {}", products);
        if (products.isEmpty() && !explicitSave)
        {
            createProduct();
            return;
        }
        
        ProductBean testProd = null;
        
        for (ProductBean pr : products)
        {
            if (compareProduct(pr))
            {
                testProd = pr;
                break;
            }
        }
        
        if (testProd != null)
        {// found a matching Product in the DB
            productChangedProperty.set(true);
            productProperty.setValue(testProd);
            return;
        }
        
        createProduct();
        
		LOGGER.traceExit(log4jEntryMsg);
    }

    // We can't use sameAs() to check for equality as we don't have a productId
    // for the putative Product (our edited state)
    /*
    * @return   true    if the two Products compare the same
    */
    private boolean compareProduct(final ProductBean other)
    {
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("compareProduct(): other: {}", other);
        
        if (other == null)
            return false;

        if (!other.getProductCategory().sameAs(getProductCategory())) return false;
        LOGGER.debug("compareProduct(): cats same");
        
        if (other.hasProductBrand() ^ hasProductBrand()) return false;
        // either both are Branded or neither is
        if (other.hasProductBrand() && !other.getProductBrand().sameAs(getProductBrand())) return false;
        LOGGER.debug("compareProduct(): brands same");
        
        if (other.hasPlantSpecies() ^ hasPlantSpecies()) return false;
        // again, either both or neither if here
        if (other.hasPlantSpecies() && !other.getPlantSpecies().sameAs(getPlantSpecies())) return false;
        LOGGER.debug("compareProduct(): plant species same");
        
        if (other.hasPlantVariety() ^ hasPlantVariety()) return false;
        // again, either both or neither if here
        if (other.hasPlantVariety() && !other.getPlantVariety().sameAs(getPlantVariety())) return false;
        LOGGER.debug("compareProduct(): plant varieties same");

        // name will always have a value, possibly a PlantSpecies.commonName
        if (!other.getName().equalsIgnoreCase(getName())) return false;
        LOGGER.debug("compareProduct(): names same");
        if (other.getNameDetail_1() == null && getNameDetail() == null) return true;    // no difference
        if (other.getNameDetail_1() == null ^ getNameDetail() == null) return false;
        // neither is null
        if (other.getNameDetail_1().isBlank() ^ getNameDetail().isBlank()) return false;
        LOGGER.debug("compareProduct(): details same");
        
        return true;
    }
    
    private void createProduct() throws GNDBException
    {
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("createProduct()");
        
        ProductBean newBean = new ProductBean();
        newBean.setSaveRequired(true);
        newBean.setProductCategory(getProductCategory());
        newBean.setProductBrand(getProductBrand());
        if (getProductCategory().isPlantLike())
        {
            newBean.setPlantSpecies(getPlantSpecies());
            newBean.setPlantVariety(getPlantVariety());
        }
        else
        {
            newBean.setName(getName());
            newBean.setNameDetail_1(getNameDetail());
        }
        newBean.save();
        newBean.setSaveRequired(false);
        
        productProperty().set(newBean);

        LOGGER.traceExit(log4jEntryMsg);
    }
    
	@Override
	public String toString()
	{
		return "ProxyProductBean wrapping base: " + latestProduct + ", current: " + getProduct();
	}

}
