/*
 * 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.kafka.message;

import java.util.Objects;

/**
 * A version range.
 *
 * A range consists of two 16-bit numbers: the lowest version which is accepted, and the highest.
 * Ranges are inclusive, meaning that both the lowest and the highest version are valid versions.
 * The only exception to this is the NONE range, which contains no versions at all.
 *
 * Version ranges can be represented as strings.
 *
 * A single supported version V is represented as "V".
 * A bounded range from A to B is represented as "A-B".
 * All versions greater than A is represented as "A+".
 * The NONE range is represented as an the string "none".
 */
public final class Versions {
    private final short lowest;
    private final short highest;

    public static Versions parse(String input, Versions defaultVersions) {
        if (input == null) {
            return defaultVersions;
        }
        String trimmedInput = input.trim();
        if (trimmedInput.length() == 0) {
            return defaultVersions;
        }
        if (trimmedInput.equals(NONE_STRING)) {
            return NONE;
        }
        if (trimmedInput.endsWith("+")) {
            return new Versions(Short.parseShort(
            trimmedInput.substring(0, trimmedInput.length() - 1)),
                Short.MAX_VALUE);
        } else {
            int dashIndex = trimmedInput.indexOf("-");
            if (dashIndex < 0) {
                short version = Short.parseShort(trimmedInput);
                return new Versions(version, version);
            }
            return new Versions(
                Short.parseShort(trimmedInput.substring(0, dashIndex)),
                Short.parseShort(trimmedInput.substring(dashIndex + 1)));
        }
    }

    public static final Versions ALL = new Versions((short) 0, Short.MAX_VALUE);

    public static final Versions NONE = new Versions();

    public static final String NONE_STRING = "none";

    private Versions() {
        this.lowest = 0;
        this.highest = -1;
    }

    public Versions(short lowest, short highest) {
        if ((lowest < 0) || (highest < 0)) {
            throw new RuntimeException("Invalid version range " +
                lowest + " to " + highest);
        }
        this.lowest = lowest;
        this.highest = highest;
    }

    public short lowest() {
        return lowest;
    }

    public short highest() {
        return highest;
    }

    public boolean empty() {
        return lowest > highest;
    }

    @Override
    public String toString() {
        if (empty()) {
            return NONE_STRING;
        } else if (lowest == highest) {
            return String.valueOf(lowest);
        } else if (highest == Short.MAX_VALUE) {
            return String.format("%d+", lowest);
        } else {
            return String.format("%d-%d", lowest, highest);
        }
    }

    /**
     * Return the intersection of two version ranges.
     *
     * @param other     The other version range.
     * @return          A new version range.
     */
    public Versions intersect(Versions other) {
        short newLowest = lowest > other.lowest ? lowest : other.lowest;
        short newHighest = highest < other.highest ? highest : other.highest;
        if (newLowest > newHighest) {
            return Versions.NONE;
        }
        return new Versions(newLowest, newHighest);
    }

    /**
     * Return a new version range that trims some versions from this range, if possible.
     * We can't trim any versions if the resulting range would be disjoint.
     *
     * Some examples:
     * 1-4.trim(1-2) = 3-4
     * 3+.trim(4+) = 3
     * 4+.trim(3+) = none
     * 1-5.trim(2-4) = null
     *
     * @param other                 The other version range.
     * @return                      A new version range.
     */
    public Versions subtract(Versions other) {
        if (other.lowest() <= lowest) {
            if (other.highest >= highest) {
                // Case 1: other is a superset of this.  Trim everything.
                return Versions.NONE;
            } else if (other.highest < lowest) {
                // Case 2: other is a disjoint version range that is lower than this.  Trim nothing.
                return this;
            } else {
                // Case 3: trim some values from the beginning of this range.
                //
                // Note: it is safe to assume that other.highest() + 1 will not overflow.
                // The reason is because if other.highest() were Short.MAX_VALUE,
                // other.highest() < highest could not be true.
                return new Versions((short) (other.highest() + 1), highest);
            }
        } else if (other.highest >= highest) {
            int newHighest = other.lowest - 1;
            if (newHighest < 0) {
                // Case 4: other was NONE.  Trim nothing.
                return this;
            } else if (newHighest < highest) {
                // Case 5: trim some values from the end of this range.
                return new Versions(lowest, (short) newHighest);
            } else {
                // Case 6: other is a disjoint range that is higher than this.  Trim nothing.
                return this;
            }
        } else {
            // Case 7: the difference between this and other would be two ranges, not one.
            return null;
        }
    }

    public boolean contains(short version) {
        return version >= lowest && version <= highest;
    }

    public boolean contains(Versions other) {
        if (other.empty()) {
            return true;
        }
        return !((lowest > other.lowest) || (highest < other.highest));
    }

    @Override
    public int hashCode() {
        return Objects.hash(lowest, highest);
    }

    @Override
    public boolean equals(Object other) {
        if (!(other instanceof Versions)) {
            return false;
        }
        Versions otherVersions = (Versions) other;
        return lowest == otherVersions.lowest &&
               highest == otherVersions.highest;
    }
}
