/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.sis.metadata;

import java.util.Set;
import java.util.Map;
import java.util.List;
import java.util.Iterator;
import java.util.Collection;
import java.util.Objects;
import java.util.NoSuchElementException;
import java.util.ConcurrentModificationException;
import java.util.function.Function;
import org.opengis.annotation.Obligation;
import org.apache.sis.xml.NilReason;
import org.apache.sis.xml.bind.lan.LocaleAndCharset;
import org.apache.sis.util.Debug;
import org.apache.sis.util.Classes;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.ObjectConverters;
import org.apache.sis.util.internal.shared.CollectionsExt;
import org.apache.sis.util.internal.shared.Unsafe;
import org.apache.sis.util.iso.Types;
import org.apache.sis.util.collection.TableColumn;
import org.apache.sis.util.collection.TreeTable.Node;
import org.apache.sis.util.collection.CheckedContainer;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.resources.Vocabulary;


/**
 * A node in a {@link TreeTableView} view. The {@code TreeNode} base class is used directly only for the root node,
 * or for nodes containing a fixed value instead of a value fetched from the metadata object. For all other nodes,
 * the actual node class shall be either {@link Element} or {@link CollectionElement}.
 *
 * <p>The value of a node is extracted from the {@linkplain #metadata} object by {@link #getUserObject()}.
 * For each instance of {@code TreeNode}, that value is always a singleton, never a collection.
 * If a metadata property is a collection, then there is an instance of the {@link CollectionElement}
 * subclass for each element in the collection.</p>
 *
 * <p>The {@link #newChild()} operation is supported if the node is not a leaf. The user shall
 * set the identifier and the value, in that order, before any other operation on the new child.
 * See {@code newChild()} javadoc for an example.</p>
 *
 * <h2>API note</h2>
 * This class is not serializable because the values of the {@link Element#indexInData}
 * and {@link CollectionElement#indexInList} fields may not be stable.
 * The former may be invalid if the node is serialized and deserialized by two different versions of Apache SIS
 * having properties in different order. The second may be invalid if the collection is not guaranteed to preserve
 * order on serialization (e.g. {@code CodeListSet} with user supplied elements, in which case the elements order
 * depends on the instantiation order).</div>
 *
 * @author  Martin Desruisseaux (Geomatys)
 */
class TreeNode implements Node {
    /**
     * The collection of {@linkplain #children} to return when the node does not allow children.
     * This constant is also used as a sentinel value by {@link #isLeaf()}.
     *
     * <p>We choose an empty set instead of an empty list because {@link TreeNodeChildren}
     * does not implement the {@link List} interface. So we are better to never give to the user
     * a collection implementing {@code List} in order to signal incorrect casts sooner.</p>
     */
    private static final Collection<Node> LEAF = Set.of();

    /**
     * The table for which this node is an element.
     * Contains information like the metadata standard and the value existence policy.
     *
     * <p>All {@code TreeNode} instances in the same tree have
     * a reference to the same {@code TreeTableView} instance.</p>
     */
    final TreeTableView table;

    /**
     * The parent of this node to be returned by {@link #getParent()},
     * or {@code null} if this node is the root of the tree.
     *
     * @see #getParent()
     */
    private final TreeNode parent;

    /**
     * The metadata object from which the {@link #getUserObject()} method will fetch the value.
     * The value is fetched in different ways, which depend on the {@code TreeNode} subclass:
     *
     * <ul>
     *   <li>For {@code TreeNode} (the root of the tree),
     *       the value is directly {@link #metadata}.</li>
     *   <li>For {@link Element} (a metadata property which is not a collection),
     *       the value is {@code accessor.get(indexInData, metadata)}.</li>
     *   <li>For {@link CollectionElement} (an element in a collection),
     *       another index is used for fetching the element in that collection.</li>
     * </ul>
     *
     * This field shall never be null.
     *
     * @see #getUserObject()
     */
    final Object metadata;

    /**
     * The return type of the getter method that provides the value encapsulated by this node.
     * This information is used for filtering aspects when a class opportunistically implements
     * many interfaces. This value is part of the {@link CacheKey} needed for invoking
     * {@link MetadataStandard} methods.
     */
    final Class<?> baseType;

    /**
     * The value of {@link TableColumn#NAME}, computed by {@link #getName()} then cached.
     *
     * @see #getName()
     */
    private transient CharSequence name;

    /**
     * The children of this node, or {@code null} if not yet computed. If and only if the node
     * cannot have children (i.e. {@linkplain #isLeaf() is a leaf}), then this field is set to
     * {@link #LEAF}.
     *
     * @see #getChildren()
     */
    private transient Collection<Node> children;

    /**
     * The value which existed when the {@link TreeNodeChildren#iterator()} traversed this node.
     * This value is cached on the assumption that users will ask for value or for children soon
     * after they iterated over this node. The cached value is cleared after its first use.
     *
     * <p>This value shall be either {@code null}, or the exact same value as what a call to
     * {@link #getUserObject()} would return, assuming that the underlying {@linkplain #metadata}
     * object didn't changed.</p>
     *
     * <p>The purpose of this cache is to avoid invoking (by reflection) the same getter methods
     * twice in common situations like the {@link TreeTableView#toString()} implementation or in
     * Graphical User Interface. However, we may remove this field in any future SIS version if
     * experience shows that it is more problematic than helpful.</p>
     *
     * @see #getNonNilValue()
     */
    transient Object cachedValue;

    /**
     * Whether {@link #cachedValue} can be used for the value of {@link TableColumn#VALUE}.
     * This flag is set to {@code true} only by the {@link TreeNodeChildren} iterator,
     * thus allowing the use of cached value in the {@code VALUE} column only after
     * a call to {@link Iterator#next()} (for opportunistic reason), and only once.
     * This restriction does not apply to {@link MetadataColumn#NIL_REASON}.
     */
    transient boolean canUseCache;

    /**
     * Creates the root node of a new metadata tree table.
     *
     * @param  table     the table which is creating this root node.
     * @param  metadata  the root metadata object (cannot be null).
     * @param  baseType  the return type of the getter method that provides the value encapsulated by this node.
     */
    TreeNode(final TreeTableView table, final Object metadata, final Class<?> baseType) {
        this.table    = table;
        this.parent   = null;
        this.metadata = metadata;
        this.baseType = baseType;
    }

    /**
     * Creates a new child for an element of the given metadata.
     * This constructor is for the {@link Element} subclass only.
     *
     * @param  parent    the parent of this node.
     * @param  metadata  the metadata object for which this node will be a value.
     * @param  baseType  the return type of the getter method that provides the value encapsulated by this node.
     */
    private TreeNode(final TreeNode parent, final Object metadata, final Class<?> baseType) {
        this.table    = parent.table;
        this.parent   = parent;
        this.metadata = metadata;
        this.baseType = baseType;
        if (!isMetadata(baseType)) {
            children = LEAF;
        }
    }

    /**
     * Returns {@code true} if nodes for values of the given type can be expanded with more children.
     * A return value of {@code false} means that values of the given type are leaves.
     */
    final boolean isMetadata(final Class<?> type) {
        return table.standard.isMetadata(type);
    }

    /**
     * Returns the key to use for calls to {@link MetadataStandard} methods.
     * This key is used only for some default method implementations in the root node;
     * children will use the class of their node value instead.
     */
    private CacheKey key() {
        return new CacheKey(metadata.getClass(), baseType);
    }

    /**
     * Appends an identifier for this node in the given buffer, for {@link #toString()} implementation.
     * The appended value is similar to the value returned by {@link #getIdentifier()} (except for the
     * root node), but may contains additional information like the index in a collection.
     *
     * <p>The default implementation is suitable only for the root node - subclasses must override.</p>
     *
     * @param  buffer  the buffer where to complete the {@link #toString()} representation.
     */
    @Debug
    void appendIdentifier(final StringBuilder buffer) {
        buffer.append(Classes.getShortClassName(metadata));
    }

    /**
     * Returns the UML identifier defined by the standard. The default implementation is suitable
     * only for the root node, since it returns the class identifier. Subclasses must override in
     * order to return the property identifier instead.
     */
    String getIdentifier() {
        final Class<?> type = table.standard.getInterface(key());
        final String id = Types.getStandardName(type);
        return (id != null) ? id : Classes.getShortName(type);
    }

    /**
     * Returns the index in the collection if the metadata property type is a collection,
     * or {@code null} otherwise. The (<var>identifier</var>, <var>index</var>) pair can
     * be used as a primary key for identifying this node among its siblings.
     */
    Integer getIndex() {
        return null;
    }

    /**
     * Gets the human-readable name of this node. The name shall be stable, since it will be cached
     * by the caller. The name typically contains {@linkplain #getIdentifier() identifier} and
     * {@linkplain #getIndex() index} information, eventually localized.
     *
     * <p>The default implementation is suitable only for the root node - subclasses must override.</p>
     */
    CharSequence getName() {
        return CharSequences.camelCaseToSentence(Classes.getShortName(
                table.standard.getInterface(key()))).toString();
    }

    /**
     * Gets whether the property is mandatory, optional or conditional, or {@code null} if unspecified.
     */
    Obligation getObligation() {
        return null;
    }

    /**
     * Gets remarks about the value in this node, or {@code null} if none.
     */
    CharSequence getRemarks() {
        return null;
    }

    /**
     * Gets the reason why the value is missing, or {@code null} if unspecified.
     * Note that this method is expected to always return {@code null} if
     * {@link ValueExistencePolicy#acceptNilValues()} is {@code false}.
     *
     * @see #setNilReason(NilReason)
     */
    NilReason getNilReason() {
        return null;
    }

    /**
     * Returns the property value, excluding nil value and using the cached value if available.
     * Nil value are excluded because the reason why they are nil is reported in a separated column.
     *
     * <h4>Caching</h4>
     * The cached value is set by {@link TreeNodeChildren} iterator and used only once for
     * the value in {@link TableColumn#VALUE}. However, the cached value may be reused for
     * the value in {@link MetadataColumn#NIL_REASON}.
     */
    private Object getNonNilValue() {
        if (!canUseCache) {
            cachedValue = getUserObject();
        }
        canUseCache = false;                // Use the cached value only once after iteration.
        if (table.valuePolicy.acceptNilValues() && NilReason.forObject(cachedValue) != null) {
            return null;
        }
        return cachedValue;
    }

    /**
     * The metadata value for this node, to be returned by {@code getValue(TableColumn.VALUE)}.
     * The default implementation is suitable only for the root node - subclasses must override.
     */
    @Override
    public Object getUserObject() {
        return metadata;
    }

    /**
     * Sets the metadata value for this node. Subclasses must override this method.
     *
     * @throws UnsupportedOperationException if the metadata value is not writable.
     */
    void setUserObject(final Object value) throws UnsupportedOperationException {
        throw new UnsupportedOperationException(unmodifiableCellValue(TableColumn.VALUE));
    }

    /**
     * Sets the value to nil with a reason explaining why the value is nil.
     *
     * @throws UnsupportedOperationException if the metadata value is not writable.
     */
    void setNilReason(final NilReason value) {
        throw new UnsupportedOperationException(unmodifiableCellValue(MetadataColumn.NIL_REASON));
    }

    /**
     * Returns {@code true} if the metadata value can be set.
     * Subclasses must override this method.
     */
    boolean isWritable() {
        return false;
    }

    /**
     * Returns {@code true} if the given object is of the same class as this node and contains a reference
     * to the same metadata object. Since {@code TreeNode} generates all content from the wrapped metadata,
     * this condition should ensure that two equal nodes have the same values and children.
     */
    @Override
    public boolean equals(final Object other) {
        return (other != null) && other.getClass() == getClass()
                && ((TreeNode) other).metadata == metadata
                && ((TreeNode) other).baseType == baseType;
    }

    /**
     * Returns a hash code value for this node.
     */
    @Override
    public int hashCode() {
        return System.identityHashCode(metadata) ^ Objects.hashCode(baseType);
    }




    /**
     * A node for a metadata property value. This class does not store the property value directly.
     * Instead, is stores a reference to the metadata object that contains the property values,
     * together with the index for fetching the value in that object. That way, the real storage
     * objects still the metadata object, which allow {@link TreeTableView} to be a dynamic view.
     *
     * <p>Instances of this class shall be instantiated only for metadata singletons. If a metadata
     * property is a collection, then the {@link CollectionElement} subclass shall be instantiated
     * instead.</p>
     */
    static class Element extends TreeNode {
        /**
         * The accessor to use for fetching the property names, types and values from the {@link #metadata} object.
         * Note that the reference stored in this field is the same for all siblings.
         */
        private final PropertyAccessor accessor;

        /**
         * The reasons why some mandatory property values are missing.
         * Created only if cell values in the "Nil reason" column are requested.
         *
         * @see #nilReasons()
         */
        private transient NilReasonMap nilReasons;

        /**
         * Index of the value in the {@link #metadata} object to be fetched with the {@link #accessor}.
         */
        private final int indexInData;

        /**
         * If tree node should be wrapped in another object before to be returned, the function performing that wrapping.
         * This is used if we want to render a metadata property in a different way than the way implied by JavaBeans.
         * The wrapping operation should be cheap because it will be applied every time the user request the node.
         *
         * <h4>Example</h4>
         * The {@code "defaultLocale+otherLocale"} property is represented by {@code Map.Entry<Locale,Charset>} values.
         * The nodes created by this class contain those {@code Map.Entry} values, but we want to show them to users as
         * as a {@link java.util.Locale} node with a {@link java.nio.charset.Charset} child. This separation is done by
         * {@link LocaleAndCharset}.
         */
        final Function<TreeNode,Node> decorator;

        /**
         * Creates a new child for a property of the given metadata at the given index.
         *
         * @param  parent       the parent of this node.
         * @param  metadata     the metadata object for which this node will be a value.
         * @param  accessor     accessor to use for fetching the name, type and value.
         * @param  indexInData  index to be given to the accessor for fetching the value.
         */
        Element(final TreeNode parent, final Object metadata,
                final PropertyAccessor accessor, final int indexInData)
        {
            super(parent, metadata, accessor.type(indexInData, TypeValuePolicy.ELEMENT_TYPE));
            this.accessor = accessor;
            this.indexInData = indexInData;
            if (SpecialCases.isLocaleAndCharset(accessor, indexInData)) {
                decorator = LocaleAndCharset::new;
            } else {
                decorator = null;
            }
        }

        /**
         * Appends an identifier for this node in the given buffer, for {@link #toString()} implementation.
         * This method is mostly for debugging purposes and is not used for the tree table node values.
         */
        @Debug
        @Override
        void appendIdentifier(final StringBuilder buffer) {
            super.appendIdentifier(buffer);
            buffer.append('.').append(accessor.name(indexInData, KeyNamePolicy.JAVABEANS_PROPERTY));
        }

        /**
         * The property identifier to be returned in the {@link TableColumn#IDENTIFIER} cells.
         */
        @Override
        final String getIdentifier() {
            return accessor.name(indexInData, KeyNamePolicy.UML_IDENTIFIER);
        }

        /**
         * Gets the name of this node. Current implementation derives the name from the
         * {@link KeyNamePolicy#UML_IDENTIFIER} instead of {@link KeyNamePolicy#JAVABEANS_PROPERTY}
         * in order to get the singular form instead of the plural one, because we will create one
         * node for each element in a collection.
         *
         * <p>If the property name is equal, ignoring case, to the simple type name, then this method
         * returns the subtype name (<a href="https://issues.apache.org/jira/browse/SIS-298">SIS-298</a>).
         * For example, instead of:</p>
         *
         * <pre class="text">
         *   Citation
         *    └─Cited responsible party
         *       └─Party
         *          └─Name ……………………………… Jon Smith</pre>
         *
         * we format:
         *
         * <pre class="text">
         *   Citation
         *    └─Cited responsible party
         *       └─Individual
         *          └─Name ……………………………… Jon Smith</pre>
         */
        @Override
        CharSequence getName() {
            String identifier = getIdentifier();
            if (identifier.equalsIgnoreCase(Classes.getShortName(baseType))) {
                final Object value = getUserObject();
                if (value != null) {
                    Class<?> type = standardSubType(Classes.getLeafInterfaces(value.getClass(), baseType));
                    if (type != null && type != Void.TYPE) {
                        identifier = Classes.getShortName(type);
                    }
                }
            }
            identifier = SpecialCases.rename(identifier);                       // Hard-coded special case.
            return CharSequences.camelCaseToSentence(identifier).toString();
        }

        /**
         * Returns the element of the given array which is both assignable to {@link #baseType} and a member
         * of the standard represented by {@link TreeTableView#standard}. If no such type is found, returns
         * {@code null}. If more than one type is found, returns the {@link Void#TYPE} sentinel value.
         */
        private Class<?> standardSubType(final Class<?>[] subtypes) {
            Class<?> type = null;
            for (Class<?> c : subtypes) {
                if (baseType.isAssignableFrom(c)) {
                    if (!isMetadata(c)) {
                        c = standardSubType(c.getInterfaces());
                    }
                    if (type == null) {
                        type = c;
                    } else if (type != c) {
                        return Void.TYPE;
                    }
                }
            }
            return type;
        }

        /**
         * Returns the map of reasons why a mandatory value is missing.
         * The map is created only when first needed.
         */
        @SuppressWarnings("ReturnOfCollectionOrArrayField")
        private NilReasonMap nilReasons() {
            if (nilReasons == null) {
                nilReasons = new NilReasonMap(metadata, accessor, KeyNamePolicy.UML_IDENTIFIER);
            }
            return nilReasons;
        }

        /**
         * Sets the value to nil with a reason explaining why the value is nil.
         */
        @Override
        void setNilReason(final NilReason value) {
            cachedValue = null;
            canUseCache = false;
            nilReasons().setReflectively(indexInData, value);
        }

        /**
         * Gets the reason why the value is missing, or {@code null} if unspecified.
         */
        @Override
        NilReason getNilReason() {
            // Do not check `canUseCache` because it applies to TableColumn.VALUE.
            if (cachedValue == null) cachedValue = getUserObject();
            return nilReasons().getNilReason(indexInData, cachedValue);
        }

        /**
         * Gets whether the property is mandatory, optional or conditional, or {@code null} if unspecified.
         */
        @Override
        Obligation getObligation() {
            return accessor.obligation(indexInData);
        }

        /**
         * Gets remarks about the value in this node, or {@code null} if none.
         */
        @Override
        final CharSequence getRemarks() {
            return accessor.remarks(indexInData, metadata);
        }

        /**
         * Fetches the node value from the metadata object.
         */
        @Override
        public Object getUserObject() {
            return accessor.get(indexInData, metadata);
        }

        /**
         * Sets the property value for this node.
         */
        @Override
        void setUserObject(final Object value) {
            cachedValue = null;
            canUseCache = false;
            accessor.set(indexInData, metadata, value, PropertyAccessor.RETURN_NULL);
        }

        /**
         * Returns {@code true} if the metadata is writable.
         */
        @Override
        final boolean isWritable() {
            return accessor.isWritable(indexInData);
        }

        /**
         * Returns {@code true} if the value returned by {@link #getUserObject()}
         * should be the same for both nodes.
         */
        @Override
        public boolean equals(final Object other) {
            return super.equals(other) && ((Element) other).indexInData == indexInData;
        }

        /**
         * Returns a hash code value for this node.
         */
        @Override
        public int hashCode() {
            return super.hashCode() ^ (31 * indexInData);
        }
    }




    /**
     * A node for an element in a collection. This class needs the iteration order to be stable.
     */
    static final class CollectionElement extends Element {
        /**
         * Index of the element in the collection, in iteration order.
         */
        final int indexInList;

        /**
         * Creates a new node for the given collection element.
         *
         * @param  parent       the parent of this node.
         * @param  metadata     the metadata object for which this node will be a value.
         * @param  accessor     accessor to use for fetching the name, type and collection.
         * @param  indexInData  index to be given to the accessor of fetching the collection.
         * @param  indexInList  index of the element in the collection, in iteration order.
         */
        CollectionElement(final TreeNode parent, final Object metadata,
                final PropertyAccessor accessor, final int indexInData, final int indexInList)
        {
            super(parent, metadata, accessor, indexInData);
            this.indexInList = indexInList;
        }

        /**
         * Appends an identifier for this node in the given buffer, for {@link #toString()} implementation.
         * This method is mostly for debugging purposes and is not used for the tree table node values.
         */
        @Debug
        @Override
        void appendIdentifier(final StringBuilder buffer) {
            super.appendIdentifier(buffer);
            buffer.append('[').append(indexInList).append(']');
        }

        /**
         * Returns the zero-based index of this node in the metadata property.
         */
        @Override
        Integer getIndex() {
            return indexInList;
        }

        /**
         * Appends the index of this property, if there is more than one.
         * Index numbering begins at 1, since this name if for human reading.
         */
        @Override
        CharSequence getName() {
            CharSequence name = super.getName();
            final int size = CollectionsExt.size(super.getUserObject());
            if (size >= 2) {
                name = Vocabulary.formatInternational(Vocabulary.Keys.Of_3, name, indexInList+1, size);
            }
            return name;
        }

        /**
         * Fetches the property value from the metadata object, which is expected to be a collection,
         * then fetch the element at the index represented by this node.
         */
        @Override
        public Object getUserObject() {
            final Object collection = super.getUserObject();
            final Collection<?> values;
            if (collection instanceof Collection<?>) {
                values = (Collection<?>) collection;
            } else {
                /*
                 * ClassCastException should never happen here unless PropertyAccessor.isCollectionOrMap(…) has
                 * been modified, in which case there is probably many code to update (not only this method).
                 */
                values = ((Map<?,?>) collection).entrySet();
            }
            /*
             * If the collection is null or empty but the value existence policy tells
             * us that such elements shall be shown, behave as if the collection was a
             * singleton containing a null element, in order to make the property
             * visible in the tree.
             */
            if (indexInList == 0 && table.valuePolicy.substituteByNullElement(values)) {
                return null;
            }
            try {
                if (values instanceof List<?>) {
                    return ((List<?>) values).get(indexInList);
                }
                final Iterator<?> it = values.iterator();
                for (int i=0; i<indexInList; i++) {
                    it.next();      // Inefficient way to move at the desired index, but hopefully rare.
                }
                return it.next();
            } catch (NullPointerException | IndexOutOfBoundsException | NoSuchElementException e) {
                /*
                 * May happen if the collection for this metadata property changed after the iteration
                 * in the TreeNodeChildren. Users should not keep TreeNode references instances for a
                 * long time, but instead iterate again over TreeNodeChildren when needed.
                 */
                throw new ConcurrentModificationException(e);
            }
        }

        /**
         * Sets the property value for this node.
         */
        @Override
        void setUserObject(Object value) {
            cachedValue = null;
            canUseCache = false;
            final Collection<?> values = (Collection<?>) super.getUserObject();
            if (!(values instanceof List<?>)) {
                // `setValue(…)` is the public method which invoked this one.
                throw new UnsupportedOperationException(Errors.format(Errors.Keys.UnsupportedOperation_1, "setValue"));
            }
            final Class<?> targetType;
            if (values instanceof CheckedContainer<?>) {
                /*
                 * Typically the same as getElementType(), but let be safe
                 * in case some implementations have stricter requirements.
                 */
                targetType = ((CheckedContainer<?>) values).getElementType();
            } else {
                targetType = baseType;
            }
            value = ObjectConverters.convert(value, targetType);
            try {
                /*
                 * Unsafe addition into a collection. In SIS implementation, the collection is
                 * actually an instance of CheckedCollection, so the check will be performed at
                 * runtime. However, other implementations could use unchecked collection. We have
                 * done our best for converting the type above, there is not much more we can do...
                 */
                Unsafe.set((List<?>) values, indexInList, value);
            } catch (IndexOutOfBoundsException e) {
                // Same rational as in the getUserObject() method.
                throw new ConcurrentModificationException(e);
            }
        }

        /**
         * Gets the reason why the value is missing, or {@code null} if unspecified.
         * Note that this method gets the nil reason of a specific collection element.
         * This is a bit unusual, since nil reasons usually apply to the whole property.
         */
        @Override
        NilReason getNilReason() {
            // Do not check `canUseCache` because it applies to TableColumn.VALUE.
            if (cachedValue == null) cachedValue = getUserObject();
            return NilReason.forObject(cachedValue);
        }

        /**
         * Sets the value to nil with a reason explaining why the value is nil.
         * Note that this method sets the nil reason of a specific collection element.
         * This is a bit unusual, since nil reasons usually apply to the whole property.
         */
        @Override
        void setNilReason(final NilReason value) {
            setUserObject(value != null ? value.createNilObject(baseType) : null);
        }

        /**
         * Returns {@code true} if the value returned by {@link #getUserObject()}
         * should be the same for both nodes.
         */
        @Override
        public boolean equals(final Object other) {
            return super.equals(other) && ((CollectionElement) other).indexInList == indexInList;
        }

        /**
         * Returns a hash code value for this node.
         */
        @Override
        public int hashCode() {
            return super.hashCode() ^ indexInList;
        }
    }


    // -------- Final methods (defined in terms of above methods only) ----------------------------


    /**
     * Returns the parent node, or {@code null} if this node is the root of the tree.
     */
    @Override
    public final Node getParent() {
        return parent;
    }

    /**
     * Returns {@code false} if the value is a metadata object (and consequently can have children),
     * or {@code true} if the value is not a metadata object.
     */
    @Override
    public final boolean isLeaf() {
        return (children == LEAF);
    }

    /**
     * Returns the children of this node, or an empty set if none.
     * Only metadata object can have children.
     */
    @Override
    @SuppressWarnings("ReturnOfCollectionOrArrayField")
    public final Collection<Node> getChildren() {
        /*
         * `children` is set to LEAF if an only if the node *cannot* have children,
         * in which case we do not need to check for changes in the underlying metadata.
         */
        if (!isLeaf()) {
            Object value = getNonNilValue();
            if (value == null) {
                /*
                 * If there is no value, returns an empty set but *do not* set `children`
                 * to that set, in order to allow this method to check again the next time
                 * that this method is invoked.
                 */
                children = null;                                    // Let GC do its work.
                return LEAF;
            }
            /*
             * If there is a value, check if the cached collection is still applicable.
             * We verify that the collection is a wrapper for the same metadata object.
             * If we need to create a new collection, we know that the property accessor
             * exists otherwise the call to `isLeaf()` above would have returned `true`.
             */
            if (children == null || ((TreeNodeChildren) children).metadata != value) {
                PropertyAccessor accessor = table.standard.getAccessor(new CacheKey(value.getClass(), baseType), true);
                children = new TreeNodeChildren(this, value, accessor);
            }
        }
        return children;
    }

    /**
     * Returns a proxy for a new property to be defined in the metadata object.
     * The user shall set the identifier and the value, in that order, before
     * any other operation on the new child. Example:
     *
     * {@snippet lang="java" :
     *     TreeTable.Node node = ...;
     *     TreeTable.Node child = node.newChild();
     *     child.setValue(TableColumn.IDENTIFIER, "title");
     *     child.setValue(TableColumn.VALUE, "Le petit prince");
     *     // Nothing else to do - node has been added.
     *     }
     *
     * Do not keep a reference to the returned node for a long time, since it is only
     * a proxy toward the real node to be created once the identifier is known.
     *
     * @throws UnsupportedOperationException if this node {@linkplain #isLeaf() is a leaf}.
     */
    @Override
    public final Node newChild() throws UnsupportedOperationException {
        if (isLeaf()) {
            throw new UnsupportedOperationException(Errors.format(Errors.Keys.NodeIsLeaf_1, this));
        }
        return new NewChild();
    }

    /**
     * The proxy to be returned by {@link TreeNode#newChild()}.
     * User shall not keep a reference to this proxy for a long time.
     */
    private final class NewChild implements Node {
        /**
         * Index in the {@link PropertyAccessor} for the property to be set.
         * This index is known only after a value has been specified for the
         * {@link TableColumn#IDENTIFIER}.
         */
        private int indexInData = -1;

        /**
         * The real node created after the identifier and the value have been specified.
         * All operations will be delegated to that node after it has been determined.
         */
        private TreeNode delegate;

        /**
         * Returns the {@link #delegate} node if non-null, or throw an exception otherwise.
         *
         * @throws IllegalStateException if the identifier and value columns have not yet been defined.
         */
        private TreeNode delegate() throws IllegalStateException {
            if (delegate != null) {
                return delegate;
            }
            throw new IllegalStateException(Errors.format(Errors.Keys.MissingValueInColumn_1,
                    (indexInData < 0 ? TableColumn.IDENTIFIER : TableColumn.VALUE).getHeader()));
        }

        /**
         * Returns all children of the parent node. The new child will be added to that list.
         */
        private TreeNodeChildren getSiblings() {
            return (TreeNodeChildren) TreeNode.this.getChildren();
        }

        /**
         * If the {@link #delegate} is not yet known, set the identifier or the value.
         * After the identifier and value have been specified, delegates to the real node.
         */
        @Override
        public <V> void setValue(final TableColumn<V> column, final V value) {
            if (delegate == null) {
                /*
                 * For the given identifier, get the index in the property accessor.
                 * This can be done only before the `delegate` is found - after that
                 * point, the identifier will become unmodifiable.
                 */
                if (column == TableColumn.IDENTIFIER) {
                    ArgumentChecks.ensureNonNull("value", value);
                    indexInData = getSiblings().accessor.indexOf((String) value, true);
                    return;
                }
                /*
                 * Set the value for the property specified by the above identifier,
                 * then get the `delegate` on the assumption that the new value will
                 * be added at the end of collection (if the property is a collection).
                 */
                if (column == TableColumn.VALUE) {
                    ArgumentChecks.ensureNonNull("value", value);
                    if (indexInData < 0) {
                        throw new IllegalStateException(Errors.format(Errors.Keys.MissingValueInColumn_1,
                                TableColumn.IDENTIFIER.getHeader()));
                    }
                    final TreeNodeChildren siblings = getSiblings();
                    final int indexInList;
                    if (siblings.isCollectionOrMap(indexInData)) {
                        indexInList = CollectionsExt.size(siblings.valueAt(indexInData));
                    } else {
                        indexInList = -1;
                    }
                    if (!siblings.add(indexInData, value)) {
                        throw new IllegalArgumentException(Errors.format(Errors.Keys.ElementAlreadyPresent_1, value));
                    }
                    delegate = siblings.childAt(indexInData, indexInList);
                    /*
                     * Do not set `delegate.cachedValue = value`, since `value` may
                     * have been converted by the setter method to another value.
                     */
                    return;
                }
            }
            delegate().setValue(column, value);
        }

        /**
         * For all operations other than {@code setValue(…)}, delegates to the {@link #delegate} node
         * or to some code functionally equivalent.
         *
         * @throws IllegalStateException if the identifier and value columns have not yet been defined.
         */
        @Override public Node             getParent()                       {return TreeNode.this;}
        @Override public boolean          isLeaf()                          {return delegate().isLeaf();}
        @Override public Collection<Node> getChildren()                     {return delegate().getChildren();}
        @Override public Node             newChild()                        {return delegate().newChild();}
        @Override public <V> V            getValue(TableColumn<V> column)   {return delegate().getValue(column);}
        @Override public boolean          isEditable(TableColumn<?> column) {return delegate().isEditable(column);}
        @Override public Object           getUserObject()                   {return delegate().getUserObject();}
    }

    /**
     * Returns the children if the value policy is {@link ValueExistencePolicy#COMPACT}, or {@code null} otherwise.
     */
    private TreeNodeChildren getCompactChildren() {
        if (table.valuePolicy == ValueExistencePolicy.COMPACT) {
            @SuppressWarnings("LocalVariableHidesMemberVariable")
            final Collection<Node> children = getChildren();
            if (children instanceof TreeNodeChildren) {
                return (TreeNodeChildren) children;
            }
        }
        return null;
    }

    /**
     * Returns the value of this node in the given column, or {@code null} if none. This method verifies
     * the {@code column} argument, then delegates to {@link #getName()}, {@link #getUserObject()} or
     * other properties.
     */
    @Override
    public final <V> V getValue(final TableColumn<V> column) {
        Object value = null;
        ArgumentChecks.ensureNonNull("column", column);
        if (column == TableColumn.IDENTIFIER) {
            value = getIdentifier();
        } else if (column == TableColumn.INDEX) {
            value = getIndex();
        } else if (column == TableColumn.NAME) {
            if (name == null) {
                name = getName();
            }
            value = name;
        } else if (column == TableColumn.TYPE) {
            @SuppressWarnings("LocalVariableHidesMemberVariable")
            final TreeNodeChildren children = getCompactChildren();
            if (children == null || (value = children.getParentType()) == null) {
                value = baseType;
            }
        } else if (column == TableColumn.VALUE) {
            if (isLeaf()) {
                value = getNonNilValue();
            } else {
                @SuppressWarnings("LocalVariableHidesMemberVariable")
                final TreeNodeChildren children = getCompactChildren();
                if (children != null) {
                    value = children.getParentTitle();
                }
            }
        } else if (column == MetadataColumn.OBLIGATION) {
            value = getObligation();
        } else if (column == MetadataColumn.NIL_REASON) {
            value = getNilReason();
        } else if (column == TableColumn.REMARKS) {
            value = getRemarks();
        }
        return column.getElementType().cast(value);
    }

    /**
     * Sets the value if the given column is {@link TableColumn#VALUE}. This method verifies
     * the {@code column} argument, then delegates to {@link #setUserObject(Object)}.
     *
     * <p>This method does not accept null value, because setting a singleton property to null
     * with {@link ValueExistencePolicy#NON_EMPTY} is equivalent to removing the property, and
     * setting a collection element to null is not allowed. Those various behavior are at risk
     * of causing confusion, so we are better to never allow null.</p>
     */
    @Override
    public final <V> void setValue(final TableColumn<V> column, final V value) throws UnsupportedOperationException {
        ArgumentChecks.ensureNonNull("column", column);
        if (column == TableColumn.VALUE) {
            ArgumentChecks.ensureNonNull("value", value);                       // See javadoc.
            @SuppressWarnings("LocalVariableHidesMemberVariable")
            final TreeNodeChildren children = getCompactChildren();
            if (children == null || !(children.setParentTitle(value))) {
                setUserObject(value);
            }
        } else if (column == MetadataColumn.NIL_REASON) {
            setNilReason((NilReason) value);
        } else if (table.getColumns().contains(column)) {
            throw new UnsupportedOperationException(unmodifiableCellValue(column));
        } else {
            throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, "column", column));
        }
    }

    /**
     * Returns the error message for an unmodifiable cell value in the given column.
     */
    private String unmodifiableCellValue(final TableColumn<?> column) {
        return Errors.format(Errors.Keys.UnmodifiableCellValue_2, getValue(TableColumn.NAME), column.getHeader());
    }

    /**
     * Returns {@code true} if the given column is {@link TableColumn#VALUE} and the property is writable,
     * or {@code false} in all other cases. This method verifies the {@code column} argument, then delegates
     * to {@link #isWritable()}.
     */
    @Override
    public final boolean isEditable(final TableColumn<?> column) {
        ArgumentChecks.ensureNonNull("column", column);
        return (column == TableColumn.VALUE) && isWritable();
    }

    /**
     * Returns a string representation of this node for debugging purpose.
     */
    @Override
    public final String toString() {
        final var buffer = new StringBuilder(60);
        appendStringTo(buffer);
        return buffer.toString();
    }

    /**
     * Implementation of {@link #toString()} appending the string representation in the given buffer.
     * This method is mostly for debugging purposes and is not used for the tree table node values.
     */
    @Debug
    final void appendStringTo(final StringBuilder buffer) {
        appendIdentifier(buffer.append("Node["));
        buffer.append(" : ").append(Classes.getShortName(baseType)).append(']');
    }
}
