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

import java.util.Map;
import java.util.Objects;
import jakarta.xml.bind.annotation.XmlType;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import javax.measure.Unit;
import javax.measure.quantity.Angle;
import org.opengis.util.GenericName;
import org.opengis.util.InternationalString;
import org.opengis.referencing.datum.PrimeMeridian;
import org.apache.sis.referencing.AbstractIdentifiedObject;
import org.apache.sis.referencing.internal.shared.Formulas;
import org.apache.sis.referencing.internal.shared.WKTUtilities;
import org.apache.sis.referencing.internal.shared.ReferencingUtilities;
import org.apache.sis.referencing.internal.shared.WKTKeywords;
import org.apache.sis.metadata.internal.shared.ImplementationHelper;
import org.apache.sis.xml.bind.gml.Measure;
import org.apache.sis.util.ComparisonMode;
import org.apache.sis.util.internal.shared.Numerics;
import org.apache.sis.io.wkt.Formatter;
import org.apache.sis.io.wkt.Convention;
import org.apache.sis.measure.Units;
import static org.apache.sis.util.ArgumentChecks.ensureFinite;

// Specific to the main and geoapi-3.1 branches:
import org.opengis.referencing.crs.GeneralDerivedCRS;

// Specific to the main branch:
import org.opengis.referencing.ReferenceIdentifier;


/**
 * Defines the origin from which longitude values are determined.
 *
 * <h2>Creating new prime meridian instances</h2>
 * New instances can be created either directly by specifying all information to a factory method (choices 3
 * and 4 below), or indirectly by specifying the identifier of an entry in a database (choices 1 and 2 below).
 * In particular, the <a href="https://epsg.org/">EPSG repository</a> provides definitions for many prime meridians,
 * and Apache SIS provides convenience shortcuts for some of them.
 *
 * <p>Choice 1 in the following list is the easiest but most restrictive way to get a prime meridian.
 * The other choices provide more freedom. Each choice delegates its work to the subsequent items
 * (in the default configuration), so this list can be seen as <i>top to bottom</i> API.</p>
 *
 * <ol>
 *   <li>Create a {@code PrimeMeridian} from one of the static convenience shortcuts listed in
 *       {@link org.apache.sis.referencing.CommonCRS#primeMeridian()}.</li>
 *   <li>Create a {@code PrimeMeridian} from an identifier in a database by invoking
 *       {@link org.opengis.referencing.datum.DatumAuthorityFactory#createPrimeMeridian(String)}.</li>
 *   <li>Create a {@code PrimeMeridian} by invoking the {@code DatumFactory.createPrimeMeridian(…)} method
 *       (implemented for example by {@link org.apache.sis.referencing.factory.GeodeticObjectFactory}).</li>
 *   <li>Create a {@code DefaultPrimeMeridian} by invoking the
 *       {@linkplain #DefaultPrimeMeridian(Map, double, Unit) constructor}.</li>
 * </ol>
 *
 * <b>Example:</b> the following code gets the Greenwich prime meridian:
 *
 * {@snippet lang="java" :
 *     PrimeMeridian pm = CommonCRS.WGS84.primeMeridian();
 *     }
 *
 * <h2>Immutability and thread safety</h2>
 * This 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. Unless otherwise noted in the javadoc, this condition holds if
 * all components were created using only SIS factories and static constants.
 *
 * @author  Martin Desruisseaux (IRD, Geomatys)
 * @author  Cédric Briançon (Geomatys)
 * @version 1.4
 *
 * @see org.apache.sis.referencing.CommonCRS#primeMeridian()
 * @see org.apache.sis.referencing.factory.GeodeticAuthorityFactory#createPrimeMeridian(String)
 *
 * @since 0.4
 */
@XmlType(name = "PrimeMeridianType")
@XmlRootElement(name = "PrimeMeridian")
public class DefaultPrimeMeridian extends AbstractIdentifiedObject implements PrimeMeridian {
    /**
     * Serial number for inter-operability with different versions.
     */
    private static final long serialVersionUID = 541978454643213305L;;

    /**
     * Longitude of the prime meridian measured from the Greenwich meridian, positive eastward.
     *
     * <p><b>Consider this field as final!</b>
     * This field is modified only at unmarshalling time by {@link #setGreenwichMeasure(Measure)}</p>
     */
    private double greenwichLongitude;

    /**
     * The angular unit of the {@linkplain #getGreenwichLongitude() Greenwich longitude}.
     *
     * <p><b>Consider this field as final!</b>
     * This field is modified only at unmarshalling time by {@link #setGreenwichMeasure(Measure)}</p>
     */
    @SuppressWarnings("serial")         // Most SIS implementations are serializable.
    private Unit<Angle> angularUnit;

    /**
     * Creates a prime meridian from the given properties. The properties map is given unchanged to the
     * {@linkplain AbstractIdentifiedObject#AbstractIdentifiedObject(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>{@value org.opengis.referencing.IdentifiedObject#NAME_KEY}</td>
     *     <td>{@link ReferenceIdentifier} or {@link String}</td>
     *     <td>{@link #getName()}</td>
     *   </tr><tr>
     *     <td>{@value org.opengis.referencing.IdentifiedObject#ALIAS_KEY}</td>
     *     <td>{@link 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 ReferenceIdentifier} (optionally as array)</td>
     *     <td>{@link #getIdentifiers()}</td>
     *   </tr><tr>
     *     <td>{@value org.opengis.referencing.IdentifiedObject#REMARKS_KEY}</td>
     *     <td>{@link InternationalString} or {@link String}</td>
     *     <td>{@link #getRemarks()}</td>
     *   </tr>
     * </table>
     *
     * @param  properties          the properties to be given to the identified object.
     * @param  greenwichLongitude  the longitude value relative to the Greenwich Meridian.
     * @param  angularUnit         the angular unit of the longitude.
     *
     * @see org.apache.sis.referencing.factory.GeodeticObjectFactory#createPrimeMeridian(Map, double, Unit)
     */
    public DefaultPrimeMeridian(final Map<String,?> properties, final double greenwichLongitude,
                                final Unit<Angle> angularUnit)
    {
        super(properties);
        ensureFinite("greenwichLongitude", greenwichLongitude);
        this.greenwichLongitude = greenwichLongitude;
        this.angularUnit = Objects.requireNonNull(angularUnit);
    }

    /**
     * Creates a new prime meridian with the same values as the specified one.
     * This copy constructor provides a way to convert an arbitrary implementation into a SIS one
     * or a user-defined one (as a subclass), usually in order to leverage some implementation-specific API.
     *
     * <p>This constructor performs a shallow copy, i.e. the properties are not cloned.</p>
     *
     * @param  meridian  the prime meridian to copy.
     *
     * @see #castOrCopy(PrimeMeridian)
     */
    protected DefaultPrimeMeridian(final PrimeMeridian meridian) {
        super(meridian);
        greenwichLongitude = meridian.getGreenwichLongitude();
        angularUnit        = meridian.getAngularUnit();
    }

    /**
     * Returns a SIS prime meridian implementation with the same values as the given arbitrary implementation.
     * If the given object is {@code null}, then this method returns {@code null}.
     * Otherwise if the given object is already a SIS implementation, then the given object is returned unchanged.
     * Otherwise a new SIS implementation is created and initialized to the attribute values of the given object.
     *
     * @param  object  the object to get as a SIS implementation, or {@code null} if none.
     * @return a SIS implementation containing the values of the given object (may be the
     *         given object itself), or {@code null} if the argument was null.
     */
    public static DefaultPrimeMeridian castOrCopy(final PrimeMeridian object) {
        return (object == null) || (object instanceof DefaultPrimeMeridian)
                ? (DefaultPrimeMeridian) object : new DefaultPrimeMeridian(object);
    }

    /**
     * Returns the GeoAPI interface implemented by this class.
     * The SIS implementation returns {@code PrimeMeridian.class}.
     *
     * <h4>Note for implementers</h4>
     * Subclasses usually do not need to override this method since GeoAPI does not define {@code PrimeMeridian}
     * sub-interface. Overriding possibility is left mostly for implementers who wish to extend GeoAPI with their
     * own set of interfaces.
     *
     * @return {@code PrimeMeridian.class} or a user-defined sub-interface.
     */
    @Override
    public Class<? extends PrimeMeridian> getInterface() {
        return PrimeMeridian.class;
    }

    /**
     * Longitude of the prime meridian measured from the Greenwich meridian, positive eastward.
     *
     * @return the prime meridian Greenwich longitude, in {@linkplain #getAngularUnit() angular unit}.
     */
    @Override
    public double getGreenwichLongitude() {
        return greenwichLongitude;
    }

    /**
     * Returns the longitude value relative to the Greenwich Meridian, expressed in the specified units.
     * This convenience method makes it easier to obtain longitude in decimal degrees using the following
     * code, regardless of the underlying angular units of this prime meridian:
     *
     * {@snippet lang="java" :
     *     double longitudeInDegrees = primeMeridian.getGreenwichLongitude(Units.DEGREE);
     *     }
     *
     * @param  unit  the unit in which to express longitude.
     * @return the Greenwich longitude in the given units.
     */
    public double getGreenwichLongitude(final Unit<Angle> unit) {
        return getAngularUnit().getConverterTo(unit).convert(getGreenwichLongitude());
    }

    /**
     * Returns the angular unit of the Greenwich longitude.
     *
     * @return the angular unit of the {@linkplain #getGreenwichLongitude() Greenwich longitude}.
     */
    @Override
    public Unit<Angle> getAngularUnit() {
        return angularUnit;
    }

    /**
     * Compares this prime meridian with the specified object for equality.
     *
     * @param  object  the object to compare to {@code this}.
     * @param  mode    the strictness level of the comparison.
     * @return {@code true} if both objects are equal.
     *
     * @hidden because nothing new to said.
     */
    @Override
    public boolean equals(final Object object, final ComparisonMode mode) {
        if (object == this) {
            return true;                                // Slight optimization.
        }
        if (super.equals(object, mode)) switch (mode) {
            case STRICT: {
                final var that = (DefaultPrimeMeridian) object;
                return Numerics.equals(this.greenwichLongitude, that.greenwichLongitude) &&
                        Objects.equals(this.angularUnit,        that.angularUnit);
            }
            case BY_CONTRACT: {
                final var that = (PrimeMeridian) object;
                return Numerics.equals(getGreenwichLongitude(), that.getGreenwichLongitude()) &&
                        Objects.equals(getAngularUnit(),        that.getAngularUnit());
            }
            default: {
                final double v1 = getGreenwichLongitude(Units.DEGREE);
                final double v2 = ReferencingUtilities.getGreenwichLongitude((PrimeMeridian) object, Units.DEGREE);
                if (mode.isIgnoringMetadata()) {
                    /*
                     * We relax the check on unit of measurement because EPSG uses sexagesimal degrees
                     * for the Greenwich meridian.  Requirying the same unit would make more difficult
                     * for isWGS84(…) methods to recognize EPSG's WGS84. We allow this relaxation here
                     * because the unit of the prime meridian is usually not inherited by axes (indeed,
                     * they are often not the same in the EPSG dataset). The same is not true for other
                     * objects like DefaultEllipsoid.
                     */
                    if (Numerics.epsilonEqual(v1, v2, Formulas.ANGULAR_TOLERANCE)) {
                        return true;
                    }
                } else {
                    return Numerics.equals(v1, v2);
                }
                assert (mode != ComparisonMode.DEBUG) : Numerics.messageForDifference("greenwichLongitude", v1, v2);
            }
        }
        return false;
    }

    /**
     * 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.
     *
     * @hidden because nothing new to said.
     */
    @Override
    protected long computeHashCode() {
        return super.computeHashCode() + Double.doubleToLongBits(greenwichLongitude) + Objects.hashCode(angularUnit);
    }

    /**
     * Returns {@code true} if the given formatter is in the process of formatting the prime meridian of a base CRS
     * of an {@code AbstractDerivedCRS}. In such case, base CRS coordinate system axes shall not be formatted, which
     * has the consequence of bringing the {@code UNIT[…]} element right below the {@code PRIMEM[…]} one. Example:
     *
     * {@snippet lang="wkt" :
     *   ProjectedCRS[“NTF (Paris) / Lambert zone II”,
     *     BaseGeogCRS[“NTF (Paris)”,
     *       Datum[“Nouvelle Triangulation Francaise”,
     *         Ellipsoid[“NTF”, 6378249.2, 293.4660212936269]],
     *       PrimeMeridian[“Paris”, 2.5969213],
     *       AngleUnit[“grad”, 0.015707963267948967]],
     *     Conversion[“Lambert zone II”,
     *       etc...
     *   }
     *
     * If we were not formatting a base CRS, we would have many lines between {@code PrimeMeridian[…]} and
     * {@code AngleUnit[…]} in the above example, which would make less obvious that the angle unit applies
     * also to the prime meridian. It does not bring any ambiguity from an ISO 19162 standard point of view,
     * but historically some other software products interpreted the {@code PRIMEM[…]} units wrongly, which
     * is why we try to find a compromise between keeping the WKT simple and avoiding an historical ambiguity.
     *
     * @see org.apache.sis.referencing.crs.AbstractCRS#isBaseCRS(Formatter)
     */
    private static boolean isElementOfBaseCRS(final Formatter formatter) {
        return formatter.getEnclosingElement(2) instanceof GeneralDerivedCRS;
    }

    /**
     * Returns {@code true} if {@link #formatTo(Formatter)} should conservatively format the angular unit
     * even if it would be legal to omit it.
     *
     * <h4>Rational</h4>
     * According the ISO 19162 standard, it is legal to omit the {@code PrimeMeridian} angular unit when
     * that unit is the same as the unit of the axes of the enclosing {@code GeographicCRS}. However, the
     * relationship between the CRS axes and the prime meridian is less obvious in WKT2 than it was in WKT1,
     * because the WKT2 {@code UNIT[…]} element is far from the {@code PRIMEM[…]} element while it was just
     * below it in WKT1. Furthermore, the {@code PRIMEM[…]} unit is one source of incompatibility between
     * various WKT1 parsers (i.e. some popular libraries are not conform to OGC 01-009 and ISO 19162).
     * So we are safer to unconditionally format any unit other than degrees, even if we could legally
     * omit them.
     *
     * <p>However, in order to keep the WKT slightly simpler in {@link Convention#WKT2_SIMPLIFIED} mode,
     * we make an exception to the above-cited safety if the {@code UNIT[…]} element is formatted right
     * below the {@code PRIMEM[…]} one, which happen if we are inside a base CRS.
     * See {@link #isElementOfBaseCRS(Formatter)} for more discussion.
     */
    private static boolean beConservative(final Formatter formatter, final Unit<Angle> contextualUnit) {
        return !contextualUnit.equals(Units.DEGREE) && !isElementOfBaseCRS(formatter);
    }

    /**
     * Formats this prime meridian as a <i>Well Known Text</i> {@code PrimeMeridian[…]} element.
     *
     * @return {@code "PrimeMeridian"} (WKT 2) or {@code "PrimeM"} (WKT 1).
     */
    @Override
    protected String formatTo(final Formatter formatter) {
        super.formatTo(formatter);
        final Convention  convention = formatter.getConvention();
        final boolean     isWKT1 = (convention.majorVersion() == 1);
        final Unit<Angle> contextualUnit = formatter.toContextualUnit(Units.DEGREE);
        Unit<Angle> unit = contextualUnit;
        if (!isWKT1) {
            unit = getAngularUnit();
            if (convention != Convention.INTERNAL) {
                unit = WKTUtilities.toFormattable(unit);
            }
        }
        formatter.append(getGreenwichLongitude(unit));
        if (isWKT1) {
            return WKTKeywords.PrimeM;
        }
        if (!convention.isSimplified() || !contextualUnit.equals(unit) || beConservative(formatter, contextualUnit)) {
            formatter.append(unit);
        }
        return formatter.shortOrLong(WKTKeywords.PrimeM, WKTKeywords.PrimeMeridian);
    }




    /*
     ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
     ┃                                                                                  ┃
     ┃                               XML support with JAXB                              ┃
     ┃                                                                                  ┃
     ┃        The following methods are invoked by JAXB using reflection (even if       ┃
     ┃        they are private) or are helpers for other methods invoked by JAXB.       ┃
     ┃        Those methods can be safely removed if Geographic Markup Language         ┃
     ┃        (GML) support is not needed.                                              ┃
     ┃                                                                                  ┃
     ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
     */

    /**
     * Constructs a new object in which every attributes are set to a null value.
     * <strong>This is not a valid object.</strong> This constructor is strictly
     * reserved to JAXB, which will assign values to the fields using reflection.
     */
    private DefaultPrimeMeridian() {
        super(org.apache.sis.referencing.internal.shared.NilReferencingObject.INSTANCE);
        /*
         * Angular units are mandatory for SIS working. We do not verify their presence here (because the
         * verification would have to be done in an 'afterMarshal(…)' method and throwing an exception in
         * that method causes the whole unmarshalling to fail). But the CD_PrimeMeridian adapter does some
         * verifications.
         */
    }

    /**
     * Invoked by JAXB for obtaining the Greenwich longitude to marshal together with its {@code "uom"} attribute.
     */
    @XmlElement(name = "greenwichLongitude", required = true)
    private Measure getGreenwichMeasure() {
        return new Measure(greenwichLongitude, angularUnit);
    }

    /**
     * Invoked by JAXB for setting the Greenwich longitude and its unit of measurement.
     */
    private void setGreenwichMeasure(final Measure measure) {
        if (greenwichLongitude == 0 && angularUnit == null) {
            greenwichLongitude = measure.value;
            angularUnit = measure.getUnit(Angle.class);
            if (angularUnit == null) {
                /*
                 * Missing unit: if the Greenwich longitude is zero, any angular unit gives the same result
                 * (assuming that the missing unit was not applying an offset), so we can select a default.
                 * If the Greenwich longitude is not zero, presume egrees but log a warning.
                 */
                angularUnit = Units.DEGREE;
                if (greenwichLongitude != 0) {
                    Measure.missingUOM(DefaultPrimeMeridian.class, "setGreenwichMeasure");
                }
            }
        } else {
            ImplementationHelper.propertyAlreadySet(DefaultPrimeMeridian.class, "setGreenwichMeasure", "greenwichLongitude");
        }
    }
}
