/*
 * 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 com.datastax.driver.core;

import com.datastax.driver.core.utils.MoreObjects;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * A version number in the form X.Y.Z with optional pre-release labels and build metadata.
 *
 * <p>Version numbers compare the usual way, the major number (X) is compared first, then the minor
 * one (Y) and then the patch level one (Z). Lastly, versions with pre-release sorts before the
 * versions that don't have one, and labels are sorted alphabetically if necessary. Build metadata
 * are ignored for sorting versions. The versions supported loosely correspond to what
 * http://semver.org/ defines though it does not adhere to it strictly.
 */
public class VersionNumber implements Comparable<VersionNumber> {

  private static final String VERSION_REGEXP =
      "(\\d+)\\.(\\d+)(\\.\\d+)?(\\.\\d+)?([~\\-]\\w[.\\w]*(?:\\-\\w[.\\w]*)*)?(\\+[.\\w]+)?";
  private static final Pattern pattern = Pattern.compile(VERSION_REGEXP);

  private final int major;
  private final int minor;
  private final int patch;
  private final int dsePatch;

  private final String[] preReleases;
  private final String build;

  private VersionNumber(
      int major, int minor, int patch, int dsePatch, String[] preReleases, String build) {
    this.major = major;
    this.minor = minor;
    this.patch = patch;
    this.dsePatch = dsePatch;
    this.preReleases = preReleases;
    this.build = build;
  }

  /**
   * Parse a version from a string.
   *
   * <p>The version string should have primarily the form X.Y.Z to which can be appended one or more
   * pre-release label after dashes (2.0.1-beta1, 2.1.4-rc1-SNAPSHOT) and an optional build label
   * (2.1.0-beta1+a20ba.sha). Out of convenience, the "patch" version number, Z, can be omitted, in
   * which case it is assumed to be 0.
   *
   * @param version the string to parse
   * @return the parsed version number.
   * @throws IllegalArgumentException if the provided string does not represent a valid version.
   */
  public static VersionNumber parse(String version) {
    if (version == null) return null;

    Matcher matcher = pattern.matcher(version);
    if (!matcher.matches())
      throw new IllegalArgumentException("Invalid version number: " + version);

    try {
      int major = Integer.parseInt(matcher.group(1));
      int minor = Integer.parseInt(matcher.group(2));

      String pa = matcher.group(3);
      int patch =
          pa == null || pa.isEmpty()
              ? 0
              : Integer.parseInt(
                  pa.substring(1)); // dropping the initial '.' since it's included this time

      String dse = matcher.group(4);
      int dsePatch =
          dse == null || dse.isEmpty()
              ? -1
              : Integer.parseInt(
                  dse.substring(1)); // dropping the initial '.' since it's included this time

      String pr = matcher.group(5);
      String[] preReleases =
          pr == null || pr.isEmpty()
              ? null
              : pr.substring(1)
                  .split("\\-"); // drop initial '-' or '~' then split on the remaining ones

      String bl = matcher.group(6);
      String build = bl == null || bl.isEmpty() ? null : bl.substring(1); // drop the initial '+'

      return new VersionNumber(major, minor, patch, dsePatch, preReleases, build);
    } catch (NumberFormatException e) {
      throw new IllegalArgumentException("Invalid version number: " + version);
    }
  }

  /**
   * The major version number.
   *
   * @return the major version number, i.e. X in X.Y.Z.
   */
  public int getMajor() {
    return major;
  }

  /**
   * The minor version number.
   *
   * @return the minor version number, i.e. Y in X.Y.Z.
   */
  public int getMinor() {
    return minor;
  }

  /**
   * The patch version number.
   *
   * @return the patch version number, i.e. Z in X.Y.Z.
   */
  public int getPatch() {
    return patch;
  }

  /**
   * The DSE patch version number (will only be present for version of Cassandra in DSE).
   *
   * <p>DataStax Entreprise (DSE) adds a fourth number to the version number to track potential hot
   * fixes and/or DSE specific patches that may have been applied to the Cassandra version. In that
   * case, this method return that fourth number.
   *
   * @return the DSE patch version number, i.e. D in X.Y.Z.D, or -1 if the version number is not
   *     from DSE.
   */
  public int getDSEPatch() {
    return dsePatch;
  }

  /**
   * The pre-release labels if relevant, i.e. label1 and label2 in X.Y.Z-label1-lable2.
   *
   * @return the pre-releases labels. The return list will be {@code null} if the version number
   *     doesn't have one.
   */
  public List<String> getPreReleaseLabels() {
    return preReleases == null ? null : Collections.unmodifiableList(Arrays.asList(preReleases));
  }

  /**
   * The build label if there is one.
   *
   * @return the build label or {@code null} if the version number doesn't have one.
   */
  public String getBuildLabel() {
    return build;
  }

  /**
   * The next stable version, i.e. the version stripped of its pre-release labels and build
   * metadata.
   *
   * <p>This is mostly used during our development stage, where we test the driver against
   * pre-release versions of Cassandra like 2.1.0-rc7-SNAPSHOT, but need to compare to the stable
   * version 2.1.0 when testing for native protocol compatibility, etc.
   *
   * @return the next stable version.
   */
  public VersionNumber nextStable() {
    return new VersionNumber(major, minor, patch, dsePatch, null, null);
  }

  @Override
  public int compareTo(VersionNumber other) {
    if (major < other.major) return -1;
    if (major > other.major) return 1;

    if (minor < other.minor) return -1;
    if (minor > other.minor) return 1;

    if (patch < other.patch) return -1;
    if (patch > other.patch) return 1;

    if (dsePatch < 0) {
      if (other.dsePatch >= 0) return -1;
    } else {
      if (other.dsePatch < 0) return 1;

      // Both are >= 0
      if (dsePatch < other.dsePatch) return -1;
      if (dsePatch > other.dsePatch) return 1;
    }

    if (preReleases == null) return other.preReleases == null ? 0 : 1;
    if (other.preReleases == null) return -1;

    for (int i = 0; i < Math.min(preReleases.length, other.preReleases.length); i++) {
      int cmp = preReleases[i].compareTo(other.preReleases[i]);
      if (cmp != 0) return cmp;
    }

    return preReleases.length == other.preReleases.length
        ? 0
        : (preReleases.length < other.preReleases.length ? -1 : 1);
  }

  @Override
  public boolean equals(Object other) {
    if (other == this) return true;
    if (!(other instanceof VersionNumber)) return false;
    VersionNumber that = (VersionNumber) other;
    return this.major == that.major
        && this.minor == that.minor
        && this.patch == that.patch
        && this.dsePatch == that.dsePatch
        && (this.preReleases == null
            ? that.preReleases == null
            : Arrays.equals(this.preReleases, that.preReleases))
        && MoreObjects.equal(this.build, that.build);
  }

  @Override
  public int hashCode() {
    return MoreObjects.hashCode(major, minor, patch, dsePatch, Arrays.hashCode(preReleases), build);
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append(major).append('.').append(minor).append('.').append(patch);
    if (dsePatch >= 0) sb.append('.').append(dsePatch);
    if (preReleases != null) {
      for (String preRelease : preReleases) sb.append('-').append(preRelease);
    }
    if (build != null) sb.append('+').append(build);
    return sb.toString();
  }
}
