/*
 This file is part of GNU Taler
 (C) 2023 Taler Systems S.A.

 GNU Taler is free software; you can redistribute it and/or modify it under the
 terms of the GNU General Public License as published by the Free Software
 Foundation; either version 3, or (at your option) any later version.

 GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

 You should have received a copy of the GNU General Public License along with
 GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */

/*
Encoding rules (inspired by Firefox, but slightly simplified):

Numbers:   0x10 n n n n n n n n
Dates:     0x20 n n n n n n n n
Strings:   0x30 s s s s ... 0
Binaries:  0x40 s s s s ... 0
Arrays:    0x50 i i i ... 0

Numbers/dates are encoded as 64-bit IEEE 754 floats with the sign bit
flipped, in order to make them sortable.
*/

/**
 * Imports.
 */
import { IDBValidKey } from "../idbtypes.js";

const tagNum = 0xa0;
const tagDate = 0xb0;
const tagString = 0xc0;
const tagBinary = 0xc0;
const tagArray = 0xe0;

const oneByteOffset = 0x01;
const twoByteOffset = 0x7f;
const oneByteMax = 0x7e;
const twoByteMax = 0x3fff + twoByteOffset;
const twoByteMask = 0b1000_0000;
const threeByteMask = 0b1100_0000;

export function countEncSize(c: number): number {
  if (c > twoByteMax) {
    return 3;
  }
  if (c > oneByteMax) {
    return 2;
  }
  return 1;
}

export function writeEnc(dv: DataView, offset: number, c: number): number {
  if (c > twoByteMax) {
    dv.setUint8(offset + 2, (c & 0xff) << 6);
    dv.setUint8(offset + 1, (c >>> 2) & 0xff);
    dv.setUint8(offset, threeByteMask | (c >>> 10));
    return 3;
  } else if (c > oneByteMax) {
    c -= twoByteOffset;
    dv.setUint8(offset + 1, c & 0xff);
    dv.setUint8(offset, (c >>> 8) | twoByteMask);
    return 2;
  } else {
    c += oneByteOffset;
    dv.setUint8(offset, c);
    return 1;
  }
}

export function internalSerializeString(
  dv: DataView,
  offset: number,
  key: string,
): number {
  dv.setUint8(offset, tagString);
  let n = 1;
  for (let i = 0; i < key.length; i++) {
    let c = key.charCodeAt(i);
    n += writeEnc(dv, offset + n, c);
  }
  // Null terminator
  dv.setUint8(offset + n, 0);
  n++;
  return n;
}

export function countSerializeKey(key: IDBValidKey): number {
  if (typeof key === "number") {
    return 9;
  }
  if (key instanceof Date) {
    return 9;
  }
  if (key instanceof ArrayBuffer) {
    let len = 2;
    const uv = new Uint8Array(key);
    for (let i = 0; i < uv.length; i++) {
      len += countEncSize(uv[i]);
    }
    return len;
  }
  if (ArrayBuffer.isView(key)) {
    let len = 2;
    const uv = new Uint8Array(key.buffer, key.byteOffset, key.byteLength);
    for (let i = 0; i < uv.length; i++) {
      len += countEncSize(uv[i]);
    }
    return len;
  }
  if (typeof key === "string") {
    let len = 2;
    for (let i = 0; i < key.length; i++) {
      len += countEncSize(key.charCodeAt(i));
    }
    return len;
  }
  if (Array.isArray(key)) {
    let len = 2;
    for (let i = 0; i < key.length; i++) {
      len += countSerializeKey(key[i]);
    }
    return len;
  }
  throw Error("unsupported type for key");
}

function internalSerializeNumeric(
  dv: DataView,
  offset: number,
  tag: number,
  val: number,
): number {
  dv.setUint8(offset, tagNum);
  dv.setFloat64(offset + 1, val);
  // Flip sign bit
  let b = dv.getUint8(offset + 1);
  b ^= 0x80;
  dv.setUint8(offset + 1, b);
  return 9;
}

function internalSerializeArray(
  dv: DataView,
  offset: number,
  key: any[],
): number {
  dv.setUint8(offset, tagArray);
  let n = 1;
  for (let i = 0; i < key.length; i++) {
    n += internalSerializeKey(key[i], dv, offset + n);
  }
  dv.setUint8(offset + n, 0);
  n++;
  return n;
}

function internalSerializeBinary(
  dv: DataView,
  offset: number,
  key: Uint8Array,
): number {
  dv.setUint8(offset, tagBinary);
  let n = 1;
  for (let i = 0; i < key.length; i++) {
    n += internalSerializeKey(key[i], dv, offset + n);
  }
  dv.setUint8(offset + n, 0);
  n++;
  return n;
}

function internalSerializeKey(
  key: IDBValidKey,
  dv: DataView,
  offset: number,
): number {
  if (typeof key === "number") {
    return internalSerializeNumeric(dv, offset, tagNum, key);
  }
  if (key instanceof Date) {
    return internalSerializeNumeric(dv, offset, tagDate, key.getDate());
  }
  if (typeof key === "string") {
    return internalSerializeString(dv, offset, key);
  }
  if (Array.isArray(key)) {
    return internalSerializeArray(dv, offset, key);
  }
  if (key instanceof ArrayBuffer) {
    return internalSerializeBinary(dv, offset, new Uint8Array(key));
  }
  if (ArrayBuffer.isView(key)) {
    const uv = new Uint8Array(key.buffer, key.byteOffset, key.byteLength);
    return internalSerializeBinary(dv, offset, uv);
  }
  throw Error("unsupported type for key");
}

export function serializeKey(key: IDBValidKey): Uint8Array {
  const len = countSerializeKey(key);
  let buf = new Uint8Array(len);
  const outLen = internalSerializeKey(key, new DataView(buf.buffer), 0);
  if (len != outLen) {
    throw Error("internal invariant failed");
  }
  let numTrailingZeroes = 0;
  for (let i = buf.length - 1; i >= 0 && buf[i] == 0; i--, numTrailingZeroes++);
  if (numTrailingZeroes > 0) {
    buf = buf.slice(0, buf.length - numTrailingZeroes);
  }
  return buf;
}

function internalReadString(dv: DataView, offset: number): [number, string] {
  const chars: string[] = [];
  while (offset < dv.byteLength) {
    const v = dv.getUint8(offset);
    if (v == 0) {
      // Got end-of-string.
      offset += 1;
      break;
    }
    let c: number;
    if ((v & threeByteMask) === threeByteMask) {
      const b1 = v;
      const b2 = dv.getUint8(offset + 1);
      const b3 = dv.getUint8(offset + 2);
      c = (b1 << 10) | (b2 << 2) | (b3 >> 6);
      offset += 3;
    } else if ((v & twoByteMask) === twoByteMask) {
      const b1 = v & ~twoByteMask;
      const b2 = dv.getUint8(offset + 1);
      c = ((b1 << 8) | b2) + twoByteOffset;
      offset += 2;
    } else {
      c = v - oneByteOffset;
      offset += 1;
    }
    chars.push(String.fromCharCode(c));
  }
  return [offset, chars.join("")];
}

function internalReadBytes(dv: DataView, offset: number): [number, Uint8Array] {
  let count = 0;
  while (offset + count < dv.byteLength) {
    const v = dv.getUint8(offset + count);
    if (v === 0) {
      break;
    }
    count++;
  }
  let writePos = 0;
  const bytes = new Uint8Array(count);
  while (offset < dv.byteLength) {
    const v = dv.getUint8(offset);
    if (v == 0) {
      offset += 1;
      break;
    }
    let c: number;
    if ((v & threeByteMask) === threeByteMask) {
      const b1 = v;
      const b2 = dv.getUint8(offset + 1);
      const b3 = dv.getUint8(offset + 2);
      c = (b1 << 10) | (b2 << 2) | (b3 >> 6);
      offset += 3;
    } else if ((v & twoByteMask) === twoByteMask) {
      const b1 = v & ~twoByteMask;
      const b2 = dv.getUint8(offset + 1);
      c = ((b1 << 8) | b2) + twoByteOffset;
      offset += 2;
    } else {
      c = v - oneByteOffset;
      offset += 1;
    }
    bytes[writePos] = c;
    writePos++;
  }
  return [offset, bytes];
}

/**
 * Same as DataView.getFloat64, but logically pad input
 * with zeroes on the right if read offset would be out
 * of bounds.
 * 
 * This allows reading from buffers where zeros have been
 * truncated.
 */
function getFloat64Trunc(dv: DataView, offset: number): number {
  if (offset + 7 >= dv.byteLength) {
    const buf = new Uint8Array(8);
    for (let i = offset; i < dv.byteLength; i++) {
      buf[i - offset] = dv.getUint8(i);
    }
    const dv2 = new DataView(buf.buffer);
    return dv2.getFloat64(0);
  } else {
    return dv.getFloat64(offset);
  }
}

function internalDeserializeKey(
  dv: DataView,
  offset: number,
): [number, IDBValidKey] {
  let tag = dv.getUint8(offset);
  switch (tag) {
    case tagNum: {
      const num = -getFloat64Trunc(dv, offset + 1);
      const newOffset = Math.min(offset + 9, dv.byteLength);
      return [newOffset, num];
    }
    case tagDate: {
      const num = -getFloat64Trunc(dv, offset + 1);
      const newOffset = Math.min(offset + 9, dv.byteLength);
      return [newOffset, new Date(num)];
    }
    case tagString: {
      return internalReadString(dv, offset + 1);
    }
    case tagBinary: {
      return internalReadBytes(dv, offset + 1);
    }
    case tagArray: {
      const arr: any[] = [];
      offset += 1;
      while (offset < dv.byteLength) {
        const innerTag = dv.getUint8(offset);
        if (innerTag === 0) {
          offset++;
          break;
        }
        const [innerOff, innerVal] = internalDeserializeKey(dv, offset);
        arr.push(innerVal);
        offset = innerOff;
      }
      return [offset, arr];
    }
    default:
      throw Error("invalid key (unrecognized tag)");
  }
}

export function deserializeKey(encodedKey: Uint8Array): IDBValidKey {
  const dv = new DataView(
    encodedKey.buffer,
    encodedKey.byteOffset,
    encodedKey.byteLength,
  );
  let [off, res] = internalDeserializeKey(dv, 0);
  if (off != encodedKey.byteLength) {
    throw Error("internal invariant failed");
  }
  return res;
}
