/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed 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 com.android.i18n.timezone;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Version information associated with the set of time zone data on a device.
 *
 * <p>Time Zone Data Sets have a major ({@link #getFormatMajorVersion()}) and minor
 * ({@link #currentFormatMinorVersion()}) version number:
 * <ul>
 *   <li>Major version numbers are mutually incompatible. e.g. v2 is not compatible with a v1 or a
 *   v3 device.</li>
 *   <li>Minor version numbers are backwards compatible. e.g. a v2.2 data set will work
 *   on a v2.1 device but not a v2.3 device. The minor version is reset to 1 when the major version
 *   is incremented.</li>
 * </ul>
 *
 * <p>Data sets contain time zone rules and other data associated wtih a tzdb release
 * ({@link #getRulesVersion()}) and an additional Android-specific revision number
 * ({@link #getRevision()}).
 *
 * <p>See platform/system/timezone/README.android for more information.
 * @hide
 */
@libcore.api.CorePlatformApi
public final class TzDataSetVersion {

    // Remove from CorePlatformApi when all users in platform code are removed. http://b/123398797
    /**
     * The name typically given to the {@link TzDataSetVersion} file. See
     * {@link TzDataSetVersion#readFromFile(File)}.
     */
    @libcore.api.CorePlatformApi
    public static final String DEFAULT_FILE_NAME = "tz_version";

    /**
     * The major tz data format version supported by this device.
     * Increment this for non-backwards compatible changes to the tz data format. Reset the minor
     * version to 1 when doing so.
     */
    // @VisibleForTesting : Keep this inline-able: it is used from CTS tests.
    public static final int CURRENT_FORMAT_MAJOR_VERSION = 8; // Android V

    /**
     * Returns the major tz data format version supported by this device.
     */
    @libcore.api.CorePlatformApi
    public static int currentFormatMajorVersion() {
        return CURRENT_FORMAT_MAJOR_VERSION;
    }

    /**
     * The minor tz data format version supported by this device. Increment this for
     * backwards-compatible changes to the tz data format.
     */
    // @VisibleForTesting : Keep this inline-able: it is used from CTS tests.
    public static final int CURRENT_FORMAT_MINOR_VERSION = 1;

    /**
     * Returns the minor tz data format version supported by this device.
     */
    @libcore.api.CorePlatformApi
    public static int currentFormatMinorVersion() {
        return CURRENT_FORMAT_MINOR_VERSION;
    }

    /** The full major + minor tz data format version for this device. */
    private static final String FULL_CURRENT_FORMAT_VERSION_STRING =
            toFormatVersionString(CURRENT_FORMAT_MAJOR_VERSION, CURRENT_FORMAT_MINOR_VERSION);

    private static final int FORMAT_VERSION_STRING_LENGTH =
            FULL_CURRENT_FORMAT_VERSION_STRING.length();
    private static final Pattern FORMAT_VERSION_PATTERN = Pattern.compile("(\\d{3})\\.(\\d{3})");

    /** A pattern that matches the IANA rules value of a rules update. e.g. "2016g" */
    private static final Pattern RULES_VERSION_PATTERN = Pattern.compile("(\\d{4}\\w)");

    private static final int RULES_VERSION_LENGTH = 5;

    /** A pattern that matches the revision of a rules update. e.g. "001" */
    private static final Pattern REVISION_PATTERN = Pattern.compile("(\\d{3})");

    private static final int REVISION_LENGTH = 3;

    /**
     * The length of a well-formed tz data set version file:
     * {Format version}|{Rule version}|{Revision}
     */
    private static final int TZ_DATA_VERSION_FILE_LENGTH = FORMAT_VERSION_STRING_LENGTH + 1
            + RULES_VERSION_LENGTH
            + 1 + REVISION_LENGTH;

    private static final Pattern TZ_DATA_VERSION_FILE_PATTERN = Pattern.compile(
            FORMAT_VERSION_PATTERN.pattern() + "\\|"
                    + RULES_VERSION_PATTERN.pattern() + "\\|"
                    + REVISION_PATTERN.pattern()
                    + ".*" /* ignore trailing */);

    private final int formatMajorVersion;
    private final int formatMinorVersion;
    private final String rulesVersion;
    private final int revision;

    @libcore.api.CorePlatformApi
    public TzDataSetVersion(int formatMajorVersion, int formatMinorVersion, String rulesVersion,
            int revision) throws TzDataSetException {
        this.formatMajorVersion = validate3DigitVersion(formatMajorVersion);
        this.formatMinorVersion = validate3DigitVersion(formatMinorVersion);
        if (!RULES_VERSION_PATTERN.matcher(rulesVersion).matches()) {
            throw new TzDataSetException("Invalid rulesVersion: " + rulesVersion);
        }
        this.rulesVersion = rulesVersion;
        this.revision = validate3DigitVersion(revision);
    }

    // VisibleForTesting
    public static TzDataSetVersion fromBytes(byte[] bytes) throws TzDataSetException {
        String tzDataVersion = new String(bytes, StandardCharsets.US_ASCII);
        try {
            Matcher matcher = TZ_DATA_VERSION_FILE_PATTERN.matcher(tzDataVersion);
            if (!matcher.matches()) {
                throw new TzDataSetException(
                        "Invalid tz data version string: \"" + tzDataVersion + "\"");
            }
            String formatMajorVersion = matcher.group(1);
            String formatMinorVersion = matcher.group(2);
            String rulesVersion = matcher.group(3);
            String revision = matcher.group(4);
            return new TzDataSetVersion(
                    from3DigitVersionString(formatMajorVersion),
                    from3DigitVersionString(formatMinorVersion),
                    rulesVersion,
                    from3DigitVersionString(revision));
        } catch (IndexOutOfBoundsException e) {
            // The use of the regexp above should make this impossible.
            throw new TzDataSetException(
                    "tz data version string too short: \"" + tzDataVersion + "\"");
        }
    }

    // Remove from CorePlatformApi when all users in platform code are removed. http://b/123398797
    @libcore.api.CorePlatformApi
    public static TzDataSetVersion readFromFile(File file) throws IOException, TzDataSetException {
        byte[] versionBytes = readBytes(file, TzDataSetVersion.TZ_DATA_VERSION_FILE_LENGTH);
        return fromBytes(versionBytes);
    }

    /** Returns the major version number. See {@link TzDataSetVersion}. */
    @libcore.api.CorePlatformApi
    public int getFormatMajorVersion() {
        return formatMajorVersion;
    }

    /** Returns the minor version number. See {@link TzDataSetVersion}. */
    @libcore.api.CorePlatformApi
    public int getFormatMinorVersion() {
        return formatMinorVersion;
    }

    /** Returns the tzdb version string. See {@link TzDataSetVersion}. */
    @libcore.api.CorePlatformApi
    public String getRulesVersion() {
        return rulesVersion;
    }

    // Remove from CorePlatformApi when all users in platform code are removed. http://b/123398797
    /** Returns the Android revision. See {@link TzDataSetVersion}. */
    @libcore.api.CorePlatformApi
    public int getRevision() {
        return revision;
    }

    // Remove from CorePlatformApi when all users in platform code are removed. http://b/123398797
    @libcore.api.CorePlatformApi
    public byte[] toBytes() {
        return toBytes(formatMajorVersion, formatMinorVersion, rulesVersion, revision);
    }

    private static byte[] toBytes(
            int majorFormatVersion, int minorFormatVerison, String rulesVersion, int revision) {
        return (toFormatVersionString(majorFormatVersion, minorFormatVerison)
                + "|" + rulesVersion + "|" + to3DigitVersionString(revision))
                .getBytes(StandardCharsets.US_ASCII);
    }

    @libcore.api.CorePlatformApi
    public static boolean isCompatibleWithThisDevice(TzDataSetVersion tzDataVersion) {
        return (CURRENT_FORMAT_MAJOR_VERSION == tzDataVersion.formatMajorVersion)
                && (CURRENT_FORMAT_MINOR_VERSION <= tzDataVersion.formatMinorVersion);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        TzDataSetVersion that = (TzDataSetVersion) o;
        if (formatMajorVersion != that.formatMajorVersion) {
            return false;
        }
        if (formatMinorVersion != that.formatMinorVersion) {
            return false;
        }
        if (revision != that.revision) {
            return false;
        }
        return rulesVersion.equals(that.rulesVersion);
    }

    @Override
    public int hashCode() {
        int result = formatMajorVersion;
        result = 31 * result + formatMinorVersion;
        result = 31 * result + rulesVersion.hashCode();
        result = 31 * result + revision;
        return result;
    }

    @Override
    public String toString() {
        return "TzDataSetVersion{" +
                "formatMajorVersion=" + formatMajorVersion +
                ", formatMinorVersion=" + formatMinorVersion +
                ", rulesVersion='" + rulesVersion + '\'' +
                ", revision=" + revision +
                '}';
    }

    /**
     * Returns a version as a zero-padded three-digit String value.
     */
    private static String to3DigitVersionString(int version) {
        try {
            return String.format(Locale.ROOT, "%03d", validate3DigitVersion(version));
        } catch (TzDataSetException e) {
            throw new IllegalArgumentException(e);
        }
    }

    /**
     * Validates and parses a zero-padded three-digit String value.
     */
    private static int from3DigitVersionString(String versionString) throws TzDataSetException {
        final String parseErrorMessage = "versionString must be a zero padded, 3 digit, positive"
                + " decimal integer";
        if (versionString.length() != 3) {
            throw new TzDataSetException(parseErrorMessage);
        }
        try {
            int version = Integer.parseInt(versionString);
            return validate3DigitVersion(version);
        } catch (NumberFormatException e) {
            throw new TzDataSetException(parseErrorMessage, e);
        }
    }

    private static int validate3DigitVersion(int value) throws TzDataSetException {
        // 0 is allowed but is reserved for testing.
        if (value < 0 || value > 999) {
            throw new TzDataSetException("Expected 0 <= value <= 999, was " + value);
        }
        return value;
    }

    private static String toFormatVersionString(int majorFormatVersion, int minorFormatVersion) {
        return to3DigitVersionString(majorFormatVersion)
                + "." + to3DigitVersionString(minorFormatVersion);
    }

    /**
     * Reads up to {@code maxBytes} bytes from the specified file. The returned array can be
     * shorter than {@code maxBytes} if the file is shorter.
     */
    private static byte[] readBytes(File file, int maxBytes) throws IOException {
        if (maxBytes <= 0) {
            throw new IllegalArgumentException("maxBytes ==" + maxBytes);
        }

        try (FileInputStream in = new FileInputStream(file)) {
            byte[] max = new byte[maxBytes];
            int bytesRead = in.read(max, 0, maxBytes);
            byte[] toReturn = new byte[bytesRead];
            System.arraycopy(max, 0, toReturn, 0, bytesRead);
            return toReturn;
        }
    }

    /**
     * A checked exception used in connection with time zone data sets.
     */
    @libcore.api.CorePlatformApi
    public static class TzDataSetException extends Exception {

        @libcore.api.CorePlatformApi
        public TzDataSetException(String message) {
            super(message);
        }

        @libcore.api.CorePlatformApi
        public TzDataSetException(String message, Throwable cause) {
            super(message, cause);
        }
    }
}
