import { isString } from '../utils/lang';

const NUMERIC_IDENTIFIER_REGEX = /^[0-9]+$/;

const METADATA_DELIMITER = '+';
const PRERELEASE_DELIMITER = '-';
const VALUE_DELIMITER = '.';

/**
 * Compares two strings. If both strings are numeric identifiers, they are compared numerically. Otherwise, they are compared lexicographically.
 * This could be implemented using `a.localeCompare(b, undefined, { numeric: true })` but locale options are not broadly supported.
 */
function compareStrings(a: string, b: string): number {
  if (NUMERIC_IDENTIFIER_REGEX.test(a) && NUMERIC_IDENTIFIER_REGEX.test(b)) {
    const result = a.length - b.length;
    if (result !== 0) {
      return result;
    }
  }
  return a < b ? -1 : a > b ? 1 : 0;
}

// Sanitizes a numeric identifier by removing leading zeros
function sanitizeNumericIdentifier(value: string): string {
  return value.replace(/^0+(?=\d)/, '');
}

function throwError(version: string) {
  throw new Error('Unable to convert to Semver, incorrect format: ' + version);
}

export class Semver {

  private readonly _major: string;
  private readonly _minor: string;
  private readonly _patch: string;
  private readonly _preRelease: string[];
  private readonly _isStable: boolean;

  // Version string for 'equal' and 'in list' comparisons
  public readonly version: string;

  public constructor(version: string) {
    if (!isString(version)) throwError(version);

    // Separate metadata if exists
    let index = version.indexOf(METADATA_DELIMITER);
    let [vWithoutMetadata, metadata] = index === -1 ? [version] : [version.slice(0, index), version.slice(index + 1)];
    if (metadata === '') throwError(version);

    // Set pre-release versions if exists
    index = vWithoutMetadata.indexOf(PRERELEASE_DELIMITER);
    if (index === -1) {
      this._isStable = true;
      this._preRelease = [];
    } else {
      this._isStable = false;
      this._preRelease = vWithoutMetadata.slice(index + 1).split(VALUE_DELIMITER).map((value) => {
        if (!value) throwError(version);
        return NUMERIC_IDENTIFIER_REGEX.test(value) ?
          sanitizeNumericIdentifier(value) :
          value;
      });
      vWithoutMetadata = vWithoutMetadata.slice(0, index);
    }

    // Set major, minor, and patch versions
    const vParts = vWithoutMetadata.split(VALUE_DELIMITER).map((value) => {
      if (!value || !NUMERIC_IDENTIFIER_REGEX.test(value)) throwError(version);
      return sanitizeNumericIdentifier(value);
    });

    if (vParts.length !== 3) throwError(version);
    this._major = vParts[0];
    this._minor = vParts[1];
    this._patch = vParts[2];

    // Set version string
    this.version = vParts.join(VALUE_DELIMITER);
    if (this._preRelease.length) this.version += PRERELEASE_DELIMITER + this._preRelease.join(VALUE_DELIMITER);
    if (metadata) this.version += METADATA_DELIMITER + metadata;
  }

  /**
   * Precedence comparision between 2 Semver objects.
   *
   * @returns `0` if `this === toCompare`, `-1` if `this < toCompare`, and `1` if `this > toCompare`
   */
  public compare(toCompare: Semver): number {
    if (this.version === toCompare.version) return 0;

    let result = compareStrings(this._major, toCompare._major);
    if (result !== 0) return result;

    result = compareStrings(this._minor, toCompare._minor);
    if (result !== 0) return result;

    result = compareStrings(this._patch, toCompare._patch);
    if (result !== 0) return result;

    if (!this._isStable && toCompare._isStable) return -1;
    if (this._isStable && !toCompare._isStable) return 1;

    for (let i = 0, length = Math.min(this._preRelease.length, toCompare._preRelease.length); i < length; i++) {
      const result = compareStrings(this._preRelease[i], toCompare._preRelease[i]);
      if (result !== 0) return result;
    }

    return this._preRelease.length - toCompare._preRelease.length;
  }
}
