/*
 * 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.Map;
import java.util.Iterator;
import java.util.Collections;
import java.util.AbstractCollection;
import java.util.NoSuchElementException;
import java.util.ConcurrentModificationException;
import org.apache.sis.util.collection.BackingStoreException;
import org.apache.sis.util.collection.TableColumn;
import org.apache.sis.util.collection.TreeTable;
import org.apache.sis.util.resources.Errors;


/**
 * The collection of children to be returned by {@link TreeNode#getChildren()}.
 * This collection holds a reference to the metadata object at creation time;
 * it does not track changes in {@code parent.getUserObject()}.
 *
 * <h2>Note on value existence policy</h2>
 * It is better to use this class with {@link ValueExistencePolicy#NON_EMPTY} in order
 * to avoid code complication and surprising behavior of {@link Iter#remove()} operation.
 * If the policy is set to another value, we need to keep the following aspects in mind:
 *
 * <ul>
 *   <li>When {@link Iter#hasNext()} finds a null or empty collection,
 *       it may needs to simulate a singleton with a null value.</li>
 *   <li>In {@link TreeNode#getUserObject()}, we need the same check as above
 *       for simulating a singleton collection with a null value if the node is
 *       for the element at index 0.</li>
 * </ul>
 *
 * @author  Martin Desruisseaux (Geomatys)
 */
final class TreeNodeChildren extends AbstractCollection<TreeTable.Node> {
    /**
     * The parent of the children to be returned by the iterator.
     * Some useful information are available indirectly through this parent:
     *
     * <ul>
     *   <li>{@link ValueExistencePolicy}: {@code parent.table.valuePolicy}</li>
     * </ul>
     *
     * @see #childAt(int, int)
     */
    private final TreeNode parent;

    /**
     * The metadata object for which property values will be the elements of this collection.
     * This is typically an {@link AbstractMetadata} instance, but not necessarily.
     * Any type for which {@link MetadataStandard#isMetadata(Class)} returns {@code true} is okay.
     *
     * <p>This field is a snapshot of the {@linkplain #parent} {@link TreeNode#getUserObject()} at creation time.
     * This collection does not track changes in the reference returned by the above-cited {@code getUserObject()}.
     * In other words, changes in the {@code metadata} object will be reflected in this collection,
     * but if {@code parent.getUserObject()} returns a reference to another object,
     * then this change will not be reflected in this collection.
     */
    final Object metadata;

    /**
     * The accessor to use for accessing the property names, types and values of the {@link #metadata} object.
     * This is given at construction time and shall be the same as the following code:
     *
     * {@snippet lang="java" :
     *     accessor = parent.table.standard.getAccessor(metadata.getClass(), true);
     *     }
     */
    final PropertyAccessor accessor;

    /**
     * The children to be returned by this collection. All elements in this collection are
     * initially {@code null}, then created by {@link #childAt(int, int)} when first needed.
     *
     * <p>Not all elements in this array will be returned by the iterator.
     * The value needs to be verified for the {@link ValueExistencePolicy}.</p>
     */
    private final TreeNode.Element[] children;

    /**
     * Index of the property to write in the parent node instead of as a child.
     * If a property has the same name as the parent property that contains it,
     * we write its value in that parent property. For example, instead of:
     *
     * <pre class="text">
     *   Citation
     *    └─Date
     *       ├─Date………………… 2012/01/01
     *       └─Date type…… Creation</pre>
     *
     * We simplify as:
     *
     * <pre class="text">
     *   Citation
     *    └─Date………………………… 2012/01/01
     *       └─Date type…… Creation</pre>
     *
     * @see <a href="https://issues.apache.org/jira/browse/SIS-298">SIS-298</a>
     */
    final int titleProperty;

    /**
     * Modification count, incremented when the content of this collection is modified. This check
     * is done on a <em>best effort basis</em> only, since we cannot not track the changes which
     * are done independently in the {@linkplain #metadata} object.
     */
    private int modCount;

    /**
     * Creates a collection of children for the specified metadata.
     *
     * @param  parent    the parent for which this node is an element.
     * @param  metadata  the metadata object for which property values will be the elements of this collection.
     * @param  accessor  the accessor to use for accessing the property names, types and values of the metadata object.
     */
    TreeNodeChildren(final TreeNode parent, final Object metadata, final PropertyAccessor accessor) {
        this.parent   = parent;
        this.metadata = metadata;
        this.accessor = accessor;
        this.children = new TreeNode.Element[accessor.count()];
        /*
         * Search for something that looks like the main property, to be associated with the parent node
         * instead of provided as a child. The intent is to have more compact and easy to read trees.
         * That property shall be a singleton for a simple value (not another metadata object).
         */
        if (parent.table.valuePolicy == ValueExistencePolicy.COMPACT) {
            TitleProperty an = accessor.implementation.getAnnotation(TitleProperty.class);
            if (an == null) {
                Class<?> implementation = parent.table.standard.getImplementation(accessor.type);
                if (implementation != null) {
                    an = implementation.getAnnotation(TitleProperty.class);
                }
            }
            if (an != null) {
                final int index = accessor.indexOf(an.name(), false);
                final Class<?> type = accessor.type(index, TypeValuePolicy.ELEMENT_TYPE);
                if (type != null && !parent.isMetadata(type) && type == accessor.type(index, TypeValuePolicy.PROPERTY_TYPE)) {
                    titleProperty = index;
                    return;
                }
            }
        }
        titleProperty = -1;
    }

    /**
     * If a simple value should be associated to the parent node, returns the type of that value.
     * Otherwise returns {@code null}.
     */
    final Class<?> getParentType() {
        return (titleProperty >= 0) ? accessor.type(titleProperty, TypeValuePolicy.ELEMENT_TYPE) : null;
    }

    /**
     * If a simple value should be associated to the parent node, returns that value.
     * Otherwise returns {@code null}.
     */
    final Object getParentTitle() {
        return (titleProperty >= 0) ? valueAt(titleProperty) : null;
    }

    /**
     * Sets the value associated to the parent node, if possible.
     * This returned boolean tells whether the value has been written.
     */
    final boolean setParentTitle(final Object value) {
        if (titleProperty < 0) {
            return false;
        }
        accessor.set(titleProperty, metadata, value, PropertyAccessor.RETURN_NULL);
        return true;
    }

    /**
     * Clears the value at the given index. The given {@code index} is relative to
     * the {@link #accessor} indexing, <strong>not</strong> to this collection.
     *
     * <p>The cleared elements may or may not be considered as removed, depending on the
     * value policy. To check if the element shall be considered as removed (for example
     * in order to update index), invoke {@code isSkipped(value)} after this method.</p>
     *
     * <h4>Implementation note</h4>
     * This method sets the property to {@code null}. This is not strictly correct for collections,
     * since we should rather set the property to an empty collection. However, this approach would
     * force us to check if the expected collection type is actually a list, a set or any other type.
     * Passing null avoid the type check and is safe at least with SIS implementation.
     * We may revisit later if this appears to be a problem with other implementations.
     *
     * @param  index  the index in the accessor (<em>not</em> the index in this collection).
     */
    final void clearAt(final int index) {
        accessor.set(index, metadata, null, PropertyAccessor.RETURN_NULL);
    }

    /**
     * Returns the value at the given index. The given {@code index} is relative to
     * the {@link #accessor} indexing, <strong>not</strong> to this collection.
     *
     * @param  index  the index in the accessor (<em>not</em> the index in this collection).
     * @return the value at the given index. May be {@code null} or a collection.
     */
    final Object valueAt(final int index) {
        return accessor.get(index, metadata);
    }

    /**
     * Returns {@code true} if the type at the given index is a collection or a map.
     * The given {@code index} is relative to the {@link #accessor} indexing, <strong>not</strong> to this collection.
     *
     * <h4>Implementation note</h4>
     * We do not test {@code (value instanceof Collection)} because the value could be any user's implementation.
     * Nothing prevent users from implementing the collection interface even for singleton elements if they wish.
     *
     * @param  index  the index in the accessor (<em>not</em> the index in this collection).
     * @return {@code true} if the value at the given index is a collection.
     */
    final boolean isCollectionOrMap(final int index) {
        return accessor.isCollectionOrMap(index);
    }

    /**
     * Returns {@code true} if the give value shall be skipped by the iterators,
     * according the value policy.
     *
     * @param  value  the value to test.
     * @return {@code true} if the given value shall be skipped by the iterators.
     */
    final boolean isSkipped(final Object value) {
        return parent.table.valuePolicy.isSkipped(value);
    }

    /**
     * Returns the child at the given index, creating it if needed. The given {@code index}
     * is relative to the {@link #accessor} indexing, <strong>not</strong> to this collection.
     *
     * <p>This method does not check if the child at the given index should be skipped.
     * It is caller responsibility to do such verification before this method call.</p>
     *
     * @param  index     the index in the accessor (<em>not</em> the index in this collection).
     * @param  subIndex  if the property at {@code index} is a collection, the index in that
     *         collection (<em>not</em> the index in <em>this</em> collection). Otherwise -1.
     * @return the node to be returned by public API.
     */
    final TreeNode.Element childAt(final int index, final int subIndex) {
        TreeNode.Element node = children[index];
        if (subIndex >= 0) {
            /*
             * If the value is an element of a collection, we will cache only the last used value.
             * We don't cache all elements in order to avoid yet more complex code, and this cover
             * the majority of cases where the collection has only one element anyway.
             *
             * Note: subIndex is ≥ 0 only if node is an instance of CollectionElement.
             * A ClassCastException below would be a logical error in this class.
             */
            if (node == null || ((TreeNode.CollectionElement) node).indexInList != subIndex) {
                node = new TreeNode.CollectionElement(parent, metadata, accessor, index, subIndex);
            }
        } else {
            /*
             * If the property is a singleton (not an element of a collection), returns a more
             * dynamic node which will fetch the value from the metadata object. This allows
             * the node to reflect changes in the metadata object, and conversely.
             */
            if (node == null) {
                node = new TreeNode.Element(parent, metadata, accessor, index);
            }
        }
        children[index] = node;
        return node;
    }

    /**
     * Returns the maximal number of children. This is the number of all possible elements
     * according the {@link #accessor}, including {@linkplain #isSkipped(Object) skipped}
     * ones. This is <strong>not</strong> the collection size.
     */
    final int childCount() {
        return children.length;
    }

    /**
     * Returns the number of elements in this collection,
     * ignoring the {@link #isSkipped(Object) skipped} ones.
     */
    @Override
    public int size() {
        int count = accessor.count(metadata, parent.table.valuePolicy, PropertyAccessor.COUNT_DEEP);
        if (titleProperty >= 0 && !isSkipped(valueAt(titleProperty))) count--;
        return count;
    }

    /**
     * Returns {@code true} if this collection contains no elements. Invoking this method is more efficient
     * than testing {@code size() == 0} because this method does not iterate over all properties.
     */
    @Override
    public boolean isEmpty() {
        if (titleProperty >= 0) return size() == 0;     // COUNT_FIRST is not reliable in this case.
        return accessor.count(metadata, parent.table.valuePolicy, PropertyAccessor.COUNT_FIRST) == 0;
    }

    /**
     * Clears all properties in the metadata object. Note that this collection will effectively
     * by empty after this method call only if the value existence policy is {@code NON_EMPTY},
     * which is the default.
     */
    @Override
    public void clear() {
        for (int i=childCount(); --i>=0;) {
            clearAt(i);
        }
    }

    /**
     * Returns an iterator over the nodes in the collection of children.
     */
    @Override
    public Iterator<TreeTable.Node> iterator() {
        return new Iter();
    }

    /**
     * The iterator over the elements in the enclosing {@link TreeNodeChildren} collection.
     * Each element is identified by its index in the {@link PropertyAccessor}, together with
     * its position in its sub-iterator when the metadata property is a collection.
     *
     * <h4>Implementation note</h4>
     * It could be cheaper to not take an iterator for the properties that are collections,
     * and instead just increment a "sub-index" from 0 to the collection size.  It would be
     * cheaper because we don't really need to extract the values of those collections (i.e.
     * the {@link #nextValue} field is not really needed). Nevertheless we prefer (for now)
     * the iterator approach anyway because it makes easier to implement the {@link #remove()}
     * method and has the desired side-effect to check for concurrent modifications. It also
     * keeps the {@link #nextValue} field up-to-date in case we would like to use it in a
     * future SIS version. We do that on the assumption that sub-iterators are cheap since
     * they are {@code ArrayList} iterators in the majority of cases.
     */
    private final class Iter implements Iterator<TreeTable.Node> {
        /**
         * Index in {@link TreeNodeChildren#accessor} of the next element to be
         * returned by {@link #next()}, or {@link PropertyAccessor#count()} if
         * we have reached the end of the list.
         */
        private int nextInAccessor;

        /**
         * Index in {@link TreeNodeChildren#accessor} of the element returned by
         * the last call to {@link #next()}, or -1 if none.
         */
        private int previousInAccessor = -1;

        /**
         * {@code true} if we have verified the value at {@link #nextInAccessor} index
         * for non-null or non-empty element.
         */
        private boolean isNextVerified;

        /**
         * The value of the node to be returned by the {@link #next()} method. This value is computed
         * ahead of time by {@link #hasNext()} because we need that information in order to determine
         * if the value should be skipped or not.
         *
         * <h4>Implementation note</h4>
         * Actually we don't really need to keep this value, since it is not used outside the {@link #hasNext()}
         * method. But we keep it for allowing the {@link #next()} method to opportunistically update the
         * {@link TreeNode#cachedValue} field.
         */
        private Object nextValue;

        /**
         * The node returned by the last call to {@link #next()}. This is used for clearing the
         * {@link TreeNode#cachedValue} field when the iterator moves to the next element.
         */
        private TreeNode current;

        /**
         * If the call to {@link #next()} found a collection, the iterator over the elements in that collection.
         * Otherwise {@code null}. A non-null value (even if that sub-iterator has no next elements) means that
         * {@link #nextValue} is an element of that sub-iteration.
         */
        private Iterator<?> subIterator;

        /**
         * Position of the {@link #nextValue} in the {@link #subIterator},
         * or -1 if the sub-iterator is null.
         */
        private int subIndex = -1;

        /**
         * The value of {@link TreeNodeChildren#modCount} at construction time or after
         * the last change done by this iterator. Used for concurrent modification checks.
         *
         * <h4>Implementation note</h4>
         * Actually this iterator should be robust to most concurrent modifications.
         * But we check anyway in order to prevent concurrent modifications in user
         * code, in case a future SIS version become more sensitive to such changes.
         */
        private int modCountCheck;

        /**
         * Creates a new iterator.
         */
        Iter() {
            modCountCheck = modCount;
        }

        /**
         * Throws {@link ConcurrentModificationException} if an unexpected change has been detected.
         * Also opportunistically clears the cached value of the previous node, since this method is
         * invoked either before moving to the next node or for removing the current node.
         */
        private void checkConcurrentModification() {
            if (current != null) {
                current.canUseCache = false;
                current.cachedValue = null;
                current = null;
            }
            if (modCountCheck != modCount) {
                throw new ConcurrentModificationException();
            }
        }

        /**
         * Ensures that {@link #nextInAccessor} is valid. If the index has not been validated, then this method
         * moves the iterator to the next valid element, starting at the current {@link #nextInAccessor} value.
         *
         * @return {@code true} on success, or {@code false} if we reached the end of the iteration.
         */
        @Override
        public boolean hasNext() {
            checkConcurrentModification();
            if (isNextVerified) {
                return true;
            }
            /*
             * If we were iterating over the elements of a sub-collection, move to the next element
             * in that iteration. We do not check for `isSkipped(value)` here because null or empty
             * elements in collections are probably mistakes, and we want to see them.
             */
            if (subIterator != null) {
                if (subIterator.hasNext()) {
                    nextValue = subIterator.next();
                    subIndex++;
                    isNextVerified = true;
                    return true;
                }
                subIterator = null;
                subIndex = -1;
                nextInAccessor++;       // See the comment before nextInAccessor++ in the next() method.
            }
            /*
             * Search for the next property, which may be either a singleton or the first element
             * of a sub-collection. In the latter case, we will create a sub-iterator.
             */
            final int count = childCount();
            while (nextInAccessor < count) {
                if (nextInAccessor != titleProperty) {
                    nextValue = valueAt(nextInAccessor);
                    if (!isSkipped(nextValue)) {
                        if (isCollectionOrMap(nextInAccessor)) {
                            /*
                             * Null collections are illegal (it shall be empty collections instead),
                             * but we try to keep the iterator robust to ill-formed metadata because
                             * we want AbstractMetadata.toString() to work so we can spot problems.
                             */
                            if (nextValue == null) {
                                subIterator = Collections.emptyIterator();
                            } else if (nextValue instanceof Iterable<?>) {
                                subIterator = ((Iterable<?>) nextValue).iterator();
                            } else {
                                subIterator = ((Map<?,?>) nextValue).entrySet().iterator();
                            }
                            /*
                             * If the property is a collection, unconditionally get the first element
                             * even if absent (null) in order to comply with the ValueExistencePolicy.
                             * if we were expected to ignore empty collections, `isSkipped(nextValue)`
                             * would have returned `true`.
                             */
                            subIndex = 0;
                            if (subIterator.hasNext()) {
                                nextValue = subIterator.next();
                            } else {
                                nextValue = null;
                                /*
                                 * Do not set `childIterator` to null, because the above `nextValue`
                                 * is considered as part of the child iteration.
                                 */
                            }
                        }
                        isNextVerified = true;
                        return true;
                    }
                }
                nextInAccessor++;
            }
            return false;
        }

        /**
         * Returns the node for the metadata property at the current {@link #nextInAccessor}.
         * The value in `TableColumn.VALUE` is initially set to {@link #nextValue},
         * but may change later if the user modifies the underlying metadata object.
         */
        @Override
        public TreeTable.Node next() {
            if (hasNext()) {
                final TreeNode.Element node = childAt(nextInAccessor, subIndex);
                node.canUseCache   = true;
                node.cachedValue   = nextValue;
                previousInAccessor = nextInAccessor;
                if (subIterator == null) {
                    /*
                     * If we are iterating over the elements in a collection, the PropertyAccessor index
                     * still the same and will be incremented by `hasNext()` only when the iteration is
                     * over. Otherwise (not iterating in a collection), move to the next property. The
                     * `hasNext()` method will determine later if this property is non-empty, or if we
                     * need to move forward again.
                     */
                    nextInAccessor++;
                }
                isNextVerified = false;
                current = node;
                return (node.decorator == null) ? node : node.decorator.apply(node);
            }
            throw new NoSuchElementException();
        }

        /**
         * Clears the element returned by the last call to {@link #next()}.
         * Whether the cleared element is considered removed or not depends
         * on the value policy and on the element type.
         * With the default {@code NON_EMPTY} policy, the effect is a removal.
         */
        @Override
        public void remove() {
            if (previousInAccessor < 0) {
                throw new IllegalStateException();
            }
            checkConcurrentModification();
            if (subIterator != null) {
                subIterator.remove();
            } else {
                clearAt(previousInAccessor);
                previousInAccessor = -1;
            }
            modCountCheck = ++modCount;
        }
    }

    /**
     * Adds the given node to this list. This method fetches the object from {@link TableColumn#VALUE}
     * and assigns it to the property identified by {@link TableColumn#IDENTIFIER}. All other columns
     * are ignored.
     *
     * <p>If the identified property is a collection, then this method adds the value to that collection.
     * Otherwise the new value will be set only if the previous value is null,
     * {@linkplain org.apache.sis.xml.NilObject nil} or empty.</p>
     *
     * <p>This method does not iterate explicitly through the children list, because adding a metadata
     * object implicitly adds all its children.</p>
     *
     * @param  node  the node from which to get the values.
     * @return {@code true} if the metadata changed as a result of this method call.
     * @throws NullPointerException if the given node is null.
     * @throws IllegalArgumentException if this list does not have a property for the node identifier.
     * @throws IllegalStateException if a value already exists and no more value can be added for the node identifier.
     * @throws UnmodifiableMetadataException if the property for the node identifier is read-only.
     * @throws ClassCastException if the node value cannot be converted to the expected type.
     * @throws BackingStoreException if the metadata implementation threw a checked exception.
     */
    @Override
    public boolean add(final TreeTable.Node node) throws IllegalStateException {
        final String identifier = node.getValue(TableColumn.IDENTIFIER);
        if (identifier == null) {
            throw new IllegalArgumentException(Errors.format(
                    Errors.Keys.MissingValueInColumn_1, TableColumn.IDENTIFIER.getHeader()));
        }
        return add(accessor.indexOf(identifier, true), node.getValue(TableColumn.VALUE));
    }

    /**
     * Implementation of {@link #add(TreeTable.Node)}, also invoked by {@code TreeNode.NewChild}.
     * This method will attempt to convert the given {@code value} to the expected type.
     *
     * @param  index  the index in the accessor (<em>not</em> the index in this collection).
     * @param  value  the property value to add.
     * @return {@code true} if the metadata changed as a result of this method call.
     */
    final boolean add(final int index, final Object value) throws IllegalStateException {
        if (ValueExistencePolicy.isNullOrEmpty(value)) {
            return false;
        }
        // Conversion attempt happen in the PropertyAccessor.set(…) method.
        final var changed = (Boolean) accessor.set(index, metadata, value, PropertyAccessor.APPEND);
        if (changed == null) {
            throw new IllegalStateException(Errors.format(Errors.Keys.ValueAlreadyDefined_1,
                    accessor.name(index, KeyNamePolicy.UML_IDENTIFIER)));
        }
        if (changed) {
            modCount++;
        }
        return changed;
    }

    /**
     * Returns a string representation of this collection for debugging purpose.
     * This string representation uses one line per element instead of formatting
     * everything on a single line.
     */
    @Override
    public String toString() {
        final String lineSeparator = System.lineSeparator();
        final StringBuilder buffer = new StringBuilder(512);
        parent.appendStringTo(buffer);
        buffer.append(lineSeparator);
        for (final TreeTable.Node node : this) {
            buffer.append("  ");
            ((TreeNode) node).appendStringTo(buffer);
            buffer.append(lineSeparator);
        }
        return buffer.toString();
    }
}
