// License: CC0 (no rights reserved).

// This is based on https://observablehq.com/@dgreensp/implementing-fractional-indexing

export const digits = 'abcdefghijklmnopqrstuvwxyz';

const zero = digits[0] as 'a';

const integerZero = 'na';

const smallestInteger = 'a'.repeat(14);

// `a` may be an empty string, `b` is null or a non-empty string.
// `a < b` lexicographically if `b` is non-null.
// No trailing zeros are allowed.
function midpoint(a: string, b: string | null): string {
  if (b !== null && a >= b) {
    throw new Error(a + ' >= ' + b);
  }
  if (a.slice(-1) === zero || b?.slice(-1) === zero) {
    throw new Error('trailing zero');
  }
  if (b) {
    // Remove longest common prefix. Pad `a` with zeroes as we go.
    // Note that we don't need to pad `b` because it can't
    // end before `a` while traversing the common prefix.
    let n = 0;
    while ((a[n] || zero) === b[n]) {
      n++;
    }
    if (n > 0) {
      return b.slice(0, n) + midpoint(a.slice(n), b.slice(n));
    }
  }
  // The first digits (or lack of digit) are different.
  const digitA = a ? digits.indexOf(a[0] as string) : 0;
  const digitB = b ? digits.indexOf(b[0] as string) : digits.length;
  if (digitB - digitA > 1) {
    return digits[Math.round((digitA + digitB) / 2)] as string;
  }
  // The first digits are consecutive.
  if (b && b.length > 1) {
    return b.slice(0, 1);
  }
  // `b` is null or has length 1 (a single digit).
  // the first digit of `a` is the previous digit to `b`,
  // or 9 if `b` is null.
  // given, for example, midpoint('49', '5'), return
  // '4' + midpoint('9', null), which will become
  // '4' + '9' + midpoint('', null), which is '495'
  return digits[digitA] + midpoint(a.slice(1), null);
}

const getIntegerLength = (head: string) => {
  if (head >= 'a' && head <= 'm') {
    // We add 2 because there's the header and there's always at least one digit after the header.
    return 'm'.charCodeAt(0) - head.charCodeAt(0) + 2;
  }
  if (head >= 'n' && head <= 'z') {
    return head.charCodeAt(0) - 'n'.charCodeAt(0) + 2;
  }
  throw new Error('invalid order key head: ' + head);
};

const getIntegerPart = (key: string) => key.slice(0, getIntegerLength(key[0] as string));

// Note that this may return null, as there is a largest integer.
const incrementInteger = (x: string) => {
  const [head, ...digs] = x.split('') as [string, ...string[]];
  let carry = true;
  for (let i = digs.length - 1; carry && i >= 0; i--) {
    const d = digits.indexOf(digs[i] as string) + 1;
    if (d === digits.length) {
      digs[i] = zero;
    } else {
      digs[i] = digits[d] as string;
      carry = false;
    }
  }
  if (carry) {
    if (head === 'm') {
      return 'n' + zero;
    }
    if (head === 'z') {
      return null;
    }
    const newHead = String.fromCharCode(head.charCodeAt(0) + 1);
    if (newHead > 'n') {
      return newHead + digs.join('') + zero;
    }
    digs.pop();
    return newHead + digs.join('');
  }
  return head + digs.join('');
};

const z = digits.slice(-1);

// Note that this may return null, as there is a smallest integer.
const decrementInteger = (x: string) => {
  const [head, ...digs] = x.split('') as [string, ...string[]];
  let borrow = true;
  for (let i = digs.length - 1; borrow && i >= 0; i--) {
    const d = digits.indexOf(digs[i] as string) - 1;
    if (d === -1) {
      digs[i] = z;
    } else {
      digs[i] = digits[d] as string;
      borrow = false;
    }
  }
  if (borrow) {
    if (head === 'n') {
      return 'm' + z;
    }
    if (head === 'a') {
      return null;
    }
    const newHead = String.fromCharCode(head.charCodeAt(0) - 1);
    if (newHead < 'm') {
      return newHead + digs.join('') + z;
    }
    digs.pop();
    return newHead + digs.join('');
  }
  return head + digs.join('');
};

/**
 * Returns the key that is halfway between the two given keys, which are expected to have the format that this library produces.
 * If both a and b are non-null, a must sort lexicographically before b.
 */
export function generateKeyBetween(a: string | null, b: string | null): string {
  if (a !== null && b !== null && a >= b) {
    throw new Error(`expected ${a} >= ${b}`);
  }
  if (a === null) {
    if (b === null) {
      return integerZero;
    }

    const bIntegerPart = getIntegerPart(b);
    const bFractionPart = b.slice(bIntegerPart.length);
    if (bIntegerPart === smallestInteger) {
      return bIntegerPart + midpoint('', bFractionPart);
    }
    if (bIntegerPart < b) {
      return bIntegerPart;
    }
    const res = decrementInteger(bIntegerPart);
    if (res == null) {
      throw new Error('cannot decrement any more');
    }
    return res;
  }

  const aIntegerPart = getIntegerPart(a);
  const aFractionPart = a.slice(aIntegerPart.length);

  if (b === null) {
    const nextInteger = incrementInteger(aIntegerPart);
    return nextInteger === null ? aIntegerPart + midpoint(aFractionPart, null) : nextInteger;
  }

  const bIntegerPart = getIntegerPart(b);
  const bFractionPart = b.slice(bIntegerPart.length);
  if (aIntegerPart === bIntegerPart) {
    return aIntegerPart + midpoint(aFractionPart, bFractionPart);
  }

  const nextInteger = incrementInteger(aIntegerPart);
  if (nextInteger === null) {
    throw new Error('cannot increment any more');
  }
  if (nextInteger < b) {
    return nextInteger;
  }
  return aIntegerPart + midpoint(aFractionPart, null);
}

/**
 * Returns an array of n distinct keys in sorted order.
 * If a and b are both null, keys start at the middle of the possible key range, so it returns [na, nb, ...].
 * If only a is null, returns the "integer" key immediately before b.
 * If only b is null, returns the "integer" key immediately after a.
 * Otherwise, returns (short) keys strictly between a and b.
 */
export function generateKeysBetween(a: string | null, b: string | null, n: number): string[] {
  if (n === 0) {
    return [];
  }
  if (n === 1) {
    return [generateKeyBetween(a, b)];
  }
  if (b == null) {
    let c = generateKeyBetween(a, b);
    const result = [c];
    for (let i = 0; i < n - 1; i++) {
      c = generateKeyBetween(c, b);
      result.push(c);
    }
    return result;
  }
  if (a === null) {
    let c = generateKeyBetween(a, b);
    const result = [c];
    for (let i = 0; i < n - 1; i++) {
      c = generateKeyBetween(a, c);
      result.push(c);
    }
    result.reverse();
    return result;
  }
  const mid = Math.floor(n / 2);
  const c = generateKeyBetween(a, b);

  return [...generateKeysBetween(a, c, mid), c, ...generateKeysBetween(c, b, n - mid - 1)];
}
