export type SourceMapSegment =
  | [number]
  | [number, number, number, number]
  | [number, number, number, number, number];
export type SourceMapLine = SourceMapSegment[];
export type SourceMapMappings = SourceMapLine[];

const comma = ','.charCodeAt(0);
const semicolon = ';'.charCodeAt(0);
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const intToChar = new Uint8Array(64); // 64 possible chars.
const charToInt = new Uint8Array(128); // z is 122 in ASCII

for (let i = 0; i < chars.length; i++) {
  const c = chars.charCodeAt(i);
  intToChar[i] = c;
  charToInt[c] = i;
}

// Provide a fallback for older environments.
const td =
  typeof TextDecoder !== 'undefined'
    ? /* #__PURE__ */ new TextDecoder()
    : typeof Buffer !== 'undefined'
    ? {
        decode(buf: Uint8Array) {
          const out = Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength);
          return out.toString();
        },
      }
    : {
        decode(buf: Uint8Array) {
          let out = '';
          for (let i = 0; i < buf.length; i++) {
            out += String.fromCharCode(buf[i]);
          }
          return out;
        },
      };

export function decode(mappings: string): SourceMapMappings {
  const state: [number, number, number, number, number] = new Int32Array(5) as any;
  const decoded: SourceMapMappings = [];

  let index = 0;
  do {
    const semi = indexOf(mappings, index);
    const line: SourceMapLine = [];
    let sorted = true;
    let lastCol = 0;
    state[0] = 0;

    for (let i = index; i < semi; i++) {
      let seg: SourceMapSegment;

      i = decodeInteger(mappings, i, state, 0); // genColumn
      const col = state[0];
      if (col < lastCol) sorted = false;
      lastCol = col;

      if (hasMoreVlq(mappings, i, semi)) {
        i = decodeInteger(mappings, i, state, 1); // sourcesIndex
        i = decodeInteger(mappings, i, state, 2); // sourceLine
        i = decodeInteger(mappings, i, state, 3); // sourceColumn

        if (hasMoreVlq(mappings, i, semi)) {
          i = decodeInteger(mappings, i, state, 4); // namesIndex
          seg = [col, state[1], state[2], state[3], state[4]];
        } else {
          seg = [col, state[1], state[2], state[3]];
        }
      } else {
        seg = [col];
      }

      line.push(seg);
    }

    if (!sorted) sort(line);
    decoded.push(line);
    index = semi + 1;
  } while (index <= mappings.length);

  return decoded;
}

function indexOf(mappings: string, index: number): number {
  const idx = mappings.indexOf(';', index);
  return idx === -1 ? mappings.length : idx;
}

function decodeInteger(mappings: string, pos: number, state: SourceMapSegment, j: number): number {
  let value = 0;
  let shift = 0;
  let integer = 0;

  do {
    const c = mappings.charCodeAt(pos++);
    integer = charToInt[c];
    value |= (integer & 31) << shift;
    shift += 5;
  } while (integer & 32);

  const shouldNegate = value & 1;
  value >>>= 1;

  if (shouldNegate) {
    value = -0x80000000 | -value;
  }

  state[j] += value;
  return pos;
}

function hasMoreVlq(mappings: string, i: number, length: number): boolean {
  if (i >= length) return false;
  return mappings.charCodeAt(i) !== comma;
}

function sort(line: SourceMapSegment[]) {
  line.sort(sortComparator);
}

function sortComparator(a: SourceMapSegment, b: SourceMapSegment): number {
  return a[0] - b[0];
}

export function encode(decoded: SourceMapMappings): string;
export function encode(decoded: Readonly<SourceMapMappings>): string;
export function encode(decoded: Readonly<SourceMapMappings>): string {
  const state: [number, number, number, number, number] = new Int32Array(5) as any;
  const bufLength = 1024 * 16;
  const subLength = bufLength - 36;
  const buf = new Uint8Array(bufLength);
  const sub = buf.subarray(0, subLength);
  let pos = 0;
  let out = '';

  for (let i = 0; i < decoded.length; i++) {
    const line = decoded[i];
    if (i > 0) {
      if (pos === bufLength) {
        out += td.decode(buf);
        pos = 0;
      }
      buf[pos++] = semicolon;
    }
    if (line.length === 0) continue;

    state[0] = 0;

    for (let j = 0; j < line.length; j++) {
      const segment = line[j];
      // We can push up to 5 ints, each int can take at most 7 chars, and we
      // may push a comma.
      if (pos > subLength) {
        out += td.decode(sub);
        buf.copyWithin(0, subLength, pos);
        pos -= subLength;
      }
      if (j > 0) buf[pos++] = comma;

      pos = encodeInteger(buf, pos, state, segment, 0); // genColumn

      if (segment.length === 1) continue;
      pos = encodeInteger(buf, pos, state, segment, 1); // sourcesIndex
      pos = encodeInteger(buf, pos, state, segment, 2); // sourceLine
      pos = encodeInteger(buf, pos, state, segment, 3); // sourceColumn

      if (segment.length === 4) continue;
      pos = encodeInteger(buf, pos, state, segment, 4); // namesIndex
    }
  }

  return out + td.decode(buf.subarray(0, pos));
}

function encodeInteger(
  buf: Uint8Array,
  pos: number,
  state: SourceMapSegment,
  segment: SourceMapSegment,
  j: number,
): number {
  const next = segment[j];
  let num = next - state[j];
  state[j] = next;

  num = num < 0 ? (-num << 1) | 1 : num << 1;
  do {
    let clamped = num & 0b011111;
    num >>>= 5;
    if (num > 0) clamped |= 0b100000;
    buf[pos++] = intToChar[clamped];
  } while (num > 0);

  return pos;
}
