/*
 * ====================================================================
 * 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.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 */

package org.apache.hc.core5.util;

import java.text.ParseException;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.concurrent.TimeUnit;

/**
 * A deadline based on a UNIX time, the elapsed since 00:00:00 Coordinated Universal Time (UTC), Thursday, 1 January
 * 1970.
 *
 * @since 5.0
 */
public class Deadline {

    /**
     * The format used for parsing and formatting dates.
     */
    public static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";

    /**
     * A special internal value that marks a deadline as the longest possible.
     */
    private static final long INTERNAL_MAX_VALUE = Long.MAX_VALUE;

    /**
     * A special internal value that marks a deadline as the shortest possible.
     */
    private static final long INTERNAL_MIN_VALUE = 0;

    /**
     * The maximum (longest-lived) deadline.
     */
    public static Deadline MAX_VALUE = new Deadline(INTERNAL_MAX_VALUE);

    /**
     * The minimum (shortest-lived) deadline.
     */
    public static Deadline MIN_VALUE = new Deadline(INTERNAL_MIN_VALUE);

    private static final DateTimeFormatter DATE_TIME_FORMATTER = new DateTimeFormatterBuilder()
            .parseLenient()
            .parseCaseInsensitive()
            .appendPattern(DATE_FORMAT)
            .toFormatter();

    /**
     * Calculates a deadline with a given time in milliseconds plus a given time value. Non-positive time values
     * represent an indefinite timeout without a deadline.
     *
     * @param timeMillis A time in UNIX milliseconds, usually the current time.
     * @param timeValue time value to add to {@code timeMillis}.
     * @return a deadline representing the current time plus the given time value.
     */
    public static Deadline calculate(final long timeMillis, final TimeValue timeValue) {
        if (TimeValue.isPositive(timeValue)) {
            return Deadline.fromUnixMilliseconds(timeMillis +
                    Math.min(timeValue.toMilliseconds(), Long.MAX_VALUE - timeMillis));
        }
        return Deadline.MAX_VALUE;
    }

    /**
     * Calculates a deadline from now plus a given time value. Non-positive time values
     * represent an indefinite timeout without a deadline.
     *
     * @param timeValue time value to add to {@code timeMillis}.
     * @return a deadline representing the current time plus the given time value.
     */
    public static Deadline calculate(final TimeValue timeValue) {
        return calculate(System.currentTimeMillis(), timeValue);
    }

    /**
     * Creates a deadline from a UNIX time in milliseconds.
     *
     * @param value a UNIX time in milliseconds.
     * @return a new deadline.
     */
    public static Deadline fromUnixMilliseconds(final long value) {
        if (value == INTERNAL_MAX_VALUE) {
            return MAX_VALUE;
        }
        if (value == INTERNAL_MIN_VALUE) {
            return MIN_VALUE;
        }
        return new Deadline(value);
    }

    /**
     * Creates a deadline from a string in the format {@value #DATE_FORMAT}.
     *
     * @param source a string in the format {@value #DATE_FORMAT}.
     * @return a deadline from a string in the format {@value #DATE_FORMAT}.
     * @throws ParseException if the specified source string cannot be parsed.
     */
    public static Deadline parse(final String source) throws ParseException {
        if (source == null) {
            return null;
        }
        final Instant instant = Instant.from(DATE_TIME_FORMATTER.parse(source));
        return fromUnixMilliseconds(instant.toEpochMilli());
    }

    private volatile boolean frozen;

    private volatile long lastCheck;

    /*
     * Internal representation is a UNIX time.
     */
    private final long value;

    /**
     * Constructs a new instance with the given UNIX time in milliseconds.
     *
     * @param deadlineMillis UNIX time in milliseconds.
     */
    private Deadline(final long deadlineMillis) {
        super();
        this.value = deadlineMillis;
        setLastCheck();
    }

    @Override
    public boolean equals(final Object obj) {
        // Only take into account the deadline value.
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Deadline other = (Deadline) obj;
        return value == other.value;
    }

    @Override
    public int hashCode() {
        // Only take into account the deadline value.
        return Long.hashCode(value);
    }

    /**
     * Formats this deadline.
     *
     * @param overdueTimeUnit the time unit to show how much over the deadline we are.
     * @return a formatted string.
     */
    public String format(final TimeUnit overdueTimeUnit) {
        if (value == MAX_VALUE.value) {
            return "No deadline (infinite)";
        }
        return String.format("Deadline: %s, %s overdue", formatTarget(), TimeValue.of(remaining(), overdueTimeUnit));
    }

    /**
     * Formats the deadline value as a string in the format {@value #DATE_FORMAT}.
     *
     * @return a formatted string in the format {@value #DATE_FORMAT}.
     */
    public String formatTarget() {
        if (value == MAX_VALUE.value) {
            return "(infinite)";
        }
        return DATE_TIME_FORMATTER.format(Instant.ofEpochMilli(value).atOffset(ZoneOffset.UTC));
    }

    public Deadline freeze() {
        frozen = true;
        return this;
    }

    /**
     * Package private for testing.
     *
     * @return the last time we checked the current time.
     */
    long getLastCheck() {
        return lastCheck;
    }

    /**
     * Gets the UNIX time deadline value.
     *
     * @return the UNIX time deadline value.
     */
    public long getValue() {
        return value;
    }

    /**
     * Returns whether this deadline occurs before the given time in milliseconds.
     *
     * @param millis the time to compare.
     * @return whether this deadline occurs before the given time in milliseconds.
     */
    public boolean isBefore(final long millis) {
        return value < millis;
    }

    /**
     * Returns whether the deadline has expired.
     *
     * @return whether the deadline has expired.
     */
    public boolean isExpired() {
        setLastCheck();
        return value < this.lastCheck;
    }

    /**
     * Returns whether this deadline is the maximum deadline.
     *
     * @return whether this deadline is the maximum deadline.
     */
    public boolean isMax() {
        return value == INTERNAL_MAX_VALUE;
    }

    /**
     * Returns whether this deadline is the minimum deadline.
     *
     * @return whether this deadline is the minimum deadline.
     */
    public boolean isMin() {
        return value == INTERNAL_MIN_VALUE;
    }

    /**
     * Returns whether this deadline has not expired.
     *
     * @return whether this deadline has not expired.
     */
    public boolean isNotExpired() {
        setLastCheck();
        return value >= this.lastCheck;
    }

    /**
     * Returns the smaller of this and another {@code Deadline}.
     *
     * @param other another deadline.
     * @return the smaller of {@code this} and {@code other}.
     */
    public Deadline min(final Deadline other) {
        return value <= other.value ? this : other;
    }

    /**
     * Returns the difference in milliseconds between the deadline and now.
     *
     * @return the different in milliseconds between the deadline and now.
     */
    public long remaining() {
        setLastCheck();
        return value - lastCheck;
    }

    /**
     * Returns the difference as a TimeValue between the deadline and now.
     *
     * @return Returns the different as a TimeValue between the deadline and now.
     */
    public TimeValue remainingTimeValue() {
        return TimeValue.of(remaining(), TimeUnit.MILLISECONDS);
    }

    private void setLastCheck() {
        if (!frozen) {
            this.lastCheck = System.currentTimeMillis();
        }
    }

    @Override
    public String toString() {
        return formatTarget();
    }

}
