/*
 *
 *  Copyright (C) 2021, 2022 Andrew Gegg
 *
 * 	This file is part of the Gardeners Notebook application
 *
 *  The Gardeners 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
	3.0.0   First version
    3.0.4	Comment handling
 */

package uk.co.gardennotebook.fxbean;

import javafx.beans.property.*;
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.*;
import uk.co.gardennotebook.util.StoryLineTree;

import java.beans.PropertyChangeListener;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/**
 *	A physical or conceptual location.
 *
 *	@author	Andy Gegg
 *	@version	3.0.4
 *	@since	3.0.0
 */
final public class LocationBean implements INotebookBean
{
    private static final Logger LOGGER = LogManager.getLogger();

    private ILocation baseItem = null;

    private Integer itemKey = 0;
    private boolean newItem = false;
    private boolean explicitSave = false;
    private final SimpleBooleanProperty saveRequiredProperty = new SimpleBooleanProperty(this, "saveRequired", explicitSave);
    private ILocationBuilder explicitBuilder = null;

    // handle changes to the base item itself
    private PropertyChangeListener baseItemDeleted;
    private PropertyChangeListener baseItemReplaced;

    private final SimpleObjectProperty<LocationBean> parentLocationProperty = new SimpleObjectProperty<>(this, "parentLocation", null);
    private final ChangeListener<LocationBean> parentLocationIdListener = this::onParentLocationIdChange;
    private final ReadOnlyBooleanWrapper hasParentLocationProperty = new ReadOnlyBooleanWrapper(this, "hasParentLocation", false);
    private final SimpleStringProperty nameProperty = new SimpleStringProperty(this, "name", "");
    private final ChangeListener<String> nameListener = this::onNameChange;
    private final ReadOnlyBooleanWrapper duplicateNameProperty = new ReadOnlyBooleanWrapper(this, "duplicateName", false);  //  2.2.0
    private final SimpleStringProperty descriptionProperty = new SimpleStringProperty(this, "description", "");
    private final ChangeListener<String> descriptionListener = this::onDescriptionChange;
    private final SimpleBooleanProperty underCoverProperty = new SimpleBooleanProperty(this, "underCover", false);
    private final ChangeListener<Boolean> underCoverListener = this::onUnderCoverChange;
    private final SimpleStringProperty geometryProperty = new SimpleStringProperty(this, "geometry", "");
    private final ChangeListener<String> geometryListener = this::onGeometryChange;

    private final ReadOnlyObjectWrapper<LocalDateTime> lastUpdatedProperty = new ReadOnlyObjectWrapper<>(this, "lastUpdated", LocalDateTime.now());
    private final ReadOnlyObjectWrapper<LocalDateTime> createdProperty = new ReadOnlyObjectWrapper<>(this, "created", LocalDateTime.now());

    private ObservableList<LocationBean> childrenLocation = null;
    private PropertyChangeListener baseItemSublocationChanged;

    private ReadOnlyBooleanWrapper canDeleteProperty = null;
    private final ReadOnlyBooleanWrapper hasAncestorProperty = new ReadOnlyBooleanWrapper(this, "hasAncestor", false);
    private final ReadOnlyBooleanWrapper hasDescendantProperty = new ReadOnlyBooleanWrapper(this, "hasDescendant", false);


    private ReadOnlyBooleanWrapper isNewProperty = new ReadOnlyBooleanWrapper(this, "isNew", newItem);

    private BeanCommentHandler<ILocation> beanCommentHandler;
    private final ReadOnlyStringWrapper commentTextProperty = new ReadOnlyStringWrapper(this, "commentText", "");	//	2.9.6

    /**
     *	Construct an 'empty' Bean.  Set the various property values then call save() to create the new LocationBean
     */
    public LocationBean()
    {
        this(null);
    }
    /**
     *	Construct a Bean wrapping the given Location
     *	If the parameter is null a new 'empty' Bean will be constructed
     *
     *	@param	initialValue	the Location to wrap.  If null an 'empty' bean will be constructed
     *
     */
    public LocationBean(final ILocation initialValue)
    {
        ChangeListener<Boolean> saveRequiredListener = (obs, old, nval) -> {
            if (nval && !explicitSave)
            {
                explicitSave = true;
                ITrug server = TrugServer.getTrugServer().getTrug();
                explicitBuilder = server.getLocationBuilder(baseItem);
            }
            if (!nval && explicitSave && (baseItem != null))
            {
                explicitSave = false;
                explicitBuilder = null;
            }
        };

        saveRequiredProperty.addListener(saveRequiredListener);

        if(initialValue == null)
        {
            newItem = true;
            //	add the listeners BEFORE setting values, or default values never get sent to the builder!
            addListeners();
            setDefaults();
            saveRequiredProperty.set(true);
            return;
        }

        baseItem = initialValue;

        itemKey = baseItem.getKey();

        newItem = false;
        setValues();

        addListeners();
        declareBaseListeners();
        addBaseListeners();
    }

    /**
     *	Returns all Location items wrapped as LocationBean.
     *
     *	@return	a collection of LocationBean beans
     *
     *	@throws	GNDBException	if the underlying persisted storage engine (e.g. database server) throws an exception
     *				The original error can be retrieved by <code>getCause()</code>
     */
    public static ObservableList<LocationBean> fetchAll() throws GNDBException
    {
        EntryMessage log4jEntryMsg = LOGGER.traceEntry("fetchAll()");
        ITrug server = TrugServer.getTrugServer().getTrug();
        ILocationLister gal = server.getLocationLister();
        List<LocationBean> ll = gal.fetch().stream()
                .collect(ArrayList::new, (c, e) -> c.add(new LocationBean(e)), ArrayList::addAll);
        LOGGER.traceExit();
        return FXCollections.observableArrayList(ll);
    }

    /**
     *	Returns all top level Location items wrapped as LocationBean.
     *
     *	@return	a collection of LocationBean beans
     *
     *	@throws	GNDBException	if the underlying persisted storage engine (e.g. database server) throws an exception
     *				The original error can be retrieved by <code>getCause()</code>
     */
    public static ObservableList<LocationBean> fetchTop() throws GNDBException
    {
        EntryMessage log4jEntryMsg = LOGGER.traceEntry("fetchTop()");
        ITrug server = TrugServer.getTrugServer().getTrug();
        ILocationLister gal = server.getLocationLister();
        List<LocationBean> ll = gal.forceTopLevel().fetch().stream()
                .collect(ArrayList::new, (c, e) -> c.add(new LocationBean(e)), ArrayList::addAll);
        LOGGER.traceExit();
        return FXCollections.observableArrayList(ll);
    }

    /**
     *  Fetch the descendants of this Location - daughters, granddaughters, etc.
     *
     * @param parent    fetch the descendants of this Location
     * @return  a list of Locations
     * @throws GNDBException    if there is an error in the data persistence engine
     */
    public static ObservableList<LocationBean> fetchDescendantLocations(LocationBean parent) throws GNDBException
    {
        EntryMessage log4jEntryMsg = LOGGER.traceEntry("fetchDescendants(): parent: {}", parent);
        ITrug server = TrugServer.getTrugServer().getTrug();
        if (parent == null || parent.get().isEmpty())
        {
            return FXCollections.emptyObservableList();
        }
        ILocationLister gal = server.getLocationLister();
        List<LocationBean> ll = gal.location(parent.get().orElse(null)).fetchDescendantLocations().stream()
                .collect(ArrayList::new, (c, e) -> c.add(new LocationBean(e)), ArrayList::addAll);
        LOGGER.traceExit();
        return FXCollections.observableArrayList(ll);
    }

    /**
     *	Returns the underlying Location, if present
     *
     *	@return	the underlying Location, if present
     */
    public Optional<ILocation> get()
    {
        return getValue();
    }

    /**
     *	Returns the underlying Location if present
     *
     *	@return	the underlying Location, if present
     */
    public Optional<ILocation> getValue()
    {
        return Optional.ofNullable(baseItem);
    }

    @Override
    public Integer getKey()
    {
        return itemKey;
    }

    @Override
    public NotebookEntryType getType()
    {
        return NotebookEntryType.LOCATION;
    }

    @Override
    public boolean sameAs(INotebookBean other)
    {
        if (other == null || ((LocationBean)other).baseItem == null || baseItem == null)
        {
            return false;
        }
        if (other.getType() != NotebookEntryType.LOCATION)
        {
            return false;
        }
        return baseItem.sameAs(((LocationBean)other).baseItem);
    }

    @Override
    public boolean isNew()
    {
        return isNewProperty().get();
    }

    @Override
    public ReadOnlyBooleanProperty isNewProperty()
    {
        if (isNewProperty == null)
        {
            isNewProperty = new ReadOnlyBooleanWrapper(this, "isNew", newItem);
        }
        return isNewProperty.getReadOnlyProperty();
    }

    @Override
    public boolean canDelete() throws GNDBException
    {
        return canDeleteProperty().get();
    }

    @Override
    public ReadOnlyBooleanProperty canDeleteProperty() throws GNDBException
    {
        if (canDeleteProperty == null)
        {
            ITrug server = TrugServer.getTrugServer().getTrug();
            boolean canDel = server.getLocationBuilder(baseItem).canDelete();
            canDeleteProperty = new ReadOnlyBooleanWrapper(this, "canDelete", canDel);
        }
        return canDeleteProperty.getReadOnlyProperty();
    }

    @Override
    public boolean hasAncestor() throws GNDBException
    {
        //	Location items do not participate in story lines
        return false;
    }

    @Override
    public ReadOnlyBooleanProperty hasAncestorProperty() throws GNDBException
    {
        //	Location items do not participate in story lines
        return hasAncestorProperty.getReadOnlyProperty();
    }

    @Override
    public StoryLineTree<? extends INotebookBean> getAncestors() throws GNDBException
    {
        //	Location items do not participate in story lines
       return StoryLineTree.emptyTree();
    }

    @Override
    public boolean hasDescendant() throws GNDBException
    {
        //	Location items do not participate in story lines
        return false;
    }

    @Override
    public ReadOnlyBooleanProperty hasDescendantProperty() throws GNDBException
    {
        //	Location items do not participate in story lines
        return hasDescendantProperty.getReadOnlyProperty();
    }

    @Override
    public StoryLineTree<? extends INotebookBean> getDescendants() throws GNDBException
    {
        //	Location items do not participate in story lines
        return StoryLineTree.emptyTree();
    }

//  Location fields ----------------------------------------------------------

    public boolean hasParent()
    {
        return hasParentLocationProperty.get();
    }
    public ReadOnlyBooleanProperty hasParentProperty()
    {
        return hasParentLocationProperty.getReadOnlyProperty();
    }

    public LocationBean getParentLocation()
    {
        return parentLocationProperty().getValue();
    }
    public void setParentLocation(final LocationBean bean)
    {
        parentLocationProperty().setValue(bean);
    }
    public void setParentLocation(final ILocation item)
    {
        parentLocationProperty().setValue(new LocationBean(item));
    }
    /**
     *	Returns the Location parent of the Location this Bean wraps
     *
     *	@return	the Location parent of the Location this Bean wraps
     */
    public ObjectProperty<LocationBean> parentLocationProperty()
    {
        return parentLocationProperty;
    }

    /**
     *	Handle changes to the parentLocationId value
     *
     *	@throws	GNDBRuntimeException	if the underlying persisted storage engine (e.g. database server) throws an exception
     *				The original error can be retrieved by <code>getCause()</code>
     */
    private void onParentLocationIdChange(ObservableValue<? extends LocationBean> obs, LocationBean old, LocationBean nval)
    {
        EntryMessage log4jEntryMsg = LOGGER.traceEntry("onParentLocationIdChange(): old={}, new={}", old, nval);

        if (nval != null && nval.sameAs(old))
        {
            LOGGER.debug("onParentLocationIdChange(): nval is sameAs old");
            return;
        }
        hasParentLocationProperty.set(nval != null);
        if ((nval != null) && !nval.isNew())
        {
            if (explicitSave)
            {
                explicitBuilder.parentLocation(nval.get().get());
            }
            else
            {
                LOGGER.debug("onParentLocationIdChange(): NOT explicitSave");
                ITrug server = TrugServer.getTrugServer().getTrug();
                //	the Builder will send an event to the baseItem to say it's been replaced
                try
                {
                    server.getLocationBuilder(baseItem).parentLocation(nval.get().get()).save();
                } catch (GNDBException ex) {
                    throw new GNDBRuntimeException(ex);
                }
            }
        }
        else if (nval == null)
        {
            if (explicitSave)
            {
                explicitBuilder.parentLocation(null);
            }
            else
            {
                LOGGER.debug("onParentLocationIdChange(): NOT explicitSave");
                ITrug server = TrugServer.getTrugServer().getTrug();
                //	the Builder will send an event to the baseItem to say it's been replaced
                try
                {
                    server.getLocationBuilder(baseItem).parentLocation(null).save();
                } catch (GNDBException ex) {
                    throw new GNDBRuntimeException(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 Location
     *
     *	@return	a writable property wrapping the name attribute
     */
    public StringProperty nameProperty()
    {
        return nameProperty;
    }

    private void onNameChange(ObservableValue<? extends String> obs, String old, String nval)
    {
        EntryMessage log4jEntryMsg = LOGGER.traceEntry("onNameChange(): old={}, new={}", old, nval);
        if (explicitSave)
        {
            LOGGER.debug("onNameChange(): explicitSave");
            try
            {
                explicitBuilder.name(nval);
            } catch (GNDBException ex) {
                throw new GNDBRuntimeException(ex);
            }
        }
        else
        {
            LOGGER.debug("onNameChange(): NOT explicitSave");
            ITrug server = TrugServer.getTrugServer().getTrug();
            try
            {
                baseItem = server.getLocationBuilder(baseItem).name(nval).save();
            } catch (GNDBException ex) {
                throw new GNDBRuntimeException(ex);
            }
        }
        LOGGER.traceExit(log4jEntryMsg);
    }

    /**
     * Use this to check if the name being applied is a duplicate of an existing name.
     * NB relies on calling {@code checkForDuplicateName} on focusLost.
     *
     * @return a read-only indication that the name being set is a duplicate of an existing name
     */
    public ReadOnlyBooleanProperty duplicateNameProperty()
    {
        return duplicateNameProperty.getReadOnlyProperty();
    }

    /**
     *  Must be called from the editor when the user has finished entering a new name value,
     * typically on a lost focus event.
     *
     * @param newVal    the name the user is attempting to give
     * @return  true if newVal duplicates an existing name
     */
    public boolean checkForDuplicateName(final String newVal)
    {
        boolean duplicate = false;

        if (explicitSave)
        {
            LOGGER.debug("checkNameDuplicate(): explicitSave");
            try
            {
                duplicate = explicitBuilder.isNameDuplicate(newVal);
            } catch (GNDBException ex) {
                throw new GNDBRuntimeException(ex);
            }
        }
        else
        {
            LOGGER.debug("checkNameDuplicate(): NOT explicitSave");
            ITrug server = TrugServer.getTrugServer().getTrug();
            try
            {
                duplicate = server.getLocationBuilder(baseItem).isNameDuplicate(newVal);
            } catch (GNDBException ex) {
                throw new GNDBRuntimeException(ex);
            }
        }
        duplicateNameProperty.set(duplicate);
        return duplicate;
    }

    public String getDescription()
    {
        return descriptionProperty.get();
    }
    public void setDescription(final String newVal)
    {
        descriptionProperty.set(newVal);
    }
    /**
     *	Wraps the Description value of the Location
     *
     *	@return	a writable property wrapping the description attribute
     */
    public StringProperty descriptionProperty()
    {
        return descriptionProperty;
    }

    private void onDescriptionChange(ObservableValue<? extends String> obs, String old, String nval)
    {
        EntryMessage log4jEntryMsg = LOGGER.traceEntry("onDescriptionChange(): old={}, new={}", old, nval);
        if (explicitSave)
        {
            LOGGER.debug("onDescriptionChange(): explicitSave");
            explicitBuilder.description(nval);
        }
        else
        {
            LOGGER.debug("onDescriptionChange(): NOT explicitSave");
            ITrug server = TrugServer.getTrugServer().getTrug();
            try
            {
                baseItem = server.getLocationBuilder(baseItem).description(nval).save();
            } catch (GNDBException ex) {
                throw new GNDBRuntimeException(ex);
            }
        }
        LOGGER.traceExit(log4jEntryMsg);
    }

    public boolean isUnderCover()
    {
        return underCoverProperty.get();
    }
    public void setUnderCover(final boolean newVal)
    {
        underCoverProperty.set(newVal);
    }
    /**
     *	Wraps the underCover value of the Reminder
     *
     *	@return	a writable property wrapping the singleShot attribute
     */
    public BooleanProperty underCoverProperty()
    {
        return underCoverProperty;
    }

    /**
     *	Handle changes to the underCover value
     *
     *	@throws	GNDBRuntimeException	if the underlying persisted storage engine (e.g. database server) throws an exception
     *				The original error can be retrieved by <code>getCause()</code>
     */
    private void onUnderCoverChange(ObservableValue<? extends Boolean> obs, boolean old, boolean nval)
    {
        EntryMessage log4jEntryMsg = LOGGER.traceEntry("onUnderCoverChange(): old={}, new={}", old, nval);
        if (explicitSave)
        {
            LOGGER.debug("onUnderCoverChange(): explicitSave");
            explicitBuilder.underCover(nval);
        }
        else
        {
            LOGGER.debug("onUnderCoverChange(): NOT explicitSave");
            ITrug server = TrugServer.getTrugServer().getTrug();
            try
            {
                baseItem = server.getLocationBuilder(baseItem).underCover(nval).save();
            } catch (GNDBException ex) {
                throw new GNDBRuntimeException(ex);
            }
        }
        LOGGER.traceExit(log4jEntryMsg);
    }

    public String getGeometry()
    {
        return geometryProperty.get();
    }
    public void setGeometry(final String newVal)
    {
        geometryProperty.set(newVal);
    }
    /**
     *	Wraps the Description value of the Location
     *
     *	@return	a writable property wrapping the description attribute
     */
    public StringProperty geometryProperty()
    {
        return geometryProperty;
    }

    private void onGeometryChange(ObservableValue<? extends String> obs, String old, String nval)
    {
        EntryMessage log4jEntryMsg = LOGGER.traceEntry("onGeometryChange(): old={}, new={}", old, nval);
        if (explicitSave)
        {
            LOGGER.debug("onGeometryChange(): explicitSave");
            explicitBuilder.geometry(nval);
        }
        else
        {
            LOGGER.debug("onGeometryChange(): NOT explicitSave");
            ITrug server = TrugServer.getTrugServer().getTrug();
            try
            {
                baseItem = server.getLocationBuilder(baseItem).geometry(nval).save();
            } catch (GNDBException ex) {
                throw new GNDBRuntimeException(ex);
            }
        }
        LOGGER.traceExit(log4jEntryMsg);
    }

    public LocalDateTime getLastUpdated()
    {
        return lastUpdatedProperty.get();
    }
    /**
     *	Wraps the LastUpdated value of the Location
     *	Note that this value cannot be changed by the user
     *
     *	@return	a read-only property wrapping the lastUpdated attribute
     */
    public ReadOnlyObjectProperty<LocalDateTime> lastUpdatedProperty()
    {
        return lastUpdatedProperty.getReadOnlyProperty();
    }

    public LocalDateTime getCreated()
    {
        return createdProperty.get();
    }
    /**
     *	Wraps the Created value of the Location
     *	Note that this value cannot be changed by the user
     *
     *	@return	a read-only property wrapping the created attribute
     */
    public ReadOnlyObjectProperty<LocalDateTime> createdProperty()
    {
        return createdProperty.getReadOnlyProperty();
    }

    /**
     *	Return a list of any sub-locations of this Location or an empty list.  These arre the immediate daughters.
     *
     *	@return	A list of Location items, possibly empty
     *
     *	@throws	GNDBException	if the underlying persisted storage engine (e.g. database server) throws an exception
     *				The original error can be retrieved by <code>getCause()</code>
     */
    public ObservableList<LocationBean> getSubLocations() throws GNDBException
    {
        EntryMessage log4jEntryMsg = LOGGER.traceEntry("getSublocation(): this location: {}", this);
        if (childrenLocation == null)
        {
            childrenLocation = FXCollections.observableArrayList();
            ITrug server = TrugServer.getTrugServer().getTrug();
            for (ILocation ix : server.getLocationLister().location(baseItem).fetchSubLocations())
            {
                childrenLocation.add(new LocationBean(ix));
            }
        }
        LOGGER.debug("childrenLocation: {}", childrenLocation);
        LOGGER.traceExit(log4jEntryMsg);
        return childrenLocation;
    }

//  Comment handling --------------------------------------------------------

    @Override
    public ObservableList<CommentBean> getComments()
    {
        EntryMessage log4jEntryMsg = LOGGER.traceEntry("getComments()");

        return LOGGER.traceExit(log4jEntryMsg, beanCommentHandler.getComments());
    }

    @Override
    public ReadOnlyStringProperty commentTextProperty()
    {
//        return beanCommentHandler.commentTextProperty();
        commentTextProperty.set(beanCommentHandler.commentTextProperty().get());
        return commentTextProperty;
    }

    @Override
    public void addComment(String text) throws GNDBException
    {
        EntryMessage log4jEntryMsg = LOGGER.traceEntry("addComment({})", text);
        if (text == null || text.isBlank()) return;

        beanCommentHandler.addComment(text);

        if (explicitSave)
        {
            LOGGER.debug("addComment(): explicitSave");
        }
        else
        {	//	this cannot be a new instance of the parent Location
            LOGGER.debug("addComment(): NOT explicitSave");
            ITrug server = TrugServer.getTrugServer().getTrug();
            baseItem = server.getLocationBuilder(baseItem).addComment(text).save();
            setValues();
        }
        LOGGER.traceExit(log4jEntryMsg);
    }

    @Override
    public void addComment(CommentBean comment) throws GNDBException
    {
        EntryMessage log4jEntryMsg = LOGGER.traceEntry("addComment(comment bean): comment: {}, text : {}", comment, comment==null ? "null" :comment.getComment());
        if (comment == null) return;
        if (comment.getParentType() != this.getType()) return;
        if (comment.getComment() == null || comment.getComment().isBlank()) return;

        beanCommentHandler.addComment(comment);

        if (explicitSave)
        {
            LOGGER.debug("addComment(): explicitSave");
        }
        else
        {	//	this cannot be a new instance of the parent Location
            LOGGER.debug("addComment(): NOT explicitSave");
            ITrug server = TrugServer.getTrugServer().getTrug();
            baseItem = server.getLocationBuilder(baseItem).addComment(comment.getComment()).save();
            setValues();
        }
        LOGGER.debug("addComment(comment bean): commentTextProperty: {}", commentTextProperty().get());

        LOGGER.traceExit(log4jEntryMsg);
    }

    @Override
    public void changeCommentText(CommentBean comment, String text) throws GNDBException
    {
        EntryMessage log4jEntryMsg = LOGGER.traceEntry("changeCommentText(): comment={}, text={}", comment, text);
        if (text == null || text.isBlank()) return;

        if (comment == null)
        {
            addComment(text);
            return;
        }

        if (comment.getParentType() != this.getType()) return;

        beanCommentHandler.changeCommentText(comment, text);

        if (explicitSave)
        {
            LOGGER.debug("changeCommentText(): explicitSave");
        }
        else
        {	//	this cannot be a new instance of the parent Location
            LOGGER.debug("changeCommentText(): NOT explicitSave");
            ITrug server = TrugServer.getTrugServer().getTrug();
            baseItem = server.getLocationBuilder(baseItem).changeComment(comment.get().get(), text).save();
            setValues();
        }
        LOGGER.traceExit(log4jEntryMsg);
    }

    @Override
    public void changeCommentDate(CommentBean comment, LocalDate date) throws GNDBException
    {
        EntryMessage log4jEntryMsg = LOGGER.traceEntry("changeCommentDate(): comment={}, date={}", comment, date);
        if (date == null) return;

        if (comment == null)
        {
            return;
        }

        if (comment.getParentType() != this.getType()) return;

        beanCommentHandler.changeCommentDate(comment, date);

        if (explicitSave)
        {
            LOGGER.debug("changeCommentDate(): explicitSave");
        }
        else
        {	//	this cannot be a new instance of the parent Location
            LOGGER.debug("changeCommentDate(): NOT explicitSave");
            ITrug server = TrugServer.getTrugServer().getTrug();
            baseItem = server.getLocationBuilder(baseItem).changeComment(comment.get().get(), date).save();
            setValues();
        }
        LOGGER.traceExit(log4jEntryMsg);
    }

    @Override
    public void deleteComment(CommentBean comment) throws GNDBException
    {
        EntryMessage log4jEntryMsg = LOGGER.traceEntry("deleteComment(): comment={}", comment);
        if (comment == null) return;

        if (comment.getParentType() != this.getType()) return;

        beanCommentHandler.deleteComment(comment);

        if (explicitSave)
        {
            LOGGER.debug("deleteComment(): explicitSave");
        }
        else
        {	//	this cannot be a new instance of the parent Location
            LOGGER.debug("deleteComment(): NOT explicitSave");
            ITrug server = TrugServer.getTrugServer().getTrug();
            baseItem = server.getLocationBuilder(baseItem).deleteComment(comment.get().get()).save();
            setValues();
        }
        LOGGER.traceExit(log4jEntryMsg);
    }

//  update handling ----------------------------------------------------------------------------------------------------

    public boolean isSaveRequired()
    {
        return explicitSave;
    }
    public void setSaveRequired(boolean reqd)
    {
        saveRequiredProperty.set(reqd);
    }
    public BooleanProperty saveRequiredProperty()
    {
        return saveRequiredProperty;
    }

    public boolean needSave()
    {
        if (!explicitSave)
            return false;

        return explicitBuilder.needSave() || beanCommentHandler.needSave();
    }

    public boolean canSave()
    {
        if (!explicitSave)
            return true;

        return explicitBuilder.canSave();
    }

    /**
     *	Save changes to the underlying Location item
     *
     *	@throws	GNDBException	if the underlying persisted storage engine (e.g. database server) throws an exception
     *				The original error can be retrieved by <code>getCause()</code>
     */
    public void save() throws GNDBException
    {
        EntryMessage log4jEntryMsg = LOGGER.traceEntry("save()");
        if (!explicitSave) return;

        saveComments();	//	do this here so that explicitBuilder knows there's a change

        if (!explicitBuilder.needSave())
        {
            return;
        }
        if (!explicitBuilder.canSave())
        {
            throw new IllegalStateException("LocationBean: cannot save at this time - mandatory values not set");
        }

        baseItem = explicitBuilder.save();
        LOGGER.debug("save(): after explicitBuilder.save(): comments: {}", ()->baseItem.getComments());
        setValues();
        saveRequiredProperty.set(false);
        LOGGER.traceExit(log4jEntryMsg);
    }	//	save()

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

        beanCommentHandler.saveComments(
                cb -> explicitBuilder.addComment(cb.getComment()),	//	add
                cb -> explicitBuilder.changeComment(cb.get().get(), cb.getComment()),	//	change text
                cb -> explicitBuilder.changeComment(cb.get().get(), cb.getDate()),	//	change date
                cb -> explicitBuilder.deleteComment(cb.get().get())		//	delete
        );
    }

    /**
     *	Delete the underlying Location item
     *
     *	@throws	GNDBException	if the underlying persisted storage engine (e.g. database server) throws an exception
     *				The original error can be retrieved by <code>getCause()</code>
     */
    public void delete() throws GNDBException
    {
        EntryMessage log4jEntryMsg = LOGGER.traceEntry("delete()");
        if (newItem) return;

        if (explicitSave)
        {
            explicitBuilder.delete();
            saveRequiredProperty.set(false);
        }
        else
        {
            ITrug server = TrugServer.getTrugServer().getTrug();
            server.getLocationBuilder(baseItem).delete();
        }
    }	//	delete()

    public void cancelEdit()
    {
        if (!explicitSave) return;
        if (newItem) return;

        saveRequiredProperty.set(false);
        setValues();
    }

    private void setDefaults()
    {
        saveRequiredProperty.setValue(false);
        hasParentLocationProperty.set(false);
        parentLocationProperty.setValue(null);
        nameProperty.setValue("");
        descriptionProperty.setValue("");
        underCoverProperty.setValue(false);
        geometryProperty.setValue("");
        lastUpdatedProperty.setValue(LocalDateTime.now());
        createdProperty.setValue(LocalDateTime.now());
        childrenLocation = null;
        baseItemSublocationChanged = null;

        isNewProperty.set(true);
        beanCommentHandler = new BeanCommentHandler<>(this, baseItem);
        commentTextProperty.set(beanCommentHandler.commentTextProperty().get());
    }

    private void setValues()
    {
        saveRequiredProperty.setValue(false);
        if (baseItem.hasParent())
        {
            hasParentLocationProperty.set(true);
            parentLocationProperty.setValue(new LocationBean(baseItem.getParentLocation().orElse(null)));
        }
        else
        {
            hasParentLocationProperty.set(false);
            parentLocationProperty.setValue(null);

        }
        nameProperty.setValue(baseItem.getName());
        descriptionProperty.setValue(baseItem.getDescription().orElse(""));
        underCoverProperty.setValue(baseItem.isUnderCover());
        geometryProperty.setValue(baseItem.getGeometry().orElse(""));
        lastUpdatedProperty.setValue(baseItem.getLastUpdated());
        createdProperty.setValue(baseItem.getCreated());

        itemKey = baseItem.getKey();
        newItem = false;
        isNewProperty.set(false);

        LOGGER.debug("setvalues(): about to change BeanCommentHandler");
        beanCommentHandler = new BeanCommentHandler<>(this, baseItem);
        commentTextProperty.set(beanCommentHandler.commentTextProperty().get());
    }

    private void addListeners()
    {
        parentLocationProperty.addListener(parentLocationIdListener);
        nameProperty.addListener(nameListener);
        descriptionProperty.addListener(descriptionListener);
        underCoverProperty.addListener(underCoverListener);
        geometryProperty.addListener(geometryListener);
    }
    private void removeListeners()
    {
        parentLocationProperty.removeListener(parentLocationIdListener);
        nameProperty.removeListener(nameListener);
        descriptionProperty.removeListener(descriptionListener);
        underCoverProperty.removeListener(underCoverListener);
        geometryProperty.removeListener(geometryListener);
    }

    private void declareBaseListeners()
    {
        // handle changes to the base item itself
        baseItemDeleted = evt -> {
            removeListeners();
            removeBaseListeners();
            setDefaults();
            baseItem = null;
        };
        baseItemReplaced = evt -> {
            if (evt.getNewValue() != null)
            {
                removeBaseListeners();
                baseItem = (ILocation)(evt.getNewValue());
                setValues();
                addBaseListeners();
            }
        };

        baseItemSublocationChanged = evt -> {
            if (childrenLocation == null)
            {
                return;
            }
            if (evt.getNewValue() != null)
            {
                if (!(evt.getNewValue() instanceof ILocation))
                {
                    throw new IllegalArgumentException("baseItemSubocationChanged: newVal wrong type");
                }
                childrenLocation.add(new LocationBean((ILocation) (evt.getNewValue())));
            }
            else if (evt.getOldValue() != null)
            {
                if (!(evt.getOldValue() instanceof ILocation))
                {
                    throw new IllegalArgumentException("baseItemSublocationChanged: oldVal wrong type");
                }
                //	When the db item is deleted it fires an event which is picked up here AND in the child bean
                //	The child bean sets its underlying baseItem to null so getValue() returns an Optional of null
                //	The order in which the event handlers are called is unpredictable
                childrenLocation.removeIf(pr -> (pr.getValue().isEmpty()) ||
                        (pr.getValue().
                                get().
                                getKey().
                                equals(((ILocation)(evt.getOldValue())).getKey())));
            }
        };
    }

    private void addBaseListeners()
    {
        if (baseItem == null) return;
        baseItem.addPropertyChangeListener("deleted", baseItemDeleted);
        baseItem.addPropertyChangeListener("replaced", baseItemReplaced);

        baseItem.addPropertyChangeListener("Location", baseItemSublocationChanged);
    }
    private void removeBaseListeners()
    {
        if (baseItem == null) return;
        baseItem.removePropertyChangeListener("deleted", baseItemDeleted);
        baseItem.removePropertyChangeListener("replaced", baseItemReplaced);

        baseItem.removePropertyChangeListener("Location", baseItemSublocationChanged);
    }

    @Override
    public String toString()
    {
        return "LocationBean wrapping " + baseItem;
    }

}
