import semver from 'semver';
import { regEx } from '../../../util/regex';
import { RANGE_COMPARATOR_PATTERN, VERSION_PATTERN } from './patterns';

interface LetterTag {
  letter: string;
  number: string;
}

function parseLetterTag(letter?: string, number?: string): LetterTag | null {
  if (letter !== undefined) {
    // apply the same normalizations as poetry
    const spellings: Record<string, string> = {
      alpha: 'a',
      beta: 'b',
      c: 'rc',
      pre: 'rc',
      preview: 'rc',
      r: 'post',
      rev: 'post',
    };
    return {
      letter: spellings[letter] || letter,
      number: number ?? '0',
    };
  }
  if (letter === undefined && number !== undefined) {
    return { letter: 'post', number };
  }
  return null;
}

function notEmpty(s: string): boolean {
  return s !== '';
}

/**
 * Parse versions like poetry.core.masonry.version.Version does (union of SemVer
 * and PEP440, with normalization of certain prerelease tags), and emit in SemVer
 * format. NOTE: this silently discards the epoch field in PEP440 versions, as
 * it has no equivalent in SemVer.
 */
export function poetry2semver(
  poetry_version: string,
  padRelease = true,
): string | null {
  const matchGroups = VERSION_PATTERN.exec(poetry_version)?.groups;
  if (!matchGroups) {
    return null;
  }
  // trim leading zeros from valid numbers
  const releaseParts = matchGroups.release
    .split('.')
    .map((segment) => parseInt(segment));
  while (padRelease && releaseParts.length < 3) {
    releaseParts.push(0);
  }
  const pre = parseLetterTag(matchGroups.pre_l, matchGroups.pre_n);
  const post = matchGroups.post_n1
    ? parseLetterTag(undefined, matchGroups.post_n1)
    : parseLetterTag(matchGroups.post_l, matchGroups.post_n);
  const dev = parseLetterTag(matchGroups.dev_l, matchGroups.dev_n);

  const parts = [releaseParts.map((num) => num.toString()).join('.')];
  if (pre !== null) {
    // trim leading zeros from valid numbers
    pre.number = pre.number.replace(regEx(/^0+(\d+)/), '$1');
    parts.push(`-${pre.letter}.${pre.number}`);
  }
  if (post !== null) {
    // trim leading zeros from valid numbers
    post.number = post.number.replace(regEx(/^0+(\d+)/), '$1');
    parts.push(`-${post.letter}.${post.number}`);
  }
  if (dev !== null) {
    // trim leading zeros from valid numbers
    dev.number = dev.number.replace(regEx(/^0+(\d+)/), '$1');
    parts.push(`-${dev.letter}.${dev.number}`);
  }

  return semver.valid(parts.join(''));
}

/** Reverse normalizations applied by poetry2semver */
export function semver2poetry(version?: string): string | null {
  if (!version) {
    return null;
  }
  const s = semver.parse(version);
  if (!s) {
    return null;
  }
  const spellings: Record<string, string> = {
    a: 'alpha',
    b: 'beta',
    c: 'rc',
    dev: 'alpha',
  };
  s.prerelease = s.prerelease.map((letter) => spellings[letter] ?? letter);
  return s.format();
}

/**
 * Translate a poetry-style version range to npm format
 *
 * This function works like cargo2npm, but it doesn't
 * add a '^', because poetry treats versions without operators as
 * exact versions.
 */
export function poetry2npm(input: string, throwOnUnsupported = false): string {
  // replace commas with spaces, then split at valid semver comparators
  const chunks = input
    .split(',')
    .map((str) => str.trim())
    .filter(notEmpty)
    .join(' ')
    .split(RANGE_COMPARATOR_PATTERN);
  // do not pad versions with zeros in a range
  const transformed = chunks
    .map((chunk) => poetry2semver(chunk, false) ?? chunk)
    .join('')
    .replace(/===/, '=');
  if (throwOnUnsupported) {
    const isUnsupported = transformed
      .split(regEx(/\s+/))
      .some((part) => part.startsWith('!=')); // Patterns like `!=1.2.3` can't be easily translated between poetry/npm
    if (isUnsupported) {
      throw new Error('Unsupported by Poetry versioning implementation');
    }
  }
  return transformed;
}

/**
 * Translate an npm-style version range to poetry format
 *
 * NOTE: This function is largely copied from cargo versioning code.
 * Poetry uses commas (like in cargo) instead of spaces (like in npm)
 * for AND operation.
 */
export function npm2poetry(range: string): string {
  // apply poetry-style normalizations to versions embedded in range string
  // (i.e. anything that is not a range operator, potentially surrounded by whitespace)
  const transformedRange = range
    .split(RANGE_COMPARATOR_PATTERN)
    .map((chunk) => semver2poetry(chunk) ?? chunk)
    .join('');

  // Note: this doesn't remove the ^
  const res = transformedRange
    .split(' ')
    .map((str) => str.trim())
    .filter(notEmpty);

  const operators = ['^', '~', '=', '>', '<', '<=', '>='];
  for (let i = 0; i < res.length - 1; i += 1) {
    if (operators.includes(res[i])) {
      const newValue = res[i] + ' ' + res[i + 1];
      res.splice(i, 2, newValue);
    }
  }
  return res.join(', ').replace(/\s*,?\s*\|\|\s*,?\s*/g, ' || ');
}
