/*
 * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  * Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  * Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  * Neither the name of JSR-310 nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.threeten.extra.scale;

import java.io.Serializable;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.joda.convert.FromString;
import org.joda.convert.ToString;

/**
 * An instantaneous point on the time-line measured in the TAI time-scale.
 * <p>
 * The <code>java.time</code> classes use the Java time-scale for simplicity.
 * That scale works on the assumption that the time-line is simple, there are no leap-seconds
 * and there are always 24 * 60 * 60 seconds in a day. Unfortunately, the Earth's rotation
 * is not straightforward, and a solar day does not match this definition.
 * <p>
 * This class is an alternative representation based on the TAI time-scale.
 * TAI is a single incrementing count of SI seconds.
 * There are no leap seconds or other discontinuities.
 * <p>
 * As a result of the simple definition, this time-scale would make an excellent timestamp.
 * However, there are, at the time of writing, few easy ways to obtain an accurate TAI instant,
 * but it is relatively easy to obtain a GPS instant.
 * GPS and TAI differ by the fixed amount of 19 seconds.
 * <p>
 * The duration between two points on the TAI time-scale is calculated solely using this class.
 * Do not use the {@code between} method on {@code Duration} as that will lose information.
 * Instead use {@link #durationUntil(TaiInstant)} on this class.
 * <p>
 * It is intended that most applications will use the {@code Instant} class
 * which uses the UTC-SLS mapping from UTC to guarantee 86400 seconds per day.
 * Specialist applications with access to an accurate time-source may find this class useful.
 *
 * <h3>Time-scale</h3>
 * <p>
 * The TAI time-scale is a very simple well-regarded representation of time.
 * The scale is defined using atomic clocks counting SI seconds.
 * It has proceeded in a continuous uninterrupted manner since the defined
 * epoch of {@code 1958-01-01T00:00:00(TAI)}.
 * There are no leap seconds or other discontinuities.
 * <p>
 * This class may be used for instants in the far past and far future.
 * Since some instants will be prior to 1958, it is not strictly an implementation of TAI.
 * Instead, it is a proleptic time-scale based on TAI and equivalent to it since 1958.
 *
 * <h3>Implementation Requirements:</h3>
 * This class is immutable and thread-safe.
 * <p>
 * This class must be treated as a value type. Do not synchronize, rely on the
 * identity hash code or use the distinction between equals() and ==.
 */
public final class TaiInstant
        implements Comparable<TaiInstant>, Serializable {
    // does not implement Temporal as that would enable methods like
    // Duration.between which gives the wrong answer due to lossy conversion

    /**
     * Constant for nanos per second.
     */
    private static final int NANOS_PER_SECOND = 1000000000;
    /**
     * Parse regex.
     */
    private static final Pattern PARSER = Pattern.compile("([-]?[0-9]+)\\.([0-9]{9})s[(]TAI[)]");
    /**
     * Serialization version.
     */
    private static final long serialVersionUID = 2133469726395847026L;

    /**
     * The number of seconds from the epoch of 1958-01-01T00:00:00(TAI).
     */
    private final long seconds;
    /**
     * The number of nanoseconds, later along the time-line, from the seconds field.
     * This is always positive, and never exceeds 999,999,999.
     */
    private final int nanos;

    //-----------------------------------------------------------------------
    /**
     * Obtains an instance of {@code TaiInstant} from the number of seconds from
     * the TAI epoch of 1958-01-01T00:00:00(TAI) with a nanosecond fraction of second.
     * <p>
     * This method allows an arbitrary number of nanoseconds to be passed in.
     * The factory will alter the values of the second and nanosecond in order
     * to ensure that the stored nanosecond is in the range 0 to 999,999,999.
     * For example, the following will result in the exactly the same instant:
     * <pre>
     *  TaiInstant.ofTaiSeconds(3, 1);
     *  TaiInstant.ofTaiSeconds(4, -999999999);
     *  TaiInstant.ofTaiSeconds(2, 1000000001);
     * </pre>
     *
     * @param taiSeconds  the number of seconds from the epoch of 1958-01-01T00:00:00(TAI)
     * @param nanoAdjustment  the nanosecond adjustment to the number of seconds, positive or negative
     * @return the TAI instant, not null
     * @throws ArithmeticException if numeric overflow occurs
     */
    public static TaiInstant ofTaiSeconds(long taiSeconds, long nanoAdjustment) {
        long secs = Math.addExact(taiSeconds, Math.floorDiv(nanoAdjustment, NANOS_PER_SECOND));
        int nos = (int) Math.floorMod(nanoAdjustment, NANOS_PER_SECOND);  // safe cast
        return new TaiInstant(secs, nos);
    }

    /**
     * Obtains an instance of {@code TaiInstant} from an {@code Instant}.
     * <p>
     * Converting a UTC-SLS instant to TAI requires leap second rules.
     * This method uses the latest available system rules.
     * The conversion first maps from UTC-SLS to UTC, then converts to TAI.
     * <p>
     * Conversion from an {@code Instant} will not be completely accurate near
     * a leap second in accordance with UTC-SLS.
     *
     * @param instant  the instant to convert, not null
     * @return the TAI instant, not null
     * @throws DateTimeException if the range of {@code TaiInstant} is exceeded
     * @throws ArithmeticException if numeric overflow occurs
     */
    public static TaiInstant of(Instant instant) {
        return UtcRules.system().convertToTai(instant);
    }

    /**
     * Obtains an instance of {@code TaiInstant} from a {@code UtcInstant}.
     * <p>
     * Converting a UTC instant to TAI requires leap second rules.
     * This method uses the latest available system rules.
     * <p>
     * The {@code TaiInstant} will represent exactly the same point on the
     * time-line as per the available leap-second rules.
     * If the leap-second rules change then conversion back to UTC may
     * result in a different instant.
     *
     * @param instant  the instant to convert, not null
     * @return the TAI instant, not null
     * @throws DateTimeException if the range of {@code TaiInstant} is exceeded
     * @throws ArithmeticException if numeric overflow occurs
     */
    public static TaiInstant of(UtcInstant instant) {
        return UtcRules.system().convertToTai(instant);
    }

    //-------------------------------------------------------------------------
    /**
     * Obtains an instance of {@code TaiInstant} from a text string.
     * <p>
     * The following format is accepted:
     * <ul>
     * <li>{@code {seconds}.{nanosOfSecond}s(TAI)}
     * </ul>
     * <p>
     * The accepted format is strict.
     * The seconds part must contain only numbers and a possible leading negative sign.
     * The nanoseconds part must contain exactly nine digits.
     * The trailing literal must be exactly specified.
     * This format parses the {@code toString} format.
     *
     * @param text  the text to parse such as "12345.123456789s(TAI)", not null
     * @return the parsed instant, not null
     * @throws DateTimeParseException if the text cannot be parsed
     */
    @FromString
    public static TaiInstant parse(CharSequence text) {
        Objects.requireNonNull(text, "text");
        Matcher matcher = PARSER.matcher(text);
        if (matcher.matches()) {
            try {
                long seconds = Long.parseLong(matcher.group(1));
                long nanos = Long.parseLong(matcher.group(2));
                return TaiInstant.ofTaiSeconds(seconds, nanos);
            } catch (NumberFormatException ex) {
                throw new DateTimeParseException("The text could not be parsed", text, 0, ex);
            }
        }
        throw new DateTimeParseException("The text could not be parsed", text, 0);
    }

    //-----------------------------------------------------------------------
    /**
     * Constructs an instance.
     *
     * @param taiSeconds  the number of TAI seconds from the epoch
     * @param nanoOfSecond  the nanoseconds within the second, from 0 to 999,999,999
     */
    private TaiInstant(long taiSeconds, int nanoOfSecond) {
        super();
        this.seconds = taiSeconds;
        this.nanos = nanoOfSecond;
    }

    //-----------------------------------------------------------------------
    /**
     * Gets the number of seconds from the TAI epoch of 1958-01-01T00:00:00(TAI).
     * <p>
     * The TAI second count is a simple incrementing count of seconds where
     * second 0 is 1958-01-01T00:00:00(TAI).
     * The nanosecond part of the day is returned by {@code getNanosOfSecond}.
     *
     * @return the seconds from the epoch of 1958-01-01T00:00:00(TAI)
     */
    public long getTaiSeconds() {
        return seconds;
    }

    /**
     * Returns a copy of this {@code TaiInstant} with the number of seconds
     * from the TAI epoch of 1958-01-01T00:00:00(TAI).
     * <p>
     * The TAI second count is a simple incrementing count of seconds where
     * second 0 is 1958-01-01T00:00:00(TAI).
     * The nanosecond part of the day is returned by {@code getNanosOfSecond}.
     * <p>
     * This instance is immutable and unaffected by this method call.
     *
     * @param taiSeconds  the number of seconds from the epoch of 1958-01-01T00:00:00(TAI)
     * @return a {@code TaiInstant} based on this instant with the requested second, not null
     */
    public TaiInstant withTaiSeconds(long taiSeconds) {
        return ofTaiSeconds(taiSeconds, nanos);
    }

    /**
     * Gets the number of nanoseconds, later along the time-line, from the start
     * of the second.
     * <p>
     * The nanosecond-of-second value measures the total number of nanoseconds from
     * the second returned by {@code getTaiSeconds()}.
     *
     * @return the nanoseconds within the second, from 0 to 999,999,999
     */
    public int getNano() {
        return nanos;
    }

    /**
     * Returns a copy of this {@code TaiInstant} with the nano-of-second value changed.
     * <p>
     * The nanosecond-of-second value measures the total number of nanoseconds from
     * the second returned by {@code getTaiSeconds()}.
     * <p>
     * This instance is immutable and unaffected by this method call.
     *
     * @param nanoOfSecond  the nano-of-second, from 0 to 999,999,999
     * @return a {@code TaiInstant} based on this instant with the requested nano-of-second, not null
     * @throws IllegalArgumentException if nanoOfSecond is out of range
     */
    public TaiInstant withNano(int nanoOfSecond) {
        if (nanoOfSecond < 0 || nanoOfSecond >= NANOS_PER_SECOND) {
            throw new IllegalArgumentException("NanoOfSecond must be from 0 to 999,999,999");
        }
        return ofTaiSeconds(seconds, nanoOfSecond);
    }

    //-----------------------------------------------------------------------
    /**
     * Returns a copy of this instant with the specified duration added.
     * <p>
     * The duration is added using simple addition of the seconds and nanoseconds
     * in the duration to the seconds and nanoseconds of this instant.
     * As a result, the duration is treated as being measured in TAI compatible seconds
     * for the purpose of this method.
     * <p>
     * This instance is immutable and unaffected by this method call.
     *
     * @param duration  the duration to add, not null
     * @return a {@code TaiInstant} based on this instant with the duration added, not null
     * @throws ArithmeticException if the calculation exceeds the supported range
     */
    public TaiInstant plus(Duration duration) {
        long secsToAdd = duration.getSeconds();
        int nanosToAdd = duration.getNano();
        if ((secsToAdd | nanosToAdd) == 0) {
            return this;
        }
        long secs = Math.addExact(seconds, secsToAdd);
        long nanoAdjustment = ((long) nanos) + nanosToAdd;  // safe int+int
        return ofTaiSeconds(secs, nanoAdjustment);
    }

    //-----------------------------------------------------------------------
    /**
     * Returns a copy of this instant with the specified duration subtracted.
     * <p>
     * The duration is subtracted using simple subtraction of the seconds and nanoseconds
     * in the duration from the seconds and nanoseconds of this instant.
     * As a result, the duration is treated as being measured in TAI compatible seconds
     * for the purpose of this method.
     * <p>
     * This instance is immutable and unaffected by this method call.
     *
     * @param duration  the duration to subtract, not null
     * @return a {@code TaiInstant} based on this instant with the duration subtracted, not null
     * @throws ArithmeticException if the calculation exceeds the supported range
     */
    public TaiInstant minus(Duration duration) {
        long secsToSubtract = duration.getSeconds();
        int nanosToSubtract = duration.getNano();
        if ((secsToSubtract | nanosToSubtract) == 0) {
            return this;
        }
        long secs = Math.subtractExact(seconds, secsToSubtract);
        long nanoAdjustment = ((long) nanos) - nanosToSubtract;  // safe int+int
        return ofTaiSeconds(secs, nanoAdjustment);
    }

    //-----------------------------------------------------------------------
    /**
     * Returns the duration between this instant and the specified instant.
     * <p>
     * This calculates the duration between this instant and another based on
     * the TAI time-scale. Adding the duration to this instant using {@link #plus}
     * will always result in an instant equal to the specified instant.
     *
     * @param otherInstant  the instant to calculate the duration until, not null
     * @return the duration until the specified instant, may be negative, not null
     * @throws ArithmeticException if the calculation exceeds the supported range
     */
    public Duration durationUntil(TaiInstant otherInstant) {
        long durSecs = Math.subtractExact(otherInstant.seconds, seconds);
        long durNanos = otherInstant.nanos - nanos;
        return Duration.ofSeconds(durSecs, durNanos);
    }

    //-----------------------------------------------------------------------
    /**
     * Converts this instant to an {@code Instant}.
     * <p>
     * Converting a TAI instant to UTC-SLS requires leap second rules.
     * This method uses the latest available system rules.
     * The conversion first maps from TAI to UTC, then converts to UTC-SLS.
     * <p>
     * Conversion to an {@code Instant} will not be completely accurate near
     * a leap second in accordance with UTC-SLS.
     *
     * @return an {@code Instant} representing the best approximation of this instant, not null
     * @throws DateTimeException if the range of {@code Instant} is exceeded
     * @throws ArithmeticException if numeric overflow occurs
     */
    public Instant toInstant() {
        return UtcRules.system().convertToInstant(this);
    }

    /**
     * Converts this instant to a {@code UtcInstant}.
     * <p>
     * Converting a TAI instant to UTC requires leap second rules.
     * This method uses the latest available system rules.
     * <p>
     * The {@code UtcInstant} will represent exactly the same point on the
     * time-line as per the available leap-second rules.
     * If the leap-second rules change then conversion back to TAI may
     * result in a different instant.
     *
     * @return a {@code UtcInstant} representing the same instant, not null
     * @throws DateTimeException if the range of {@code UtcInstant} is exceeded
     * @throws ArithmeticException if numeric overflow occurs
     */
    public UtcInstant toUtcInstant() {
        return UtcRules.system().convertToUtc(this);
    }

    //-----------------------------------------------------------------------
    /**
     * Compares this instant to another based on the time-line.
     *
     * @param otherInstant  the other instant to compare to, not null
     * @return the comparator value, negative if less, positive if greater
     */
    @Override
    public int compareTo(TaiInstant otherInstant) {
        int cmp = Long.compare(seconds, otherInstant.seconds);
        if (cmp != 0) {
            return cmp;
        }
        return nanos - otherInstant.nanos;
    }

    //-----------------------------------------------------------------------
    /**
     * Checks if this instant is equal to the specified {@code TaiInstant}.
     *
     * @param otherInstant  the other instant, null returns false
     * @return true if the other instant is equal to this one
     */
    @Override
    public boolean equals(Object otherInstant) {
        if (this == otherInstant) {
            return true;
        }
        if (otherInstant instanceof TaiInstant) {
            TaiInstant other = (TaiInstant) otherInstant;
            return this.seconds == other.seconds &&
                    this.nanos == other.nanos;
        }
        return false;
    }

    /**
     * Returns a hash code for this instant.
     *
     * @return a suitable hash code
     */
    @Override
    public int hashCode() {
        // TODO: Evaluate hash code
        return ((int) (seconds ^ (seconds >>> 32))) + 51 * nanos;
    }

    //-----------------------------------------------------------------------
    /**
     * A string representation of this instant.
     * <p>
     * The string is formatted as {@code {seconds).(nanosOfSecond}s(TAI)}.
     * At least one second digit will be present.
     * The nanoseconds will always be nine digits.
     *
     * @return a representation of this instant, not null
     */
    @Override
    @ToString
    public String toString() {
        StringBuilder buf = new StringBuilder();
        buf.append(seconds);
        int pos = buf.length();
        buf.append(nanos + NANOS_PER_SECOND);
        buf.setCharAt(pos, '.');
        buf.append("s(TAI)");
        return buf.toString();
    }

}
