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

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.internal.IgniteVersionUtils;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.jetbrains.annotations.NotNull;

/**
 * Represents node version.
 * <p>
 * Node version can be acquired via {@link org.apache.ignite.cluster.ClusterNode#version()} method.
 * <p>
 * Two versions are compared in the following order: major number,
 * minor number, maintenance number, revision timestamp.
 */
public class IgniteProductVersion implements Comparable<IgniteProductVersion>, Externalizable {
    /** */
    private static final long serialVersionUID = 0L;

    /** Size of the {@link #revHash }*/
    public static final int REV_HASH_SIZE = 20;

    /** Size in bytes of serialized: 3 bytes (maj, min, maintenance version), 8 bytes - timestamp */
    public static final int SIZE_IN_BYTES = 3 + 8 + REV_HASH_SIZE;

    /** Regexp parse pattern. */
    private static final Pattern VER_PATTERN =
        Pattern.compile("(\\d+)\\.(\\d+)\\.(\\d+)([-.]([^0123456789][^-]+)(-SNAPSHOT)?)?(-(\\d+))?(-([\\da-f]+))?");

    /** Major version number. */
    private byte major;

    /** Minor version number. */
    private byte minor;

    /** Maintenance version number. */
    private byte maintenance;

    /** Stage of development. */
    private String stage;

    /** Revision timestamp. */
    private long revTs;

    /** Revision hash. */
    private byte[] revHash;

    /**
     * Empty constructor required by {@link Externalizable}.
     */
    public IgniteProductVersion() {
        // No-op.
    }

    /**
     * @param major Major version number.
     * @param minor Minor version number.
     * @param maintenance Maintenance version number.
     * @param revTs Revision timestamp.
     * @param revHash Revision hash.
     */
    public IgniteProductVersion(byte major, byte minor, byte maintenance, long revTs, byte[] revHash) {
        this(major, minor, maintenance, "", revTs, revHash);
    }

    /**
     * @param major Major version number.
     * @param minor Minor version number.
     * @param maintenance Maintenance version number.
     * @param stage Stage of development.
     * @param revTs Revision timestamp.
     * @param revHash Revision hash.
     */
    public IgniteProductVersion(byte major, byte minor, byte maintenance, String stage, long revTs, byte[] revHash) {
        if (revHash != null && revHash.length != REV_HASH_SIZE) {
            throw new IllegalArgumentException("Invalid length for SHA1 hash (must be "
                + REV_HASH_SIZE + "): " + revHash.length);
        }

        this.major = major;
        this.minor = minor;
        this.maintenance = maintenance;
        this.stage = stage;
        this.revTs = revTs;
        this.revHash = revHash != null ? revHash : new byte[REV_HASH_SIZE];
    }

    /**
     * Gets major version number.
     *
     * @return Major version number.
     */
    public byte major() {
        return major;
    }

    /**
     * Gets minor version number.
     *
     * @return Minor version number.
     */
    public byte minor() {
        return minor;
    }

    /**
     * Gets maintenance version number.
     *
     * @return Maintenance version number.
     */
    public byte maintenance() {
        return maintenance;
    }

    /**
     * @return Stage of development.
     */
    public String stage() {
        return stage;
    }

    /**
     * Gets revision timestamp.
     *
     * @return Revision timestamp.
     */
    public long revisionTimestamp() {
        return revTs;
    }

    /**
     * Gets revision hash.
     *
     * @return Revision hash.
     */
    public byte[] revisionHash() {
        return revHash;
    }

    /**
     * Gets release date.
     *
     * @return Release date.
     */
    public Date releaseDate() {
        return new Date(revTs * 1000);
    }

    /**
     * @param major Major version number.
     * @param minor Minor version number.
     * @param maintenance Maintenance version number.
     * @return {@code True} if this version is greater or equal than the one passed in.
     */
    public boolean greaterThanEqual(int major, int minor, int maintenance) {
        // NOTE: Unknown version is less than any other version.
        if (major == this.major)
            return minor == this.minor ? this.maintenance >= maintenance : this.minor > minor;
        else
            return this.major > major;
    }

    /** {@inheritDoc} */
    @Override public int compareTo(@NotNull IgniteProductVersion o) {
        // NOTE: Unknown version is less than any other version.
        int res = Integer.compare(major, o.major);

        if (res != 0)
            return res;

        res = Integer.compare(minor, o.minor);

        if (res != 0)
            return res;

        res = Integer.compare(maintenance, o.maintenance);

        if (res != 0)
            return res;

        return Long.compare(revTs, o.revTs);
    }

    /**
     * @param o Other version.
     * @return Compare result.
     */
    public int compareToIgnoreTimestamp(@NotNull IgniteProductVersion o) {
        int res = Integer.compare(major, o.major);

        if (res != 0)
            return res;

        res = Integer.compare(minor, o.minor);

        if (res != 0)
            return res;

        return Integer.compare(maintenance, o.maintenance);
    }

    /** {@inheritDoc} */
    @Override public boolean equals(Object o) {
        if (this == o)
            return true;

        if (!(o instanceof IgniteProductVersion))
            return false;

        IgniteProductVersion that = (IgniteProductVersion)o;

        return revTs == that.revTs && maintenance == that.maintenance && minor == that.minor && major == that.major;
    }

    /** {@inheritDoc} */
    @Override public int hashCode() {
        int res = major;

        res = 31 * res + minor;
        res = 31 * res + maintenance;
        res = 31 * res + (int)(revTs ^ (revTs >>> 32));

        return res;
    }

    /** {@inheritDoc} */
    @Override public void writeExternal(ObjectOutput out) throws IOException {
        out.writeByte(major);
        out.writeByte(minor);
        out.writeByte(maintenance);
        out.writeLong(revTs);
        U.writeByteArray(out, revHash);
    }

    /** {@inheritDoc} */
    @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        major = in.readByte();
        minor = in.readByte();
        maintenance = in.readByte();
        revTs = in.readLong();
        revHash = U.readByteArray(in);
    }

    /** {@inheritDoc} */
    @Override public String toString() {
        String revTsStr = IgniteVersionUtils.formatBuildTimeStamp(revTs * 1000);

        String hash = U.byteArray2HexString(revHash).toLowerCase();

        hash = hash.length() > 8 ? hash.substring(0, 8) : hash;

        return major + "." + minor + "." + maintenance + "#" + revTsStr + "-sha1:" + hash;
    }

    /**
     * Tries to parse product version from it's string representation.
     *
     * @param verStr String representation of version.
     * @return Product version.
     */
    public static IgniteProductVersion fromString(String verStr) {
        assert verStr != null;

        if (verStr.endsWith("-DEV") || verStr.endsWith("-n/a")) // Development or built from source ZIP.
            verStr = verStr.substring(0, verStr.length() - 4);

        Matcher match = VER_PATTERN.matcher(verStr);

        if (match.matches()) {
            try {
                byte major = Byte.parseByte(match.group(1));
                byte minor = Byte.parseByte(match.group(2));
                byte maintenance = Byte.parseByte(match.group(3));

                String stage = "";

                if (match.group(4) != null)
                    stage = match.group(4).substring(1);

                long revTs = 0;

                if (match.group(7) != null)
                    revTs = Long.parseLong(match.group(8));

                byte[] revHash = null;

                if (match.group(9) != null)
                    revHash = U.decodeHex(match.group(10).toCharArray());

                return new IgniteProductVersion(major, minor, maintenance, stage, revTs, revHash);
            }
            catch (IllegalStateException | IndexOutOfBoundsException | NumberFormatException | IgniteCheckedException e) {
                throw new IllegalStateException("Failed to parse version: " + verStr, e);
            }
        }
        else
            throw new IllegalStateException("Failed to parse version: " + verStr);
    }
}
