/*
 * 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.HashMap;
import jakarta.xml.bind.annotation.XmlTransient;
import org.apache.sis.xml.NilReason;
import org.apache.sis.util.Emptiable;
import org.apache.sis.util.ComparisonMode;
import org.apache.sis.util.LenientComparable;
import org.apache.sis.util.collection.TreeTable;


/**
 * Provides basic operations using Java reflection for metadata implementations.
 * All {@code AbstractMetadata} instances shall be associated to a {@link MetadataStandard}.
 * The metadata standard is given by the {@link #getStandard()} method and is typically a
 * constant fixed by the subclass.
 *
 * <p>There is a large number of {@code AbstractMetadata} subclasses (not necessarily as direct children)
 * for the same standard, where each subclass implement one Java interface defined by the metadata standard.
 * This base class reduces the effort required to implement those metadata interfaces by providing
 * {@link #equals(Object)}, {@link #hashCode()} and {@link #toString()} implementations.
 * Those methods are implemented using Java reflection for invoking the getter methods
 * defined by the {@code MetadataStandard}.</p>
 *
 * {@code AbstractMetadata} subclasses may be read-only or read/write, at implementation choice.
 * The methods that modify the metadata may throw {@link UnmodifiableMetadataException} if the
 * metadata does not support the operation. Those methods are:
 *
 * <table class="sis">
 * <caption>Metadata operations</caption>
 * <tr>
 *   <th>Read-only operations</th>
 *   <th class="sep">Read/write operations</th>
 * </tr>
 * <tr>
 *   <td><ul>
 *     <li>{@link #isEmpty()}</li>
 *     <li>{@link #asMap()} with {@code get} operations</li>
 *     <li>{@link #asTreeTable()} with {@code getValue} operations</li>
 *     <li>{@link #equals(Object, ComparisonMode)}</li>
 *   </ul></td>
 *   <td class="sep"><ul>
 *     <li>{@link #prune()}</li>
 *     <li>{@link #asMap()} with {@code put} operations</li>
 *     <li>{@link #asTreeTable()} with {@code setValue} operations</li>
 *   </ul></td>
 * </tr>
 * </table>
 *
 * <h2>Thread safety</h2>
 * Instances of this class are <strong>not</strong> synchronized for multi-threading.
 * Synchronization, if needed, is caller's responsibility. Note that synchronization locks
 * are not necessarily the metadata instances. For example, another common approach is to
 * use a single lock for the whole metadata tree (including children).
 *
 * @author  Martin Desruisseaux (Geomatys)
 * @version 1.5
 *
 * @see MetadataStandard
 *
 * @since 0.3
 */
@XmlTransient
public abstract class AbstractMetadata implements LenientComparable, Emptiable {
    /**
     * The reasons why some mandatory properties are absent. This map is used only for values of
     * classes that cannot be represented as instances of {@link org.apache.sis.xml.NilObject}.
     *
     * <h4>Mutability</h4>
     * We do not make this map unmodifiable when the enclosing metadata object is made unmodifiable.
     * It should not be necessary because all {@code put(…)} operations on this map are done only after
     * the corresponding {@code set(…)} operations on this metadata. So if the metadata is unmodifiable,
     * an exception should have been thrown before. On the other hand, {@code remove(…)} operations may
     * still be done for removing entries that shouldn't be there.
     *
     * <h4>Serialization</h4>
     * This field must be declared transient for preventing JAXB to inherit it in subclasses annotated
     * with {@code @XmlAccessorType(XmlAccessType.FIELD)}. Furthermore, serializing the integer values
     * would be unstable. We should serialize property names instead.
     *
     * @see NilReasonMap
     * @see #nilReasons()
     */
    transient HashMap<Integer,NilReason> nilReasons;

    /**
     * Creates an initially empty metadata.
     */
    protected AbstractMetadata() {
    }

    /**
     * Creates an initially empty metadata with nil reasons copied from the given object.
     * The given object should be a metadata of the same class.
     *
     * @param  source  the metadata from which to copy nil reasons, or {@code null} if none.
     *
     * @since 1.5
     */
    @SuppressWarnings("unchecked")
    protected AbstractMetadata(final Object source) {
        if (source instanceof AbstractMetadata) {
            nilReasons = ((AbstractMetadata) source).nilReasons;
            if (nilReasons != null) {
                nilReasons = (HashMap<Integer,NilReason>) nilReasons.clone();
            }
        }
    }

    /**
     * Returns the metadata standard implemented by subclasses.
     * Subclasses will typically return a hard-coded constant such as
     * {@link MetadataStandard#ISO_19115}.
     *
     * <h4>Note for implementers</h4>
     * Implementation of this method shall not depend on the object state,
     * since this method may be indirectly invoked by copy constructors.
     *
     * @return the metadata standard implemented.
     */
    public abstract MetadataStandard getStandard();

    /**
     * Returns the metadata interface implemented by this class. It should be one of the interfaces
     * defined in the {@linkplain #getStandard() metadata standard} implemented by this class.
     *
     * @return the standard interface implemented by this implementation class.
     *
     * @see MetadataStandard#getInterface(Class)
     */
    public Class<?> getInterface() {
        return getStandard().getInterface(getClass());
    }

    /**
     * Returns {@code true} if this metadata contains only {@code null},
     * {@linkplain org.apache.sis.xml.NilObject nil} or empty properties.
     * A non-null and non-nil property is considered empty in any of the following cases:
     *
     * <ul>
     *   <li>An empty {@linkplain CharSequence character sequences}.</li>
     *   <li>An {@linkplain java.util.Collection#isEmpty() empty collection} or an empty array.</li>
     *   <li>A collection or array containing only {@code null}, nil or empty elements.</li>
     *   <li>Another metadata object containing only {@code null}, nil or empty properties.</li>
     * </ul>
     *
     * Note that empty properties can be removed by calling the {@link ModifiableMetadata#prune()} method.
     *
     * <h4>Note for implementers</h4>
     * The default implementation uses Java reflection indirectly, by iterating over all entries
     * returned by {@link MetadataStandard#asValueMap(Object, Class, KeyNamePolicy, ValueExistencePolicy)}.
     * Subclasses that override this method should usually not invoke {@code super.isEmpty()},
     * because the Java reflection will discover and process the properties defined in the
     * subclasses - which is usually not the intent when overriding a method.
     *
     * @return {@code true} if this metadata is empty.
     *
     * @see org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox#isEmpty()
     */
    @Override
    public boolean isEmpty() {
        return Pruner.isEmpty(this, false);
    }

    /**
     * Removes all references to empty properties. The default implementation iterates over all
     * {@linkplain ValueExistencePolicy#NON_NULL non null} properties, and sets to {@code null}
     * the properties for which {@link #isEmpty()} returned {@code true}.
     *
     * @throws UnmodifiableMetadataException if this metadata is not modifiable.
     */
    public void prune() {
        Pruner.isEmpty(this, true);
    }

    /**
     * Returns a view of the property values in a {@link Map}. The map is backed by this metadata object,
     * so changes in the underlying metadata object are immediately reflected in the map and conversely.
     * The map supports the {@link Map#put(Object, Object) put(…)} and {@link Map#remove(Object) remove(…)}
     * operations if the underlying metadata object contains setter methods.
     * The {@code remove(…)} method is implemented by a call to {@code put(…, null)}.
     *
     * <h4>Keys and values</h4>
     * The keys are case-insensitive and can be either the JavaBeans property name, the getter method name
     * or the {@linkplain org.opengis.annotation.UML#identifier() UML identifier}. The value given to a call
     * to the {@code put(…)} method shall be an instance of the type expected by the corresponding setter method,
     * or an instance of a type {@linkplain org.apache.sis.util.ObjectConverters#find(Class, Class) convertible}
     * to the expected type.
     *
     * <h4>Multi-values entries</h4>
     * Calls to {@code put(…)} replace the previous value, with one noticeable exception: if the metadata
     * property associated to the given key is a {@link java.util.Collection} but the given value is a single
     * element (not a collection), then the given value is {@linkplain java.util.Collection#add(Object) added}
     * to the existing collection. In other words, the returned map behaves as a <i>multi-values map</i>
     * for the properties that allow multiple values. If the intent is to unconditionally discard all previous
     * values, then make sure that the given value is a collection when the associated metadata property expects
     * such collection.
     *
     * <h4>Default implementation</h4>
     * The default implementation is equivalent to the following:
     *
     * {@snippet lang="java" :
     *     return getStandard().asValueMap(this, null, KeyNamePolicy.JAVABEANS_PROPERTY, ValueExistencePolicy.NON_EMPTY);
     *     }
     *
     * @return a view of this metadata object as a map.
     *
     * @see MetadataStandard#asValueMap(Object, Class, KeyNamePolicy, ValueExistencePolicy)
     */
    public Map<String,Object> asMap() {
        return getStandard().asValueMap(this, null, KeyNamePolicy.JAVABEANS_PROPERTY, ValueExistencePolicy.NON_EMPTY);
    }

    /**
     * Returns a view of the reasons why some properties are missing.
     * The map is backed by the metadata object using Java reflection, so changes in the
     * underlying metadata object are immediately reflected in the map and conversely.
     *
     * <h4>Mandatory and optional properties</h4>
     * If a {@linkplain org.opengis.annotation.Obligation#MANDATORY mandatory} property has no value,
     * then the property will have an entry in the map even if the associated {@link NilReason} is null.
     * By contrast, {@linkplain org.opengis.annotation.Obligation#OPTIONAL optional} properties have
     * entries in the map only if they have a non-null {@link NilReason}.
     *
     * <h4>Default implementation</h4>
     * The default implementation is equivalent to the following:
     *
     * {@snippet lang="java" :
     *     return getStandard().asNilReasonMap(this, null, KeyNamePolicy.JAVABEANS_PROPERTY);
     *     }
     *
     * @return a view of the reasons why some properties are missing.
     *
     * @see MetadataStandard#asNilReasonMap(Object, Class, KeyNamePolicy)
     *
     * @since 1.5
     */
    public Map<String,NilReason> nilReasons() {
        return getStandard().asNilReasonMap(this, null, KeyNamePolicy.JAVABEANS_PROPERTY);
    }

    /**
     * Returns the property types and values as a tree table.
     * The tree table is backed by the metadata object using Java reflection, so changes in the
     * underlying metadata object are immediately reflected in the tree table and conversely.
     *
     * <p>The returned {@code TreeTable} instance contains the columns listed below.
     * The {@code (IDENTIFIER, INDEX)} pair of columns can be used as a primary key for uniquely identifying
     * a node in a list of children. That uniqueness is guaranteed only for the children of a given node.
     * The same keys may appear in the children of any other nodes.</p>
     *
     * <ul class="verbose">
     *   <li>{@link org.apache.sis.util.collection.TableColumn#IDENTIFIER}<br>
     *       The {@linkplain org.opengis.annotation.UML#identifier() UML identifier} if any,
     *       or the Java Beans property name otherwise, of a metadata property. For example
     *       in a tree table view of {@link org.apache.sis.metadata.iso.citation.DefaultCitation},
     *       there is a node having the {@code "title"} identifier.</li>
     *
     *   <li>{@link org.apache.sis.util.collection.TableColumn#INDEX}<br>
     *       If the metadata property is a collection, then the zero-based index of the element in that collection.
     *       Otherwise {@code null}. For example, in a tree table view of {@code DefaultCitation}, if the
     *       {@code "alternateTitle"} collection contains two elements, then there is a node with index 0
     *       for the first element and another node with index 1 for the second element.</li>
     *
     *   <li>{@link org.apache.sis.util.collection.TableColumn#NAME}<br>
     *       A human-readable name for the node, derived from the identifier and the index.
     *       This is the column shown in the default {@link #toString()} implementation and
     *       may be localizable.</li>
     *
     *   <li>{@link org.apache.sis.util.collection.TableColumn#TYPE}<br>
     *       The base type of the value (usually an interface).</li>
     *
     *   <li>{@link org.apache.sis.util.collection.TableColumn#OBLIGATION}<br>
     *       Whether the property is mandatory, optional or conditional.</li>
     *
     *   <li>{@link org.apache.sis.util.collection.TableColumn#VALUE}<br>
     *       The metadata value for the node. Values in this column are writable if the underlying
     *       metadata class have a setter method for the property represented by the node.</li>
     *
     *   <li>{@link org.apache.sis.util.collection.TableColumn#REMARKS}<br>
     *       Remarks or warning on the property value. This is rarely present.
     *       It is provided when the value may look surprising, for example the longitude values
     *       in a geographic bounding box crossing the anti-meridian.</li>
     * </ul>
     *
     * <h4>Write operations</h4>
     * Only the {@code VALUE} column may be writable, with one exception: newly created children need
     * to have their {@code IDENTIFIER} set before any other operation. For example, the following code
     * adds a title to a citation:
     *
     * {@snippet lang="java" :
     *     TreeTable.Node node = ...;                               // The node for a DefaultCitation.
     *     TreeTable.Node child = node.newChild();
     *     child.setValue(TableColumn.IDENTIFIER, "title");
     *     child.setValue(TableColumn.VALUE, "Le petit prince");
     *     // Nothing else to do - the child node has been added.
     *     }
     *
     * Nodes can be removed by invoking the {@link java.util.Iterator#remove()} method on the
     * {@linkplain org.apache.sis.util.collection.TreeTable.Node#getChildren() children} iterator.
     *
     * <h4>Default implementation</h4>
     * The default implementation is equivalent to the following:
     *
     * {@snippet lang="java" :
     *     return getStandard().asTreeTable(this, null, ValueExistencePolicy.COMPACT);
     *     }
     *
     * @return a tree table representation of the specified metadata.
     *
     * @see MetadataStandard#asTreeTable(Object, Class, ValueExistencePolicy)
     */
    public TreeTable asTreeTable() {
        return getStandard().asTreeTable(this, null, ValueExistencePolicy.COMPACT);
    }

    /**
     * Compares this metadata with the specified object for equality. The default
     * implementation uses Java reflection. Subclasses may override this method
     * for better performances, or for comparing "hidden" properties not specified
     * by the GeoAPI (or other standard) interface.
     *
     * @param  object  the object to compare with this metadata.
     * @param  mode    the strictness level of the comparison.
     * @return {@code true} if the given object is equal to this metadata.
     */
    @Override
    public boolean equals(final Object object, final ComparisonMode mode) {
        return getStandard().equals(this, object, mode);
    }

    /**
     * Performs a {@linkplain ComparisonMode#STRICT strict} comparison of this metadata with
     * the given object. This method is implemented as below:
     *
     * {@snippet lang="java" :
     *     public final boolean equals(final Object object) {
     *         return equals(object, ComparisonMode.STRICT);
     *     }
     * }
     *
     * If a subclass needs to override the behavior of this method, then
     * override {@link #equals(Object, ComparisonMode)} instead.
     *
     * @param  object  the object to compare with this metadata for equality.
     * @return {@code true} if the given object is strictly equals to this metadata.
     */
    @Override
    public final boolean equals(final Object object) {
        return equals(object, ComparisonMode.STRICT);
    }

    /**
     * Computes a hash code value for this metadata using Java reflection. The hash code
     * is defined as the sum of hash code values of all non-empty properties, excluding
     * cyclic dependencies. For acyclic metadata, this method contract is compatible with
     * the {@link java.util.Set#hashCode()} one and ensures that the hash code value is
     * insensitive to the ordering of properties.
     *
     * <h4>Implementation note</h4>
     * This method does not cache the value because current implementation has no notification mechanism
     * for tracking changes in children properties. If this metadata is known to be immutable,
     * then subclasses may consider caching the hash code value if performance is important.
     *
     * @see MetadataStandard#hashCode(Object)
     */
    @Override
    public int hashCode() {
        return getStandard().hashCode(this);
    }

    /**
     * Returns a string representation of this metadata.
     * The default implementation is as below:
     *
     * {@snippet lang="java" :
     *     return asTreeTable().toString();
     *     }
     *
     * Note that this make extensive use of Unicode characters
     * and is better rendered with a monospaced font.
     */
    @Override
    public String toString() {
        return asTreeTable().toString();
    }

    /**
     * Returns a <em>shallow</em> clone of this metadata. The properties values and the children are not cloned.
     * This method is for internal usage by {@link ModifiableMetadata} and should usually not be invoked directly.
     *
     * @return a shallow clone of this metadata.
     * @throws CloneNotSupportedException if this metadata is not cloneable.
     *
     * @see ModifiableMetadata#deepCopy(ModifiableMetadata.State)
     * @see MetadataCopier
     */
    @Override
    @SuppressWarnings("unchecked")
    protected AbstractMetadata clone() throws CloneNotSupportedException {
        final var clone = (AbstractMetadata) super.clone();
        if (clone.nilReasons != null) {
            clone.nilReasons = (HashMap<Integer,NilReason>) clone.nilReasons.clone();
        }
        return clone;
    }
}
