export type GenericIndexable<T> = { [key: number]: T; readonly length: number };
type TypedArray =
  | Int8Array
  | Int16Array
  | Int32Array
  | Uint8Array
  | Uint16Array
  | Uint32Array
  | Float32Array
  | Float64Array;
export type Indexable<T> = string | T[] | TypedArray | GenericIndexable<T>;

export interface Sliceable<T> extends GenericIndexable<T> {
  slice(start: number, end?: number): this;
}

type Vec4 = [number, number, number, number];
type Vec3 = [number, number, number];

type Comparator = (i: number, j: number) => boolean;

type DiffState = {
  i: number;
  N: number;
  j: number;
  M: number;
  Z: number;
  b: TypedArray;
  eq: (x: number, y: number) => boolean;
  should_break?: () => boolean;
  pxs: number;
  pxe: number;
  pys: number;
  pye: number;
  oxs: number;
  oxe: number;
  oys: number;
  oye: number;
  stack_top: number;
  stack_base: number[];
};

// Find the list of differences between 2 lists by
// recursive subdivision, requring O(min(N,M)) space
// and O(min(N,M)*D) worst-case execution time where
// D is the number of differences.
function diff_internal(state: DiffState, c: number): number {
  const { b, eq, stack_base } = state;
  let { i, N, j, M, Z, stack_top } = state;
  for (;;) {
    switch (c) {
      case 0: {
        Z_block: while (N > 0 && M > 0) {
          b.fill(0, 0, 2 * Z);

          const W = N - M;
          const L = N + M;
          const parity = L & 1;
          const offsetx = i + N - 1;
          const offsety = j + M - 1;
          const hmax = (L + parity) / 2;
          let z: number;
          h_loop: for (let h = 0; h <= hmax; h++) {
            if (state.should_break && state.should_break()) {
              return 0;
            }
            const kmin = 2 * Math.max(0, h - M) - h;
            const kmax = h - 2 * Math.max(0, h - N);

            // Forward pass
            for (let k = kmin; k <= kmax; k += 2) {
              const gkm = b[k - 1 - Z * Math.floor((k - 1) / Z)];
              const gkp = b[k + 1 - Z * Math.floor((k + 1) / Z)];
              const u = k === -h || (k !== h && gkm < gkp) ? gkp : gkm + 1;
              const v = u - k;
              let x = u;
              let y = v;
              while (x < N && y < M && eq(i + x, j + y)) x++, y++;
              b[k - Z * Math.floor(k / Z)] = x;
              if (
                parity === 1 &&
                (z = W - k) >= 1 - h &&
                z < h &&
                x + b[Z + z - Z * Math.floor(z / Z)] >= N
              ) {
                if (h > 1 || x !== u) {
                  stack_base[stack_top++] = i + x;
                  stack_base[stack_top++] = N - x;
                  stack_base[stack_top++] = j + y;
                  stack_base[stack_top++] = M - y;
                  N = u;
                  M = v;
                  Z = 2 * (Math.min(N, M) + 1);
                  continue Z_block;
                } else break h_loop;
              }
            }

            // Reverse pass
            for (let k = kmin; k <= kmax; k += 2) {
              const pkm = b[Z + k - 1 - Z * Math.floor((k - 1) / Z)];
              const pkp = b[Z + k + 1 - Z * Math.floor((k + 1) / Z)];
              const u = k === -h || (k !== h && pkm < pkp) ? pkp : pkm + 1;
              const v = u - k;
              let x = u;
              let y = v;
              while (x < N && y < M && eq(offsetx - x, offsety - y)) x++, y++;
              b[Z + k - Z * Math.floor(k / Z)] = x;
              if (
                parity === 0 &&
                (z = W - k) >= -h &&
                z <= h &&
                x + b[z - Z * Math.floor(z / Z)] >= N
              ) {
                if (h > 0 || x !== u) {
                  stack_base[stack_top++] = i + N - u;
                  stack_base[stack_top++] = u;
                  stack_base[stack_top++] = j + M - v;
                  stack_base[stack_top++] = v;
                  N = N - x;
                  M = M - y;
                  Z = 2 * (Math.min(N, M) + 1);
                  continue Z_block;
                } else break h_loop;
              }
            }
          }

          if (N === M) continue;
          if (M > N) {
            i += N;
            j += N;
            M -= N;
            N = 0;
          } else {
            i += M;
            j += M;
            N -= M;
            M = 0;
          }

          // We already know either N or M is zero, so we can
          // skip the extra check at the top of the loop.
          break;
        }

        // yield delete_start, delete_end, insert_start, insert_end
        // At this point, at least one of N & M is zero, or we
        // wouldn't have gotten out of the preceding loop yet.
        if (N + M !== 0) {
          if (state.pxe === i || state.pye === j) {
            // it is a contiguous difference extend the existing one
            state.pxe = i + N;
            state.pye = j + M;
          } else {
            const sx = state.pxs;
            state.oxs = state.pxs;
            state.oxe = state.pxe;
            state.oys = state.pys;
            state.oye = state.pye;

            // Defer this one until we can check the next one
            state.pxs = i;
            state.pxe = i + N;
            state.pys = j;
            state.pye = j + M;

            if (sx >= 0) {
              state.i = i;
              state.N = N;
              state.j = j;
              state.M = M;
              state.Z = Z;
              state.stack_top = stack_top;
              return 1;
            }
          }
        }
      }
      case 1: {
        if (stack_top === 0) return 2;

        M = stack_base[--stack_top];
        j = stack_base[--stack_top];
        N = stack_base[--stack_top];
        i = stack_base[--stack_top];
        Z = 2 * (Math.min(N, M) + 1);
        c = 0;
      }
    }
  }
}

class DiffGen implements IterableIterator<Vec4> {
  private c = 0;
  private result: IteratorResult<Vec4, undefined> = {
    value: null as any,
    done: false,
  };

  constructor(private state: DiffState) {}

  [Symbol.iterator]() {
    return this;
  }

  next() {
    const { state, result } = this;

    if (state.should_break && state.should_break()) {
      result.done = true;
      result.value = undefined;
      return result;
    }

    if (this.c > 1) {
      result.done = true;
      result.value = undefined;
      return result;
    }
    const c = diff_internal(state, this.c);
    if (state.should_break && state.should_break()) {
      result.done = true;
      result.value = undefined;
      return result;
    }

    this.c = c;
    if (c === 1) {
      result.value = [state.oxs, state.oxe, state.oys, state.oye];
      return result;
    }
    if (state.pxs >= 0) {
      result.value = [state.pxs, state.pxe, state.pys, state.pye];
      return result;
    }
    result.done = true;
    result.value = undefined;
    return result;
  }
}

export function diff_core(
  i: number,
  N: number,
  j: number,
  M: number,
  eq: Comparator,
  should_break?: () => boolean
): IterableIterator<Vec4> {
  const Z = (Math.min(N, M) + 1) * 2;
  const L = N + M;

  let constructor: any = Float64Array;
  if (L < 256) constructor = Uint8Array;
  else if (L < 65536) constructor = Uint16Array;
  else if (L < 4294967297) constructor = Uint32Array;

  const b = new constructor(2 * Z);

  return new DiffGen({
    i,
    N,
    j,
    M,
    Z,
    b,
    eq,
    should_break,
    pxs: -1,
    pxe: -1,
    pys: -1,
    pye: -1,
    oxs: -1,
    oxe: -1,
    oys: -1,
    oye: -1,
    stack_top: 0,
    stack_base: [],
  });
}

export function diff<T extends Indexable<unknown>>(
  xs: T,
  ys: T,
  eq?: Comparator,
  should_break?: () => boolean
): IterableIterator<Vec4> {
  let [i, N, M] = [0, xs.length, ys.length];

  if (typeof eq === "function") {
    // eliminate common prefix
    while (i < N && i < M && eq(i, i)) i++;

    // check for equality
    if (i === N && i === M) return [][Symbol.iterator]();

    // eliminate common suffix
    while (eq(--N, --M) && N > i && M > i);
  } else {
    // eliminate common prefix
    while (i < N && i < M && xs[i] === ys[i]) i++;

    // check for equality
    if (i === N && i === M) return [][Symbol.iterator]();

    // eliminate common suffix
    while (xs[--N] === ys[--M] && N > i && M > i);

    eq = (i, j) => xs[i] === ys[j];
  }

  return diff_core(i, N + 1 - i, i, M + 1 - i, eq, should_break);
}

class LCSGen implements IterableIterator<Vec3> {
  private i = 0;
  private j = 0;

  constructor(
    private diff: IterableIterator<Vec4>,
    private N: number
  ) {}

  [Symbol.iterator]() {
    return this;
  }

  next() {
    // Convert diffs into the dual similar-aligned representation.
    // In each iteration, i and j will be aligned at the beginning
    // of a shared section. This section is yielded, and i and j
    // are re-aligned at the end of the succeeding unique sections.
    const rec = this.diff.next();
    if (rec.done) {
      const { i, j, N } = this;
      if (i < N) {
        rec.done = false as any;
        rec.value = [i, j, N - i] as any;
        this.i = N;
      }
      return rec as IteratorResult<Vec3>;
    }
    const v = rec.value;
    const sx = v[0];
    const ex = v[1];
    const ey = v[3];
    const { i, j } = this;
    if (i !== sx) {
      v.length--; // re-use the vec4 as a vec3 to avoid allocation
      v[0] = i;
      v[1] = j;
      v[2] = sx - i;
    }

    this.i = ex;
    this.j = ey;

    return rec as unknown as IteratorResult<Vec3>;
  }
}

export function lcs<T extends Indexable<unknown>>(
  xs: T,
  ys: T,
  eq?: Comparator
): IterableIterator<Vec3> {
  return new LCSGen(diff(xs, ys, eq), xs.length);
}

export function* calcPatch<T, S extends Sliceable<T>>(
  xs: S,
  ys: S,
  eq?: Comparator,
  should_break?: () => boolean
): Generator<[number, number, S]> {
  // Taking subarrays is cheaper than slicing for TypedArrays.
  const slice = ArrayBuffer.isView(xs)
    ? (Uint8Array.prototype.subarray as unknown as typeof xs.slice)
    : xs.slice;

  for (const v of diff(xs, ys, eq, should_break)) {
    v[2] = slice.call(ys, v[2], v[3]) as any;
    yield v as any;
  }
}

export function* applyPatch<T, S extends Sliceable<T>>(
  xs: S,
  patch: Iterable<[number, number, S]>
): Generator<S> {
  let i = 0; // Taking subarrays is cheaper than slicing for TypedArrays.
  const slice = ArrayBuffer.isView(xs)
    ? (Uint8Array.prototype.subarray as unknown as typeof xs.slice)
    : xs.slice;
  for (const [dels, dele, ins] of patch) {
    if (i < dels) yield slice.call(xs, i, dels);
    if (ins.length > 0) yield ins;
    i = dele;
  }
  if (i < xs.length) yield slice.call(xs, i);
}

export function* calcSlices<T, S extends Sliceable<T>>(
  xs: S,
  ys: S,
  eq?: Comparator
): Generator<[-1 | 0 | 1, S]> {
  let i = 0; // Taking subarrays is cheaper than slicing for TypedArrays.
  const slice = ArrayBuffer.isView(xs)
    ? (Uint8Array.prototype.subarray as unknown as typeof xs.slice)
    : xs.slice;
  for (const [dels, dele, inss, inse] of diff(xs, ys, eq)) {
    if (i < dels) yield [0, slice.call(xs, i, dels)];
    if (dels < dele) yield [-1, slice.call(xs, dels, dele)];
    if (inss < inse) yield [1, slice.call(ys, inss, inse)];
    i = dele;
  }
  if (i < xs.length) yield [0, xs.slice(i)];
}
