/*
 * 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.referencing.gazetteer;

import java.util.Map;
import java.util.List;
import java.util.Objects;
import java.util.HashMap;
import java.util.logging.Logger;
import jakarta.xml.bind.annotation.XmlTransient;
import javax.measure.Quantity;
import javax.measure.IncommensurableException;
import org.opengis.util.InternationalString;
import org.opengis.geometry.DirectPosition;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.referencing.AbstractReferenceSystem;
import org.apache.sis.referencing.ImmutableIdentifier;
import org.apache.sis.util.ComparisonMode;
import org.apache.sis.util.Utilities;
import org.apache.sis.util.Debug;
import org.apache.sis.util.collection.Containers;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.iso.Types;
import org.apache.sis.io.wkt.Formatter;
import org.apache.sis.io.wkt.ElementKind;
import org.apache.sis.io.wkt.FormattableObject;
import org.apache.sis.metadata.iso.extent.Extents;
import org.apache.sis.metadata.iso.citation.Citations;
import org.apache.sis.referencing.util.WKTUtilities;
import org.apache.sis.system.Modules;
import org.apache.sis.util.internal.Constants;
import org.apache.sis.util.resources.Vocabulary;

// Specific to the main branch:
import org.apache.sis.metadata.iso.citation.AbstractParty;


/**
 * Base class of reference systems that describe locations using geographic identifiers instead of coordinates.
 *
 * <h2>Immutability and thread safety</h2>
 * This base class is immutable and thus thread-safe if the property <em>values</em> (not necessarily the map itself)
 * given to the constructor are also immutable. Most SIS subclasses and related classes are immutable under similar
 * conditions. This means that unless otherwise noted in the javadoc, {@code ReferencingByIdentifiers} instances
 * created using only SIS factories and static constants can be shared by many objects and passed between threads
 * without synchronization.
 *
 * @author  Martin Desruisseaux (Geomatys)
 * @version 1.4
 *
 * @see ModifiableLocationType
 * @see AbstractLocation
 *
 * @since 0.8
 */
@XmlTransient
public abstract class ReferencingByIdentifiers extends AbstractReferenceSystem {
    /**
     * Key for the <code>{@value}</code> property to be given to the
     * object factory {@code createFoo(…)} methods.
     * This is used for setting the value to be returned by {@link #getTheme()}.
     *
     * @see #getTheme()
     */
    public static final String THEME_KEY = "theme";

    /**
     * Key for the <code>{@value}</code> property to be given to the
     * object factory {@code createFoo(…)} methods.
     * This is used for setting the value to be returned by {@link #getOverallOwner()}.
     *
     * @see #getOverallOwner()
     */
    public static final String OVERALL_OWNER_KEY = "overallOwner";

    /**
     * Serial number for inter-operability with different versions.
     */
    private static final long serialVersionUID = 5353942546043471933L;

    /**
     * The logger for coordinate operations.
     */
    static final Logger LOGGER = Logger.getLogger(Modules.REFERENCING_BY_IDENTIFIERS);

    /**
     * Property used to characterize the spatial reference system.
     *
     * @see #getTheme()
     */
    @SuppressWarnings("serial")         // Most SIS implementations are serializable.
    private final InternationalString theme;

    /**
     * Authority with overall responsibility for the spatial reference system.
     *
     * @see #getOverallOwner()
     */
    private final AbstractParty overallOwner;

    /**
     * Description of location type(s) in the spatial reference system.
     * This collection shall be unmodifiable.
     *
     * @see #getLocationTypes()
     */
    @SuppressWarnings("serial")
    final List<AbstractLocationType> locationTypes;

    /**
     * Creates a reference system from the given properties.
     * The properties given in argument follow the same rules than for the
     * {@linkplain AbstractReferenceSystem#AbstractReferenceSystem(Map) super-class constructor}.
     * The following table is a reminder of main (not all) properties:
     *
     * <table class="sis">
     *   <caption>Recognized properties (non exhaustive list)</caption>
     *   <tr>
     *     <th>Property name</th>
     *     <th>Value type</th>
     *     <th>Returned by</th>
     *   </tr>
     *   <tr>
     *     <td>"theme"</td>
     *     <td>{@link String} or {@link InternationalString}</td>
     *     <td>{@link #getTheme()}</td>
     *   </tr>
     *   <tr>
     *     <td>"overallOwner"</td>
     *     <td>{@code Party}</td>
     *     <td>{@link #getOverallOwner()}</td>
     *   </tr>
     *   <tr>
     *     <th colspan="3" class="hsep">Defined in parent class (reminder)</th>
     *   </tr>
     *   <tr>
     *     <td>{@value org.opengis.referencing.IdentifiedObject#NAME_KEY}</td>
     *     <td>{@link org.opengis.metadata.Identifier} or {@link String}</td>
     *     <td>{@link #getName()}</td>
     *   </tr>
     *   <tr>
     *     <td>{@value org.opengis.referencing.IdentifiedObject#ALIAS_KEY}</td>
     *     <td>{@link org.opengis.util.GenericName} or {@link CharSequence} (optionally as array)</td>
     *     <td>{@link #getAlias()}</td>
     *   </tr>
     *   <tr>
     *     <td>{@value org.opengis.referencing.IdentifiedObject#IDENTIFIERS_KEY}</td>
     *     <td>{@link org.opengis.metadata.Identifier} (optionally as array)</td>
     *     <td>{@link #getIdentifiers()}</td>
     *   </tr>
     *   <tr>
     *     <td>{@value org.opengis.referencing.IdentifiedObject#REMARKS_KEY}</td>
     *     <td>{@link org.opengis.util.InternationalString} or {@link String}</td>
     *     <td>{@link #getRemarks()}</td>
     *   </tr>
     *   <tr>
     *     <td>{@value org.opengis.referencing.ReferenceSystem#DOMAIN_OF_VALIDITY_KEY}</td>
     *     <td>{@link org.opengis.metadata.extent.Extent}</td>
     *     <td>{@link #getDomainOfValidity()}</td>
     *   </tr>
     *   <tr>
     *     <td>{@value org.opengis.referencing.ReferenceSystem#SCOPE_KEY}</td>
     *     <td>{@link org.opengis.util.InternationalString} or {@link String}</td>
     *     <td>{@link #getScope()}</td>
     *   </tr>
     * </table>
     *
     * This constructor copies the given {@code LocationType} instances as per
     * {@code ModifiableLocationType.snapshot(ReferenceSystemUsingIdentifiers, LocationType...)}.
     * Changes in the given location types after construction will not affect this {@code ReferencingByIdentifiers}.
     *
     * <div class="warning"><b>Upcoming API change — generalization</b><br>
     * In a future SIS version, the type of array elements may be generalized to the
     * {@code org.opengis.referencing.gazetteer.LocationType} interface.
     * This change is pending GeoAPI revision.</div>
     *
     * @param properties  the properties to be given to the reference system.
     * @param types       description of location type(s) in the spatial reference system.
     */
    @SuppressWarnings("ThisEscapedInObjectConstruction")
    public ReferencingByIdentifiers(final Map<String,?> properties, final ModifiableLocationType... types) {
        super(properties);
        theme = Types.toInternationalString(properties, THEME_KEY);
        overallOwner = Containers.property(properties, OVERALL_OWNER_KEY, AbstractParty.class);
        /*
         * Having the 'this' reference escaped in object construction should not be an issue here because
         * we invoke package-private method in such a way that if an exception is thrown, the whole tree
         * (with all 'this' references) will be discarded.
         */
        locationTypes = AbstractLocationType.snapshot(this, types);
    }

    /**
     * Convenience method for helping subclasses to build their argument for the constructor.
     * The returned properties have the domain of validity set to the whole word and the theme to "mapping".
     *
     * @param name   the reference system name as an {@link org.opengis.metadata.Identifier} or a {@link String}.
     * @param id     an identifier for the reference system. Use SIS namespace until we find an authority for them.
     * @param party  the overall owner, or {@code null} if none.
     */
    static Map<String,Object> properties(final Object name, final String id, final AbstractParty party) {
        final Map<String,Object> properties = new HashMap<>(8);
        properties.put(NAME_KEY, name);
        properties.put(IDENTIFIERS_KEY, new ImmutableIdentifier(Citations.SIS, Constants.SIS, id));
        properties.put(DOMAIN_OF_VALIDITY_KEY, Extents.WORLD);
        properties.put(THEME_KEY, Vocabulary.formatInternational(Vocabulary.Keys.Mapping));
        properties.put(OVERALL_OWNER_KEY, party);
        return properties;
    }
    /**
     * Property used to characterize the spatial reference system.
     *
     * @return property used to characterize the spatial reference system.
     *
     * @see ModifiableLocationType#getTheme()
     */
    public InternationalString getTheme() {
        return theme;
    }

    /**
     * Authority with overall responsibility for the spatial reference system.
     *
     * <div class="warning"><b>Upcoming API change — generalization</b><br>
     * in a future SIS version, the type of returned element may be generalized to the
     * {@code org.opengis.metadata.citation.Party} interface. This change is pending
     * GeoAPI revision for upgrade from ISO 19115:2003 to ISO 19115:2014.</div>
     *
     * @return authority with overall responsibility for the spatial reference system.
     *
     * @see ModifiableLocationType#getOwner()
     * @see AbstractLocation#getAdministrator()
     */
    public AbstractParty getOverallOwner() {
        return overallOwner;
    }

    /**
     * Description of location type(s) in the spatial reference system.
     * The collection returned by this method is unmodifiable.
     *
     * <div class="warning"><b>Upcoming API change — generalization</b><br>
     * in a future SIS version, the type of elements type may be generalized to the
     * {@code org.opengis.referencing.gazetteer.Location} interface.
     * This change is pending GeoAPI revision.</div>
     *
     * @return description of location type(s) in the spatial reference system.
     *
     * @see ModifiableLocationType#getReferenceSystem()
     */
    @SuppressWarnings("ReturnOfCollectionOrArrayField")         // Because the collection is unmodifiable.
    public List<? extends ModifiableLocationType> getLocationTypes() {
        return ModifiableLocationTypeAdapter.copy(locationTypes);
    }

    /**
     * Returns the first location type.
     */
    final AbstractLocationType rootType() {
        return locationTypes.get(0);
    }

    /**
     * Returns a new object performing conversions between {@code DirectPosition} and identifiers.
     * The returned object is <strong>not</strong> thread-safe; a new instance must be created
     * for each thread, or synchronization must be applied by the caller.
     *
     * @return a new object performing conversions between {@link DirectPosition} and identifiers.
     *
     * @since 1.3
     */
    public abstract Coder createCoder();

    /**
     * Conversions between direct positions and identifiers.
     * Each {@code Coder} instance can read references at arbitrary precision,
     * but formats at the {@linkplain #setPrecision specified approximate precision}.
     * The same {@code Coder} instance can be reused for reading or writing many identifiers.
     *
     * <h2>Immutability and thread safety</h2>
     * This class is <strong>not</strong> thread-safe. A new instance must be created for each thread,
     * or synchronization must be applied by the caller.
     *
     * @author  Martin Desruisseaux (Geomatys)
     * @version 1.4
     * @since   1.3
     */
    public abstract static class Coder {
        /**
         * Creates a new instance.
         */
        protected Coder() {
        }

        /**
         * Returns the reference system for which this coder is reading or writing identifiers.
         *
         * @return the enclosing reference system.
         */
        public abstract ReferencingByIdentifiers getReferenceSystem();

        /**
         * Returns approximate precision of the identifiers formatted by this coder at the given location.
         * The returned value is typically a length in linear unit (e.g. metres).
         * Precisions in angular units should be converted to linear units at the specified location.
         * If the location is {@code null}, then this method should return a precision for the worst case scenario.
         *
         * @param  position  where to evaluate the precision, or {@code null} for the worst case scenario.
         * @return approximate precision in metres of formatted identifiers.
         */
        public abstract Quantity<?> getPrecision(DirectPosition position);

        /**
         * Sets the desired precision of the identifiers formatted by this coder.
         * The given value is converted to coder-specific representation (e.g. number of digits).
         * The value returned by {@link #getPrecision(DirectPosition)} may be different than the
         * value specified to this method.
         *
         * @param  precision  the desired precision.
         * @param  position   location where the specified precision is desired, or {@code null} for the worst case scenario.
         * @throws IncommensurableException if the given precision uses incompatible units of measurement.
         */
        public abstract void setPrecision(Quantity<?> precision, DirectPosition position) throws IncommensurableException;

        /**
         * A combined method which sets the encoder precision to the given value, then formats the given position.
         * The default implementation is equivalent to the following code:
         *
         * {@snippet lang="java" :
         *     setPrecision(precision, position);
         *     return encode(position);
         *     }
         *
         * Subclasses should override with more efficient implementation,
         * for example by transforming the given position only once.
         *
         * @param  position   the coordinate to encode.
         * @param  precision  the desired precision.
         * @return identifier of the given position.
         * @throws IncommensurableException if the given precision uses incompatible units of measurement.
         * @throws TransformException if an error occurred while transforming the given coordinate to an identifier.
         */
        public String encode(DirectPosition position, Quantity<?> precision) throws IncommensurableException, TransformException {
            setPrecision(precision, position);
            return encode(position);
        }

        /**
         * Encodes the given position into an identifier.
         * The given position must have a Coordinate Reference System (CRS) associated to it.
         *
         * @param  position  the coordinate to encode.
         * @return identifier of the given position.
         * @throws TransformException if an error occurred while transforming the given coordinate to an identifier.
         */
        public abstract String encode(DirectPosition position) throws TransformException;

        /**
         * Decodes the given identifier into a latitude and a longitude.
         * The axis order depends on the coordinate reference system of the enclosing {@link ReferencingByIdentifiers}.
         *
         * <div class="warning"><b>Upcoming API change — generalization</b><br>
         * in a future SIS version, the type of returned element may be generalized
         * to the {@code org.opengis.referencing.gazetteer.Location} interface.
         * This change is pending GeoAPI revision.</div>
         *
         * @param  identifier  identifier string to decode.
         * @return a new geographic coordinate for the given identifier.
         * @throws TransformException if an error occurred while parsing the given string.
         */
        public abstract AbstractLocation decode(CharSequence identifier) throws TransformException;

        /**
         * Logs a warning for a recoverable error while transforming a position. This is used for implementations
         * of method such as {@link #getPrecision(DirectPosition)}, which can fallback on "worst case" scenario.
         *
         * @param caller  the class that wanted to transform a position.
         * @param method  the method that wanted to transform a position.
         * @param e       the transformation error.
         */
        static void recoverableException(final Class<?> caller, final String method, final Exception e) {
            Logging.recoverableException(LOGGER, caller, method, e);
        }
    }

    /**
     * Compares this reference system with the specified object for equality.
     * If the {@code mode} argument value is {@link ComparisonMode#STRICT STRICT} or
     * {@link ComparisonMode#BY_CONTRACT BY_CONTRACT}, then all available properties are
     * compared including the {@linkplain #getTheme() theme} and
     * the {@linkplain #getOverallOwner() overall owner}.
     *
     * @param  object  the object to compare to {@code this}.
     * @param  mode    {@link ComparisonMode#STRICT STRICT} for performing a strict comparison, or
     *                 {@link ComparisonMode#IGNORE_METADATA IGNORE_METADATA} for comparing only
     *                 properties relevant to location identifications.
     * @return {@code true} if both objects are equal.
     */
    @Override
    @SuppressWarnings("fallthrough")
    public boolean equals(final Object object, final ComparisonMode mode) {
        if (!super.equals(object, mode)) {
            return false;
        }
        switch (mode) {
            case STRICT: {
                final ReferencingByIdentifiers that = (ReferencingByIdentifiers) object;
                return Objects.equals(theme,        that.theme) &&
                       Objects.equals(overallOwner, that.overallOwner) &&
                       locationTypes.equals(that.locationTypes);
            }
            case BY_CONTRACT: {
                final ReferencingByIdentifiers that = (ReferencingByIdentifiers) object;
                if (!Utilities.deepEquals(getTheme(),        that.getTheme(),        mode) ||
                    !Utilities.deepEquals(getOverallOwner(), that.getOverallOwner(), mode))
                {
                    return false;
                }
                // Fall through
            }
            default: {
                // Theme and owner are metadata, so they can be ignored.
                final ReferencingByIdentifiers that = (ReferencingByIdentifiers) object;
                return Utilities.deepEquals(locationTypes, that.locationTypes, mode);
            }
        }
    }

    /**
     * Invoked by {@code hashCode()} for computing the hash code when first needed.
     * See {@link org.apache.sis.referencing.AbstractIdentifiedObject#computeHashCode()}
     * for more information.
     *
     * @return the hash code value. This value may change in any future Apache SIS version.
     */
    @Override
    protected long computeHashCode() {
        return super.computeHashCode() + Objects.hash(theme, overallOwner, locationTypes);
    }

    /**
     * Formats a pseudo-<cite>Well Known Text</cite> (WKT) representation for this object.
     * The format produced by this method is non-standard and may change in any future Apache SIS version.
     *
     * @param  formatter  the formatter where to format the inner content of this pseudo-WKT element.
     * @return an arbitrary keyword for the pseudo-WKT element.
     */
    @Debug
    @Override
    protected String formatTo(final Formatter formatter) {
        WKTUtilities.appendName(this, formatter, ElementKind.NAME);
        if (theme != null) {
            formatter.newLine();
            formatter.append(new SubElement("Theme", theme));
        }
        if (overallOwner != null) {
            formatter.newLine();
            formatter.append(new SubElement("Owner", overallOwner.getName()));
        }
        for (final AbstractLocationType type : locationTypes) {
            formatter.newLine();
            formatter.append(new SubElement("LocationType", type.getName()));
        }
        return "ReferenceSystemUsingIdentifiers";
    }

    /**
     * A sub-element inside the pseudo-WKT.
     */
    private static final class SubElement extends FormattableObject {
        /** The pseudo-WKT name of the element to format. */
        private final String name;

        /** The value of the element to format. */
        private final InternationalString value;

        /** Creates a new citation with the given value. */
        SubElement(final String name, final InternationalString value) {
            this.name  = name;
            this.value = value;
        }

        /** Formats the sub-element. */
        @Override
        protected String formatTo(final Formatter formatter) {
            formatter.append(value != null ? value.toString(formatter.getLocale()) : null, null);
            return name;
        }
    }
}
