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

import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.opengis.referencing.cs.AxisDirection;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.iso.Types;
import org.apache.sis.measure.Units;
import org.apache.sis.measure.Longitude;
import org.apache.sis.util.internal.shared.Numerics;
import org.apache.sis.referencing.internal.shared.AxisDirections;
import org.apache.sis.referencing.internal.shared.WKTKeywords;
import org.apache.sis.io.wkt.FormattableObject;
import org.apache.sis.io.wkt.Formatter;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.resources.Errors;


/**
 * Parses {@linkplain AxisDirection axis direction} of the kind <q>South along 90 deg East</q>.
 * Those directions are used in the EPSG database for polar stereographic projections.
 *
 * <h2>Reference meridian</h2>
 * This class does not know whether the meridian is relative to Greenwich or any other reference meridian.
 * The reference meridian shall be inferred from the geodetic reference frame of the {@code GeographicCRS}
 * instance that contains (through its coordinate system) the axes having those directions.
 * This is consistent with ISO 19162:2015 §7.5.4(iv) - WKT 2 formatting.
 *
 * @author  Martin Desruisseaux (IRD)
 */
final class DirectionAlongMeridian extends FormattableObject implements Comparable<DirectionAlongMeridian> {
    /**
     * A parser for EPSG axis names. Examples:
     *
     * <ul>
     *   <li><q>South along 180 deg</q></li>
     *   <li><q>South along 90 deg East</q></li>
     * </ul>
     */
    private static final Pattern EPSG = Pattern.compile(
            "(\\p{Graph}+)\\s+along\\s+([\\-\\p{Digit}\\.]+)(?:\\s+deg|\\s*°)\\s*(\\p{Graph}+)?",
            Pattern.CASE_INSENSITIVE);

    /**
     * The base directions we are interested in.
     * Any direction not in this group will be rejected by our parser.
     */
    private static final AxisDirection[] NORTH_SOUTH = {AxisDirection.NORTH, AxisDirection.SOUTH},
                                         EAST_WEST   = {AxisDirection.EAST,  AxisDirection.WEST};

    /**
     * The direction. Will be created only when first needed.
     *
     * @see #getDirection()
     */
    private transient AxisDirection direction;

    /**
     * The base direction, which must be {@link AxisDirection#NORTH} or {@link AxisDirection#SOUTH}.
     */
    public final AxisDirection baseDirection;

    /**
     * The meridian in degrees, relative to a unspecified (usually Greenwich) prime meridian.
     * Meridians in the East hemisphere are positive and meridians in the West hemisphere are negative.
     */
    public final double meridian;

    /**
     * Creates a direction.
     *
     * @param  baseDirection  the base direction, which must be {@link AxisDirection#NORTH} or {@link AxisDirection#SOUTH}.
     * @param  meridian       the meridian in degrees, relative to a unspecified (usually Greenwich) prime meridian.
     *         Meridians in the East hemisphere are positive and meridians in the West hemisphere are negative.
     */
    DirectionAlongMeridian(final AxisDirection baseDirection, final double meridian) {
        if (!baseDirection.equals(AxisDirection.NORTH) && !baseDirection.equals(AxisDirection.SOUTH)) {
            throw new IllegalArgumentException(Errors.format(
                    Errors.Keys.IllegalArgumentValue_2, "baseDirection", baseDirection));
        }
        ArgumentChecks.ensureBetween("meridian", Longitude.MIN_VALUE, Longitude.MAX_VALUE, meridian);
        this.baseDirection = baseDirection;
        this.meridian      = meridian;
    }

    /**
     * Returns {@code true} if the given direction is non-null
     * and has a name which seems to be a direction along meridian.
     */
    public static boolean matches(final String direction) {
        return (direction != null) && EPSG.matcher(direction).matches();
    }

    /**
     * Returns the direction along meridian for the specified axis direction, or {@code null} if none.
     *
     * <p>TIP: caller can check {@link AxisDirections#isUserDefined(AxisDirection)} before to invoke this method
     * for avoiding {@code DirectionAlongMeridian} initialization in the common case where it is not needed.</p>
     */
    public static DirectionAlongMeridian parse(final AxisDirection direction) {
        final DirectionAlongMeridian candidate;
        try {
            candidate = parse(direction.name());
        } catch (IllegalArgumentException e) {
            Logging.recoverableException(AbstractCS.LOGGER, DirectionAlongMeridian.class, "parse", e);
            return null;
        }
        if (candidate != null) {
            candidate.direction = direction;
        }
        return candidate;
    }

    /**
     * If the specified name is a direction along some specific meridian,
     * returns information about that. Otherwise returns {@code null}.
     *
     * @param  name  the name to parse.
     * @return the parsed name, or {@code null} if it is not a direction along a meridian.
     * @throws IllegalArgumentException if the given name looks like a direction along a meridian,
     *         but an error occurred during parsing.
     */
    public static DirectionAlongMeridian parse(final String name) throws IllegalArgumentException {
        final Matcher m = EPSG.matcher(name);
        if (!m.matches()) {
            // Not the expected pattern.
            return null;
        }
        final AxisDirection baseDirection = AxisDirections.find(m.group(1), NORTH_SOUTH);
        if (baseDirection == null) {                        // We require "North" or "South" direction.
            return null;
        }
        double meridian = Double.parseDouble(m.group(2));
        final String group = m.group(3);
        if (group != null) {
            final AxisDirection sgn = AxisDirections.find(group, EAST_WEST);
            if (sgn == null) {                              // We require "East" or "West" direction.
                return null;
            }
            if (sgn != AxisDirections.absolute(sgn)) {
                meridian = -meridian;
            }
        }
        return new DirectionAlongMeridian(baseDirection, meridian);
    }

    /**
     * Returns the axis direction for this object. If a suitable axis direction already exists,
     * it will be returned. Otherwise a new one is created and returned.
     */
    public AxisDirection getDirection() {
        if (direction == null) {
            final String name = toString();
            direction = AxisDirections.valueOf(name);
            if (direction == null) {
                direction = Types.forCodeName(AxisDirection.class, name, AxisDirection::valueOf);
            }
        }
        return direction;
    }

    /**
     * Returns the arithmetic (counterclockwise) angle from this direction to the specified direction,
     * in degrees. This method returns a value between -180° and +180°, or {@link Double#NaN} if the
     * {@linkplain #baseDirection base directions} don't match.
     * A positive angle denote a right-handed system.
     *
     * <h4>Example</h4>
     * The angle from <q>North along 90 deg East</q> to <q>North along 0 deg</q> is 90°.
     */
    public double angle(final DirectionAlongMeridian other) {
        if (!baseDirection.equals(other.baseDirection)) {
            return Double.NaN;
        }
        /*
         * Example: angle between (NORTH along 90°E, NORTH along 0°) shall be +90°
         */
        double angle = Longitude.normalize(meridian - other.meridian);
        /*
         * Reverses the sign for axis oriented toward SOUTH,
         * so a positive angle is a right-handed system.
         */
        if (AxisDirections.isOpposite(baseDirection)) {
            angle = -angle;
        }
        return angle;
    }

    /**
     * Compares this direction with the specified one for order. This method tries to reproduce
     * the ordering used for the majority of coordinate systems in the EPSG database, i.e. the
     * ordering of a right-handed coordinate system. Examples of ordered pairs that we should
     * get (extracted from the EPSG database):
     *
     * <table class="sis">
     *   <caption>Direction name examples</caption>
     *   <tr><td>North along 90° East,</td>  <td>North along 0°</td></tr>
     *   <tr><td>North along 75° West,</td>  <td>North along 165° West</td></tr>
     *   <tr><td>South along 90° West,</td>  <td>South along 0°</td></tr>
     *   <tr><td>South along 180°,</td>      <td>South along 90° West</td></tr>
     *   <tr><td>North along 130° West</td>  <td>North along 140° East</td></tr>
     * </table>
     */
    @Override
    public int compareTo(final DirectionAlongMeridian that) {
        final int c = baseDirection.compareTo(that.baseDirection);
        if (c != 0) {
            return c;
        }
        final double angle = angle(that);
        if (angle < 0) return +1;               // Really the opposite sign.
        if (angle > 0) return -1;               // Really the opposite sign.
        return 0;
    }

    /**
     * Tests this object for equality with the specified one.
     * This method is used mostly for assertions.
     */
    @Override
    public boolean equals(final Object object) {
        if (object instanceof DirectionAlongMeridian) {
            final var that = (DirectionAlongMeridian) object;
            return baseDirection.equals(that.baseDirection) &&
                   Numerics.equals(meridian, that.meridian);
        }
        return false;
    }

    /**
     * Returns a hash code value, for consistency with {@link #equals}.
     */
    @Override
    public int hashCode() {
        return Double.hashCode(meridian) + baseDirection.hashCode();
    }

    /**
     * Returns a string representation of this direction, using a syntax matching the one used
     * by EPSG. This string representation will be used for creating a new {@link AxisDirection}.
     * The generated name should be identical to EPSG name, but we use the generated one anyway
     * (rather than the one provided by EPSG) in order to make sure that we create a single
     * {@link AxisDirection} for a given direction; we avoid potential differences like lower
     * versus upper cases, number of white space, <i>etc</i>.
     */
    @Override
    public String toString() {
        String name = baseDirection.name();
        final int length = name.length();
        final var buffer = new StringBuilder(length);
        for (int i=0; i<length;) {
            final int c = name.codePointAt(i);
            buffer.appendCodePoint(i == 0 ? Character.toUpperCase(c) : Character.toLowerCase(c));
            i += Character.charCount(c);
        }
        buffer.append(" along ");
        final double md = Math.abs(meridian);
        final int    mi = (int) md;
        if (md == mi) {
            buffer.append(mi);
        } else {
            buffer.append(md);
        }
        buffer.append('°');
        if (md != 0 && md != Longitude.MAX_VALUE) {
            buffer.append(meridian < 0 ? 'W' : 'E');
        }
        name = buffer.toString();
        assert matches(name) : name;
        return name;
    }

    /**
     * Formats this object as a <i>Well Known Text</i> {@code Meridian[…]} element.
     * This element contains the meridian value and the unit of measurement.
     * The unit is currently fixed to degrees, but this may change in any future implementation.
     *
     * <h4>Compatibility note</h4>
     * {@code Meridian} is defined in the WKT 2 specification only.
     *
     * @param  formatter The formatter where to format the inner content of this WKT element.
     * @return {@code "Meridian"}.
     */
    @Override
    protected String formatTo(final Formatter formatter) {
        formatter.append(meridian);
        formatter.append(Units.DEGREE);
        return WKTKeywords.Meridian;
    }
}
