/*
 * ====================================================================
 * 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.client5.http.utils;

import java.time.DateTimeException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.MessageHeaders;
import org.apache.hc.core5.http.message.MessageSupport;
import org.apache.hc.core5.util.Args;
import org.apache.hc.core5.util.Tokenizer;

/**
 * A utility class for parsing and formatting HTTP dates as used in cookies and
 * other headers.
 *
 * @since 4.3
 */
public final class DateUtils {

    /**
     * @deprecated use {@link #INTERNET_MESSAGE_FORMAT}
     */
    @Deprecated
    public static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
    public static final String INTERNET_MESSAGE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";

    /**
     * Date formatter used to parse HTTP date headers in the Internet Message Format
     * specified by the HTTP protocol.
     *
     * @since 5.2
     */
    public static final DateTimeFormatter FORMATTER_RFC1123 = new DateTimeFormatterBuilder()
            .parseLenient()
            .parseCaseInsensitive()
            .appendPattern(INTERNET_MESSAGE_FORMAT)
            .toFormatter(Locale.ENGLISH);

    /**
     * Date format pattern used to parse HTTP date headers in RFC 1036 format.
     */
    public static final String PATTERN_RFC1036 = "EEE, dd-MMM-yy HH:mm:ss zzz";

    /**
     * Date formatter used to parse HTTP date headers in RFC 1036 format.
     *
     * @since 5.2
     */
    public static final DateTimeFormatter FORMATTER_RFC1036 = new DateTimeFormatterBuilder()
            .parseLenient()
            .parseCaseInsensitive()
            .appendPattern(PATTERN_RFC1036)
            .toFormatter(Locale.ENGLISH);

    /**
     * Date format pattern used to parse HTTP date headers in ANSI C
     * {@code asctime()} format.
     */
    public static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy";

    /**
     * Date formatter used to parse HTTP date headers in in ANSI C {@code asctime()} format.
     *
     * @since 5.2
     */
    public static final DateTimeFormatter FORMATTER_ASCTIME = new DateTimeFormatterBuilder()
            .parseLenient()
            .parseCaseInsensitive()
            .appendPattern(PATTERN_ASCTIME)
            .toFormatter(Locale.ENGLISH);

    /**
     * Standard date formatters: {@link #FORMATTER_RFC1123}, {@link #FORMATTER_RFC1036}, {@link #FORMATTER_ASCTIME}.
     *
     * @since 5.2
     */
    public static final DateTimeFormatter[] STANDARD_PATTERNS = new DateTimeFormatter[] {
            FORMATTER_RFC1123,
            FORMATTER_RFC1036,
            FORMATTER_ASCTIME
    };

    static final ZoneId GMT_ID = ZoneId.of("GMT");

    /**
     * @since 5.2
     */
    public static Date toDate(final Instant instant) {
        return instant != null ? new Date(instant.toEpochMilli()) : null;
    }

    /**
     * @since 5.2
     */
    public static Instant toInstant(final Date date) {
        return date != null ? Instant.ofEpochMilli(date.getTime()) : null;
    }

    /**
     * @since 5.2
     */
    public static LocalDateTime toUTC(final Instant instant) {
        return instant != null ? instant.atZone(ZoneOffset.UTC).toLocalDateTime() : null;
    }

    /**
     * @since 5.2
     */
    public static LocalDateTime toUTC(final Date date) {
        return toUTC(toInstant(date));
    }

    /**
     * Parses the date value using the given date/time formats.
     * <p>This method can handle strings without time-zone information by failing gracefully, in which case
     * it returns {@code null}.</p>
     *
     * @param dateValue the instant value to parse
     * @param dateFormatters the date/time formats to use
     *
     * @return the parsed instant or null if input could not be parsed
     *
     * @since 5.2
     */
    public static Instant parseDate(final String dateValue, final DateTimeFormatter... dateFormatters) {
        return parseDate((CharSequence) dateValue, dateFormatters);
    }

    /**
     * Parses the date value using the given date/time formats.
     * <p>This method can handle strings without time-zone information by failing gracefully, in which case
     * it returns {@code null}.</p>
     *
     * @param dateValue the instant value to parse
     * @param dateFormatters the date/time formats to use
     *
     * @return the parsed instant or null if input could not be parsed
     *
     * @since 5.7
     */
    public static Instant parseDate(final CharSequence dateValue, final DateTimeFormatter... dateFormatters) {
        Args.notNull(dateValue, "Date value");
        CharSequence v = dateValue;
        // trim single quotes around date if present
        // see issue #5279
        if (v.length() > 1 && v.charAt(0) == '\'' && v.charAt(v.length() - 1) == '\'') {
            v = v.subSequence(1, v.length() - 1);
        }

        for (final DateTimeFormatter dateFormatter : dateFormatters) {
            try {
                return Instant.from(dateFormatter.parse(v));
            } catch (final DateTimeException ignore) {
            }
        }
        return null;
    }

    /**
     * Parses the instant value using the standard date/time formats ({@link #PATTERN_RFC1123},
     * {@link #PATTERN_RFC1036}, {@link #PATTERN_ASCTIME}).
     *
     * @param dateValue the instant value to parse
     *
     * @return the parsed instant or null if input could not be parsed
     *
     * @since 5.2
     */
    public static Instant parseStandardDate(final String dateValue) {
        return parseDate(dateValue, STANDARD_PATTERNS);
    }

    /**
     * Parses the date value using the given date/time formats.
     * <p>This method can handle strings without time-zone information by failing gracefully, in which case
     * it returns {@code null}.</p>
     *
     * @param header the header with an instant value to parse
     * @param dateFormatters the date/time formats to use
     *
     * @return the parsed instant or null if input could not be parsed
     *
     * @since 5.7
     */
    public static Instant parseDate(final Header header, final DateTimeFormatter... dateFormatters) {
        Args.notNull(header, "Header");
        return MessageSupport.parserHeaderValue(header, (cs, cursor) -> {
            Tokenizer.INSTANCE.skipWhiteSpace(cs, cursor);
            return parseDate(cs.subSequence(cursor.getPos(), cursor.getUpperBound()), dateFormatters);
        });
    }

    /**
     * Parses an instant value from a header with the given name.
     *
     * @param headers message headers
     * @param headerName header name
     *
     * @return the parsed instant or null if input could not be parsed
     *
     * @since 5.2
     */
    public static Instant parseStandardDate(final MessageHeaders headers, final String headerName) {
        if (headers == null) {
            return null;
        }
        final Header header = headers.getFirstHeader(headerName);
        if (header == null) {
            return null;
        }
        return parseDate(header, STANDARD_PATTERNS);
    }

    /**
     * Formats the given instant according to the RFC 1123 pattern.
     *
     * @param instant Instant to format.
     * @return An RFC 1123 formatted instant string.
     *
     * @see #PATTERN_RFC1123
     *
     * @since 5.2
     */
    public static String formatStandardDate(final Instant instant) {
        return formatDate(instant, FORMATTER_RFC1123);
    }

    /**
     * Formats the given date according to the specified pattern.
     *
     * @param instant Instant to format.
     * @param dateTimeFormatter The pattern to use for formatting the instant.
     * @return A formatted instant string.
     *
     * @throws IllegalArgumentException If the given date pattern is invalid.
     *
     * @since 5.2
     */
    public static String formatDate(final Instant instant, final DateTimeFormatter dateTimeFormatter) {
        Args.notNull(instant, "Instant");
        Args.notNull(dateTimeFormatter, "DateTimeFormatter");
        return dateTimeFormatter.format(instant.atZone(GMT_ID));
    }

    /**
     * @deprecated This attribute is no longer supported as a part of the public API.
     */
    @Deprecated
    public static final TimeZone GMT = TimeZone.getTimeZone("GMT");

    /**
     * Parses a date value.  The formats used for parsing the date value are retrieved from
     * the default http params.
     *
     * @param dateValue the date value to parse
     *
     * @return the parsed date or null if input could not be parsed
     *
     * @deprecated Use {@link #parseStandardDate(String)}
     */
    @Deprecated
    public static Date parseDate(final String dateValue) {
        return parseDate(dateValue, null, null);
    }

    /**
     * Parses a date value from a header with the given name.
     *
     * @param headers message headers
     * @param headerName header name
     *
     * @return the parsed date or null if input could not be parsed
     *
     * @since 5.0
     *
     * @deprecated Use {@link #parseStandardDate(MessageHeaders, String)}
     */
    @Deprecated
    public static Date parseDate(final MessageHeaders headers, final String headerName) {
        return toDate(parseStandardDate(headers, headerName));
    }

    /**
     * Tests if the first message is after (newer) than second one
     * using the given message header for comparison.
     *
     * @param message1 the first message
     * @param message2 the second message
     * @param headerName header name
     *
     * @return {@code true} if both messages contain a header with the given name
     *  and the value of the header from the first message is newer that of
     *  the second message.
     *
     * @since 5.0
     *
     * @deprecated This method is no longer supported as a part of the public API.
     */
    @Deprecated
    public static boolean isAfter(
            final MessageHeaders message1,
            final MessageHeaders message2,
            final String headerName) {
        if (message1 != null && message2 != null) {
            final Header dateHeader1 = message1.getFirstHeader(headerName);
            if (dateHeader1 != null) {
                final Header dateHeader2 = message2.getFirstHeader(headerName);
                if (dateHeader2 != null) {
                    final Date date1 = parseDate(dateHeader1.getValue());
                    if (date1 != null) {
                        final Date date2 = parseDate(dateHeader2.getValue());
                        if (date2 != null) {
                            return date1.after(date2);
                        }
                    }
                }
            }
        }
        return false;
    }

    /**
     * Tests if the first message is before (older) than the second one
     * using the given message header for comparison.
     *
     * @param message1 the first message
     * @param message2 the second message
     * @param headerName header name
     *
     * @return {@code true} if both messages contain a header with the given name
     *  and the value of the header from the first message is older that of
     *  the second message.
     *
     * @since 5.0
     *
     * @deprecated This method is no longer supported as a part of the public API.
     */
    @Deprecated
    public static boolean isBefore(
            final MessageHeaders message1,
            final MessageHeaders message2,
            final String headerName) {
        if (message1 != null && message2 != null) {
            final Header dateHeader1 = message1.getFirstHeader(headerName);
            if (dateHeader1 != null) {
                final Header dateHeader2 = message2.getFirstHeader(headerName);
                if (dateHeader2 != null) {
                    final Date date1 = parseDate(dateHeader1.getValue());
                    if (date1 != null) {
                        final Date date2 = parseDate(dateHeader2.getValue());
                        if (date2 != null) {
                            return date1.before(date2);
                        }
                    }
                }
            }
        }
        return false;
    }

    /**
     * Parses the date value using the given date/time formats.
     *
     * @param dateValue the date value to parse
     * @param dateFormats the date/time formats to use
     *
     * @return the parsed date or null if input could not be parsed
     *
     * @deprecated Use {@link #parseDate(String, DateTimeFormatter...)}
     */
    @Deprecated
    public static Date parseDate(final String dateValue, final String[] dateFormats) {
        return parseDate(dateValue, dateFormats, null);
    }

    /**
     * Parses the date value using the given date/time formats.
     *
     * @param dateValue the date value to parse
     * @param dateFormats the date/time formats to use
     * @param startDate During parsing, two digit years will be placed in the range
     * {@code startDate} to {@code startDate + 100 years}. This value may
     * be {@code null}. When {@code null} is given as a parameter, year
     * {@code 2000} will be used.
     *
     * @return the parsed date or null if input could not be parsed
     *
     * @deprecated Use {@link #parseDate(String, DateTimeFormatter...)}
     */
    @Deprecated
    public static Date parseDate(
            final String dateValue,
            final String[] dateFormats,
            final Date startDate) {
        final DateTimeFormatter[] dateTimeFormatters;
        if (dateFormats != null) {
            dateTimeFormatters = new DateTimeFormatter[dateFormats.length];
            for (int i = 0; i < dateFormats.length; i++) {
                dateTimeFormatters[i] = new DateTimeFormatterBuilder()
                        .parseLenient()
                        .parseCaseInsensitive()
                        .appendPattern(dateFormats[i])
                        .toFormatter();
            }
        } else {
            dateTimeFormatters = STANDARD_PATTERNS;
        }
        return toDate(parseDate(dateValue, dateTimeFormatters));
    }

    /**
     * Formats the given date according to the RFC 1123 pattern.
     *
     * @param date The date to format.
     * @return An RFC 1123 formatted date string.
     *
     * @see #PATTERN_RFC1123
     *
     * @deprecated Use {@link #formatStandardDate(Instant)}
     */
    @Deprecated
    public static String formatDate(final Date date) {
        return formatStandardDate(toInstant(date));
    }

    /**
     * Formats the given date according to the specified pattern.
     *
     * @param date The date to format.
     * @param pattern The pattern to use for formatting the date.
     * @return A formatted date string.
     *
     * @throws IllegalArgumentException If the given date pattern is invalid.
     *
     * @deprecated Use {@link #formatDate(Instant, DateTimeFormatter)}
     */
    @Deprecated
    public static String formatDate(final Date date, final String pattern) {
        Args.notNull(date, "Date");
        Args.notNull(pattern, "Pattern");
        return DateTimeFormatter.ofPattern(pattern).format(toInstant(date).atZone(GMT_ID));
    }

    /**
     * Clears thread-local variable containing {@link java.text.DateFormat} cache.
     *
     * @since 4.3
     *
     * @deprecated Noop method. Do not use.
     */
    @Deprecated
    public static void clearThreadLocal() {
    }

    /** This class should not be instantiated. */
    private DateUtils() {
    }

}
