/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/
 */

const MAX_ARGUMENTS_TO_RECORD = 4;
const ZERO_ARGUMENTS_MAGIC = -2;
const EXPIRED_VALUES_MAGIC = -1;

const EXPECTED_VALUE_SUMMARIES_VERSION = 2;

const JSVAL_TYPE_DOUBLE = 0x00;
const JSVAL_TYPE_INT32 = 0x01;
const JSVAL_TYPE_BOOLEAN = 0x02;
const JSVAL_TYPE_UNDEFINED = 0x03;
const JSVAL_TYPE_NULL = 0x04;
const JSVAL_TYPE_MAGIC = 0x05;
const JSVAL_TYPE_STRING = 0x06;
const JSVAL_TYPE_SYMBOL = 0x07;
const JSVAL_TYPE_BIGINT = 0x09;
const JSVAL_TYPE_OBJECT = 0x0c;

const GETTER_SETTER_MAGIC = 0x0f;

const GENERIC_OBJECT_HAS_DENSE_ELEMENTS = 1;
const SYMBOL_NO_DESCRIPTION = 1;

const NUMBER_IS_OUT_OF_LINE_MAGIC = 0xf;
const MIN_INLINE_INT = -1;

const STRING_ENCODING_LATIN1 = 0;
const STRING_ENCODING_TWO_BYTE = 1;
const STRING_ENCODING_UTF8 = 2;

const OBJECT_KIND_NOT_IMPLEMENTED = 0;
const OBJECT_KIND_ARRAY_LIKE = 1;
const OBJECT_KIND_MAP_LIKE = 2;
const OBJECT_KIND_FUNCTION = 3;
const OBJECT_KIND_WRAPPED_PRIMITIVE_OBJECT = 4;
const OBJECT_KIND_GENERIC_OBJECT = 5;
const OBJECT_KIND_PROXY_OBJECT = 6;
const OBJECT_KIND_EXTERNAL = 7;
const OBJECT_KIND_ERROR = 8;

const MAX_COLLECTION_VALUES = 16;

const EXTERNAL_SUMMARY_EXPECTED_VERSION = 1;
const EXTERNAL_SUMMARY_KIND_OTHER = 0;
const EXTERNAL_SUMMARY_KIND_NODE = 1;
const EXTERNAL_SUMMARY_KIND_EXCEPTION = 2;

// const EXTERNAL_NODE_SUBKIND_OTHER = 0; (handled implicitly)
const EXTERNAL_NODE_SUBKIND_ELEMENT = 1;
const EXTERNAL_NODE_SUBKIND_ATTR = 2;
const EXTERNAL_NODE_SUBKIND_DOCUMENT = 3;
const EXTERNAL_NODE_SUBKIND_DOCUMENT_FRAGMENT = 4;
const EXTERNAL_NODE_SUBKIND_TEXT = 5;
const EXTERNAL_NODE_SUBKIND_COMMENT = 6;

class BufferReader {
  #view;
  #index;

  constructor(buffer, index = 0) {
    this.#view = new DataView(buffer);
    this.#index = index;
  }

  setIndex(value) {
    this.#index = value;
  }

  getIndex() {
    return this.#index;
  }

  peekUint8() {
    return this.#view.getUint8(this.#index);
  }

  readUint8() {
    const result = this.#view.getUint8(this.#index);
    this.#index += 1;
    return result;
  }

  readUint16() {
    const result = this.#view.getUint16(this.#index, true);
    this.#index += 2;
    return result;
  }

  readUint32() {
    const result = this.#view.getUint32(this.#index, true);
    this.#index += 4;
    return result;
  }

  readInt8() {
    const result = this.#view.getInt8(this.#index);
    this.#index += 1;
    return result;
  }

  readInt16() {
    const result = this.#view.getInt16(this.#index, true);
    this.#index += 2;
    return result;
  }

  readInt32() {
    const result = this.#view.getInt32(this.#index, true);
    this.#index += 4;
    return result;
  }

  readFloat64() {
    const result = this.#view.getFloat64(this.#index, true);
    this.#index += 8;
    return result;
  }

  readString() {
    const encodingAndLength = this.readUint16();
    const length = encodingAndLength & ~(0b11 << 14);
    const encoding = encodingAndLength >> 14;
    if (length == 0) {
      return "";
    }

    let result = "";
    if (encoding == STRING_ENCODING_LATIN1) {
      const decoder = new TextDecoder("latin1");
      result = decoder.decode(
        this.#view.buffer.slice(this.#index, this.#index + length)
      );
      this.#index += length;
    } else if (encoding == STRING_ENCODING_UTF8) {
      const decoder = new TextDecoder("utf-8");
      result = decoder.decode(
        this.#view.buffer.slice(this.#index, this.#index + length)
      );
      this.#index += length;
    } else if (encoding == STRING_ENCODING_TWO_BYTE) {
      const decoder = new TextDecoder("utf-16"); // this isn't quite right, is it? ugh.
      const size = length * 2;
      result = decoder.decode(
        this.#view.buffer.slice(this.#index, this.#index + size)
      );
      this.#index += size;
    }
    return result;
  }
}

function readArrayLikeSummary(result, reader, depth, shapes) {
  const shapeId = reader.readUint32();
  const shape = shapes[shapeId];

  if (!shape || shape.length <= 0) {
    return;
  }
  result.class = shape[0];

  const preview = {};
  preview.kind = "ArrayLike";

  preview.items = [];
  preview.length = reader.readUint32();
  if (depth < 1) {
    for (let i = 0; i < preview.length && i < MAX_COLLECTION_VALUES; i++) {
      if (reader.peekUint8() === JSVAL_TYPE_MAGIC) {
        reader.readUint8();
        continue;
      }

      const nestedSummary = readValueSummary(reader, depth + 1, shapes);
      preview.items.push(nestedSummary);
    }
  }

  result.preview = preview;
}

function readFunctionSummary(result, reader) {
  result.class = "Function";
  result.name = reader.readString();
  result.parameterNames = [];
  const numParameterNames = reader.readUint32();
  for (let i = 0; i < numParameterNames && i < MAX_COLLECTION_VALUES; i++) {
    result.parameterNames.push(reader.readString());
  }
}

function readMapLikeSummary(result, reader, depth, shapes) {
  const shapeId = reader.readUint32();
  const shape = shapes[shapeId];

  if (!shape || shape.length <= 0) {
    return;
  }
  result.class = shape[0];

  const preview = {};
  preview.kind = "MapLike";

  preview.entries = [];
  preview.size = reader.readUint32();
  if (depth < 1) {
    for (let i = 0; i < preview.size && i < MAX_COLLECTION_VALUES; i++) {
      const keySummary = readValueSummary(reader, depth + 1, shapes);
      const valueSummary = readValueSummary(reader, depth + 1, shapes);
      preview.entries.push([
        {
          configurable: true,
          enumerable: true,
          writable: true,
          value: keySummary,
        },
        {
          configurable: true,
          enumerable: true,
          writable: true,
          value: valueSummary,
        },
      ]);
    }
  }

  result.preview = preview;
}

function readGenericObjectSummary(result, reader, flags, depth, shapes) {
  const shapeId = reader.readUint32();
  const shape = shapes[shapeId];

  if (!shape || shape.length <= 0) {
    return;
  }
  result.class = shape[0];

  const preview = {};
  preview.kind = "Object";

  const hasDenseElements = !!(flags & GENERIC_OBJECT_HAS_DENSE_ELEMENTS);
  const ownProperties = {};
  let ownPropertiesLength = reader.readUint32();

  if (depth < 1) {
    for (let i = 1; i < shape.length && i <= MAX_COLLECTION_VALUES; i++) {
      const header = reader.peekUint8();
      const id = shape[i];
      const desc = {
        configurable: true,
        enumerable: true,
      };
      if (header === GETTER_SETTER_MAGIC) {
        reader.readUint8();
        desc.get = readValueSummary(reader, depth + 1, shapes);
        desc.set = readValueSummary(reader, depth + 1, shapes);
      } else {
        const nestedSummary = readValueSummary(reader, depth + 1, shapes);
        desc.writable = true;
        desc.value = nestedSummary;
      }
      ownProperties[id] = desc;
    }
  }

  if (hasDenseElements) {
    const elementsLength = reader.readUint32();
    if (depth < 1) {
      for (let i = 0; i < elementsLength && i < MAX_COLLECTION_VALUES; i++) {
        if (reader.peekUint8() === JSVAL_TYPE_MAGIC) {
          reader.readUint8();
          continue;
        }
        ownPropertiesLength++;
        const nestedSummary = readValueSummary(reader, depth + 1, shapes);
        ownProperties[i] = {
          configurable: true,
          enumerable: true,
          writable: true,
          value: nestedSummary,
        };
      }
    }
  }

  preview.ownProperties = ownProperties;
  preview.ownPropertiesLength = ownPropertiesLength;

  result.preview = preview;
}

function readClassFromShape(result, reader, shapes) {
  const shapeId = reader.readUint32();
  const shape = shapes[shapeId];

  if (!shape || shape.length <= 0) {
    return;
  }
  result.class = shape[0];
}

function readNodeSummary(result, reader, depth, shapes) {
  const preview = {};
  preview.kind = "DOMNode";
  preview.nodeType = reader.readUint16();
  preview.nodeName = reader.readString().toLowerCase();
  const subkindAndIsConnected = reader.readUint8();
  const subkind = subkindAndIsConnected & ~(1 << 7);
  preview.isConnected = subkindAndIsConnected >> 7;

  if (subkind === EXTERNAL_NODE_SUBKIND_ELEMENT) {
    preview.attributes = {};
    preview.attributesLength = reader.readUint32();
    for (
      let i = 0;
      i < preview.attributesLength && i < MAX_COLLECTION_VALUES;
      i++
    ) {
      const attrName = reader.readString();
      const attrVal = reader.readString();
      preview.attributes[attrName] = attrVal;
    }
  } else if (subkind === EXTERNAL_NODE_SUBKIND_ATTR) {
    preview.value = reader.readString();
  } else if (subkind === EXTERNAL_NODE_SUBKIND_DOCUMENT) {
    preview.location = reader.readString();
  } else if (subkind === EXTERNAL_NODE_SUBKIND_DOCUMENT_FRAGMENT) {
    preview.childNodesLength = reader.readUint32();
    if (depth < 1) {
      preview.childNodes = [];
      for (
        let i = 0;
        i < preview.childNodesLength && i < MAX_COLLECTION_VALUES;
        i++
      ) {
        preview.childNodes.push(readValueSummary(reader, depth + 1, shapes));
      }
    }
  } else if (
    subkind === EXTERNAL_NODE_SUBKIND_TEXT ||
    subkind === EXTERNAL_NODE_SUBKIND_COMMENT
  ) {
    preview.textContent = reader.readString();
  }

  result.preview = preview;
}

function readExceptionSummary(result, reader, _depth, _shapes) {
  result.class = "Error";
  const preview = {};
  preview.kind = "DOMException";
  preview.name = reader.readString();
  preview.message = reader.readString();
  preview.code = reader.readUint16();
  preview.result = reader.readUint32();
  preview.lineNumber = reader.readUint32();
  preview.columnNumber = reader.readUint32();
  preview.stack = reader.readString();
  result.preview = preview;
}

function readExternalObjectSummary(result, reader, depth, shapes) {
  readClassFromShape(result, reader, shapes);

  const startIndex = reader.getIndex();
  const size = reader.readUint32();
  try {
    const version = reader.readUint8();
    if (version != EXTERNAL_SUMMARY_EXPECTED_VERSION) {
      return;
    }

    const kind = reader.readUint8();
    if (kind == EXTERNAL_SUMMARY_KIND_OTHER) {
      return;
    }

    switch (kind) {
      case EXTERNAL_SUMMARY_KIND_NODE: {
        readNodeSummary(result, reader, depth, shapes);
        break;
      }
      case EXTERNAL_SUMMARY_KIND_EXCEPTION: {
        readExceptionSummary(result, reader, depth, shapes);
        break;
      }
      default:
    }
  } finally {
    reader.setIndex(startIndex + size);
  }
}

function readErrorObjectSummary(result, reader, depth, shapes) {
  const shapeId = reader.readUint32();
  const shape = shapes[shapeId];

  if (!shape || shape.length <= 0) {
    return;
  }

  const preview = {};
  result.type = "object";
  result.class = shape[0];
  result.isError = true;
  preview.kind = "Error";
  preview.name = reader.readString();
  preview.message = reader.readString();
  preview.stack = reader.readString();
  preview.fileName = reader.readString();
  preview.lineNumber = reader.readUint32();
  preview.columnNumber = reader.readUint32();
  result.preview = preview;
}

function readObjectSummary(reader, flags, depth, shapes) {
  const result = {
    type: "object",
    class: undefined,
    ownPropertyLength: 0,
    isError: false,
    extensible: false,
    sealed: false,
    frozen: false,
  };

  const kind = reader.readUint8();
  switch (kind) {
    case OBJECT_KIND_NOT_IMPLEMENTED:
      readClassFromShape(result, reader, shapes);
      break;
    case OBJECT_KIND_ARRAY_LIKE:
      readArrayLikeSummary(result, reader, depth, shapes);
      break;
    case OBJECT_KIND_MAP_LIKE:
      readMapLikeSummary(result, reader, depth, shapes);
      break;
    case OBJECT_KIND_FUNCTION:
      readFunctionSummary(result, reader);
      break;
    case OBJECT_KIND_EXTERNAL:
      readExternalObjectSummary(result, reader, depth, shapes);
      break;
    case OBJECT_KIND_ERROR:
      readErrorObjectSummary(result, reader, depth, shapes);
      break;
    case OBJECT_KIND_WRAPPED_PRIMITIVE_OBJECT: {
      result.wrappedValue = readValueSummary(reader, depth, shapes);
      readGenericObjectSummary(result, reader, flags, depth, shapes);
      break;
    }
    case OBJECT_KIND_GENERIC_OBJECT: {
      readGenericObjectSummary(result, reader, flags, depth, shapes);
      break;
    }
    case OBJECT_KIND_PROXY_OBJECT: {
      readClassFromShape(result, reader, shapes);
      result.preview = {
        kind: "Object",
        ownProperties: Object.create(null),
        ownPropertiesLength: 0,
      };
      break;
    }
    default:
      throw new Error("Bad object kind");
  }

  return result;
}

function readValueSummary(reader, depth, shapes) {
  const header = reader.readUint8();
  const type = header & 0x0f;
  const flags = (header & 0xf0) >> 4;
  switch (type) {
    case JSVAL_TYPE_DOUBLE:
      if (flags == NUMBER_IS_OUT_OF_LINE_MAGIC) {
        const value = reader.readFloat64();
        if (value === Infinity) {
          return { type: "Infinity" };
        } else if (value === -Infinity) {
          return { type: "-Infinity" };
        } else if (Number.isNaN(value)) {
          return { type: "NaN" };
        } else if (!value && 1 / value === -Infinity) {
          return { type: "-0" };
        }
        return value;
      }
      return 0;

    case JSVAL_TYPE_INT32:
      if (flags === NUMBER_IS_OUT_OF_LINE_MAGIC) {
        return reader.readInt32();
      }
      return flags + MIN_INLINE_INT;

    case JSVAL_TYPE_BOOLEAN:
      return !!flags;
    case JSVAL_TYPE_NULL:
      return { type: "null" };
    case JSVAL_TYPE_UNDEFINED:
      return { type: "undefined" };
    case JSVAL_TYPE_SYMBOL:
      if (flags & SYMBOL_NO_DESCRIPTION) {
        return {
          type: "symbol",
        };
      }
      return {
        type: "symbol",
        name: reader.readString(),
      };
    case JSVAL_TYPE_BIGINT:
      return {
        type: "BigInt",
        text: reader.readString(),
      };
    case JSVAL_TYPE_STRING: {
      return reader.readString();
    }
    case JSVAL_TYPE_OBJECT: {
      return readObjectSummary(reader, flags, depth, shapes);
    }
    default:
      throw new Error("Bad value type");
  }
}

function bufferVersion(valuesBuffer) {
  const reader = new BufferReader(valuesBuffer, 0);
  const version = reader.readUint32();
  return version;
}

function getArgumentSummaries(valuesBuffer, shapes, valuesBufferIndex) {
  if (valuesBufferIndex === ZERO_ARGUMENTS_MAGIC) {
    return [];
  }
  if (valuesBufferIndex === EXPIRED_VALUES_MAGIC) {
    return "<missing>";
  }
  const theirVersion = bufferVersion(valuesBuffer);
  if (theirVersion != EXPECTED_VALUE_SUMMARIES_VERSION) {
    throw new Error(
      `Unexpected tracer arguments buffer format. Expected ${EXPECTED_VALUE_SUMMARIES_VERSION}, received ${theirVersion}.`
    );
  }

  const reader = new BufferReader(valuesBuffer, valuesBufferIndex);
  const argc = reader.readUint32();
  const args = new Array(argc);
  for (let i = 0; i < argc && i < MAX_ARGUMENTS_TO_RECORD; i++) {
    args[i] = readValueSummary(reader, 0, shapes);
  }
  return args;
}

/**
 * Creates a filtered copy of a values buffer containing only entries at the
 * specified indices. This is useful for extracting a subset of traced function
 * call arguments from a full profiler trace.
 *
 * The source buffer format has a 4-byte version header followed by entries.
 * Each entry is preceded by a 2-byte (u16) size header indicating the total
 * byte length of that entry including the header itself.
 *
 * @param {ArrayBuffer} srcBuffer - The source values buffer from the profiler.
 * @param {Array<number|null>} entryIndices - Array of byte offsets into
 *   srcBuffer pointing to entry data (after the entry's size header). Null or
 *   negative values are skipped. These typically come from the argumentValues
 *   slot of samples in a profiler trace.
 * @returns {{valuesBuffer: ArrayBuffer, entryIndices: Array<number|null>}}
 *   An object containing:
 *   - valuesBuffer: A new ArrayBuffer with only the referenced entries
 *   - entryIndices: Updated indices pointing to the same entries in the new
 *     buffer. Duplicate source offsets map to the same destination offset.
 */
function filterValuesBufferToEntries(srcBuffer, entryIndices) {
  const theirVersion = bufferVersion(srcBuffer);
  if (theirVersion != EXPECTED_VALUE_SUMMARIES_VERSION) {
    throw new Error(
      `Unexpected tracer arguments buffer format. Expected ${EXPECTED_VALUE_SUMMARIES_VERSION}, received ${theirVersion}.`
    );
  }

  const destBuffer = new ArrayBuffer(Math.min(256, srcBuffer.byteLength), {
    maxByteLength: srcBuffer.byteLength,
  });

  const srcArray = new Uint8Array(srcBuffer);
  const destArray = new Uint8Array(destBuffer);

  const seen = new Map();
  const bufferView = new DataView(srcBuffer);
  const destEntryIndices = [...entryIndices];

  const versionHeaderSize = 4;
  let destBufferWriteOffset = 0;

  // Copy the version from the start of the buffer
  destArray.set(srcArray.subarray(0, versionHeaderSize), destBufferWriteOffset);
  destBufferWriteOffset += versionHeaderSize;

  for (let i = 0; i < destEntryIndices.length; i++) {
    if (destEntryIndices[i] === null) {
      continue;
    }

    const argumentsOffset = destEntryIndices[i];
    if (argumentsOffset < 0) {
      continue;
    }

    if (seen.has(argumentsOffset)) {
      destEntryIndices[i] = seen.get(argumentsOffset);
      continue;
    }

    // The entry header is a u16 just before the arguments containing the size
    // of the entry in bytes, including the size of the entry header itself.
    const entryHeaderSize = 2;
    const byteLengthOffset = argumentsOffset - entryHeaderSize;

    const byteLength = bufferView.getUint16(byteLengthOffset, true);

    if (destBufferWriteOffset + byteLength > destBuffer.byteLength) {
      let targetLength = destBuffer.byteLength;
      while (destBufferWriteOffset + byteLength > targetLength) {
        targetLength *= 2;
      }
      destBuffer.resize(Math.min(srcBuffer.byteLength, targetLength));
    }

    destEntryIndices[i] = destBufferWriteOffset + entryHeaderSize;
    destArray.set(
      srcArray.subarray(byteLengthOffset, byteLengthOffset + byteLength),
      destBufferWriteOffset
    );
    seen.set(argumentsOffset, destEntryIndices[i]);

    destBufferWriteOffset += byteLength;
  }

  return {
    valuesBuffer: destBuffer,
    entryIndices: destEntryIndices,
  };
}

export const ValueSummaryReader = {
  getArgumentSummaries,
  filterValuesBufferToEntries,
};
