/*
 * 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.Collection;
import java.util.Collections;
import org.opengis.metadata.extent.TemporalExtent;
import org.opengis.metadata.extent.GeographicExtent;
import org.opengis.metadata.extent.GeographicBoundingBox;
import org.opengis.geometry.Envelope;
import org.opengis.geometry.DirectPosition;
import org.opengis.util.InternationalString;
import org.apache.sis.util.iso.Types;
import org.apache.sis.geometry.Envelope2D;
import org.apache.sis.geometry.GeneralDirectPosition;

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


/**
 * Identifiable geographic place. A geographic place may be identified by a name (for example “Eiffel Tower”),
 * by a postcode, or any other method specified by the {@linkplain ModifiableLocationType#getIdentifications()
 * location type identifications}.
 *
 * <p>ISO 19112 describes the following properties as mandatory, but Apache SIS relaxes this restriction by
 * providing default values (possibly {@code null}) in most cases:</p>
 * <ul>
 *   <li><b>geographic identifier</b> (the value, for example a name or code)</li>
 *   <li><b>geographic extent</b> (the position of the identified thing)</li>
 *   <li><b>administrator</b> (who is responsible for this identifier)</li>
 *   <li><b>location type</b> (which specifies the nature of the identifier and its associated geographic location)</li>
 * </ul>
 *
 * The following properties are optional:
 * <ul>
 *   <li><b>temporal extent</b></li>
 *   <li><b>alternative geographic identifier</b></li>
 *   <li><b>envelope</b> (an Apache SIS extension not in ISO 19112 standard)</li>
 *   <li><b>position</b> (mandatory if the geographic identifier contains insufficient information to identify location)</li>
 *   <li><b>parent location instance</b></li>
 *   <li><b>child location instance</b></li>
 * </ul>
 *
 * @author  Martin Desruisseaux (Geomatys)
 * @version 0.8
 *
 * @see ModifiableLocationType
 * @see ReferencingByIdentifiers
 * @see LocationFormat
 *
 * @since 0.8
 */
public abstract class AbstractLocation {
    /**
     * The description of the nature of this geographic identifier, or {@code null} if unspecified.
     *
     * <p>This field is non-final for sub-class constructors convenience,
     * but its value should not be changed after {@code Location} construction.</p>
     *
     * @see #getLocationType()
     */
    private AbstractLocationType type;

    /**
     * The geographic identifier, or {@code null} if unspecified.
     *
     * @see #getGeographicIdentifier()
     */
    private final CharSequence identifier;

    /**
     * Creates a new location for the given geographic identifier.
     * This constructor accepts {@code null} arguments, but this is not recommended.
     *
     * <div class="warning"><b>Upcoming API change — generalization</b><br>
     * in a future SIS version, the type of {@code type} argument may be generalized to the
     * {@code org.opengis.referencing.gazetteer.Location} interface.
     * This change is pending GeoAPI revision.</div>
     *
     * @param type        the description of the nature of this geographic identifier.
     * @param identifier  the geographic identifier to be returned by {@link #getGeographicIdentifier()}.
     */
    protected AbstractLocation(final ModifiableLocationType type, final CharSequence identifier) {
        this.type       = type;
        this.identifier = identifier;
    }

    /**
     * Temporary workaround for the lack of {@code LocationType} interface and {@code AbstractLocationType}
     * being package-private. We do not want to expose {@code AbstractLocationType} in public API for now.
     */
    AbstractLocation(final AbstractLocationType type, final CharSequence identifier) {
        this.type       = type;
        this.identifier = identifier;
    }

    /**
     * Sets the location type to the unique child of current type. This method should be invoked
     * only when the caller know that there is at least one children and that the children class
     * is {@link FinalLocationType} (for performance reason).
     */
    final void setTypeToChild() {
        type = ((FinalLocationType) type).children.get(0);
    }

    /**
     * Returns a unique identifier for the location instance. The methods of identifying locations is specified
     * by the {@linkplain ModifiableLocationType#getIdentifications() location type identifications}.
     *
     * In order to ensure that a geographic identifier is unique within a wider geographic domain,
     * the geographic identifier may need to include an identifier of an instance of a parent location type,
     * for example “Paris, Texas”.
     *
     * <h4>Examples</h4>
     * If {@code LocationType.getIdentifications()} contain “name”, then geographic identifiers may be country
     * names like “Japan” or “France”, or places like “Eiffel Tower”. If location type identifications contain
     * “code”, then geographic identifiers may be “SW1P 3AD” postcode.
     *
     * @return unique identifier for the location instance.
     *
     * @see ModifiableLocationType#getIdentifications()
     */
    public InternationalString getGeographicIdentifier() {
        return Types.toInternationalString(identifier);
    }

    /**
     * Returns other identifier(s) for the location instance.
     * The default implementation returns an empty set.
     *
     * @return other identifier(s) for the location instance, or an empty collection if none.
     */
    public Collection<? extends InternationalString> getAlternativeGeographicIdentifiers() {
        return Collections.emptySet();
    }

    /**
     * Returns the date of creation of this version of the location instance.
     * The default implementation returns {@code null}.
     *
     * @return date of creation of this version of the location instance, or {@code null} if none.
     */
    public TemporalExtent getTemporalExtent() {
        return null;
    }

    /**
     * Returns a description of the location instance. This properties is mandatory according ISO 19112,
     * but Apache SIS nevertheless allows {@code null} value. If non-null, SIS implementations typically
     * provide instances of {@linkplain org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox
     * geographic bounding boxes}.
     *
     * @return description of the location instance, or {@code null} if none.
     *
     * @see org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox
     * @see org.apache.sis.metadata.iso.extent.DefaultBoundingPolygon
     */
    public GeographicExtent getGeographicExtent() {
        return null;
    }

    /**
     * Returns an envelope that encompass the location. This property is partially redundant with
     * {@link #getGeographicExtent()}, except that this method allows envelopes in non-geographic CRS.
     *
     * <p>The default implementation copies the {@link #getGeographicExtent()} in a new envelope associated
     * to the {@linkplain org.apache.sis.referencing.CommonCRS#defaultGeographic() default geographic CRS}.</p>
     *
     * @return envelope that encompass the location, or {@code null} if none.
     */
    public Envelope getEnvelope() {
        final GeographicExtent extent = getGeographicExtent();
        return (extent instanceof GeographicBoundingBox) ? new Envelope2D((GeographicBoundingBox) extent) : null;
    }

    /**
     * Returns coordinates of a representative point for the location instance.
     * This is typically (but not necessarily) the centroid of the location instance.
     *
     * <p>The default implementation returns the {@linkplain #getEnvelope()} median position.</p>
     *
     * @return coordinates of a representative point for the location instance, or {@code null} if none.
     */
    public DirectPosition getPosition() {
        final Envelope envelope = getEnvelope();
        if (envelope == null) {
            return null;
        }
        final int dimension = envelope.getDimension();
        final var pos = new GeneralDirectPosition(dimension);
        pos.setCoordinateReferenceSystem(envelope.getCoordinateReferenceSystem());
        for (int i=0; i<dimension; i++) {
            pos.setCoordinate(i, envelope.getMedian(i));
        }
        return pos;
    }

    /**
     * Returns a description of the nature of this geographic identifier.
     *
     * <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.
     * If applied, this method will be made non-final.</div>
     *
     * @return the nature of the identifier and its associated geographic location.
     */
    public final ModifiableLocationType getLocationType() {
        return ModifiableLocationTypeAdapter.copy(type);
    }

    /**
     * Workaround for the lack of {@code LocationType} interface in GeoAPI 3.0.
     * This workaround will be removed in a future SIS version if the location
     * type interface is introduced in a future GeoAPI version.
     */
    final AbstractLocationType type() {
        return type;
    }

    /**
     * Returns the organization responsible for defining the characteristics of the location instance.
     * The default implementation returns the {@linkplain ModifiableLocationType#getOwner() owner}.
     *
     * <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 organization responsible for defining the characteristics of the location instance, or {@code null}.
     *
     * @see ModifiableLocationType#getOwner()
     * @see ReferencingByIdentifiers#getOverallOwner()
     */
    public AbstractParty getAdministrator() {
        return (type != null) ? type.getOwner() : null;
    }

    /**
     * Returns location instances of a different location type, for which this location instance is a sub-division.
     * The default implementation returns an empty list.
     *
     * <div class="warning"><b>Upcoming API change — generalization</b><br>
     * in a future SIS version, the type of collection elements may be generalized
     * to the {@code org.opengis.referencing.gazetteer.Location} interface.
     * This change is pending GeoAPI revision.</div>
     *
     * @return parent locations, or an empty collection if none.
     *
     * @see ModifiableLocationType#getParents()
     */
    public Collection<? extends AbstractLocation> getParents() {
        return Collections.emptyList();
    }

    /**
     * Returns location instances of a different location type which subdivides this location instance.
     * The default implementation returns an empty list.
     *
     * <div class="warning"><b>Upcoming API change — generalization</b><br>
     * in a future SIS version, the type of collection elements may be generalized
     * to the {@code org.opengis.referencing.gazetteer.Location} interface.
     * This change is pending GeoAPI revision.</div>
     *
     * @return child locations, or an empty collection if none.
     *
     * @see ModifiableLocationType#getChildren()
     */
    public Collection<? extends AbstractLocation> getChildren() {
        return Collections.emptyList();
    }

    /**
     * Returns a string representation of this location.
     * This representation is mostly for debugging purpose and may change in any future Apache SIS version.
     *
     * @return a string representation of this location for debugging purpose.
     */
    @Override
    public String toString() {
        synchronized (LocationFormat.INSTANCE) {
            return LocationFormat.INSTANCE.format(this);
        }
    }
}
