// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import * as Platform from '../../core/platform/platform.js';
import * as TextUtils from '../../models/text_utils/text_utils.js';

import {type HeapSnapshotHeader, HeapSnapshotProgress, JSHeapSnapshot, type Profile} from './HeapSnapshot.js';
import type {HeapSnapshotWorkerDispatcher} from './HeapSnapshotWorkerDispatcher.js';

export class HeapSnapshotLoader {
  readonly #progress: HeapSnapshotProgress;
  #buffer: string[];
  #dataCallback: ((value: string|PromiseLike<string>) => void)|null;
  #done: boolean;
  // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  #snapshot?: Record<string, any>;
  #array!: Platform.TypedArrayUtilities.BigUint32Array|null;
  #arrayIndex!: number;
  #json = '';
  parsingComplete: Promise<void>;
  constructor(dispatcher: HeapSnapshotWorkerDispatcher) {
    this.#reset();
    this.#progress = new HeapSnapshotProgress(dispatcher);
    this.#buffer = [];
    this.#dataCallback = null;
    this.#done = false;
    this.parsingComplete = this.#parseInput();
  }

  dispose(): void {
    this.#reset();
  }

  #reset(): void {
    this.#json = '';
    this.#snapshot = undefined;
  }

  close(): void {
    this.#done = true;
    if (this.#dataCallback) {
      this.#dataCallback('');
    }
  }

  async buildSnapshot(secondWorker: MessagePort): Promise<JSHeapSnapshot> {
    await this.parsingComplete;

    this.#snapshot = this.#snapshot || {};

    this.#progress.updateStatus('Processing snapshot…');
    const result = new JSHeapSnapshot((this.#snapshot as Profile), this.#progress);
    await result.initialize(secondWorker);
    this.#reset();
    return result;
  }

  #parseUintArray(): boolean {
    let index = 0;
    const char0 = '0'.charCodeAt(0);
    const char9 = '9'.charCodeAt(0);
    const closingBracket = ']'.charCodeAt(0);
    const length = this.#json.length;
    while (true) {
      while (index < length) {
        const code = this.#json.charCodeAt(index);
        if (char0 <= code && code <= char9) {
          break;
        } else if (code === closingBracket) {
          this.#json = this.#json.slice(index + 1);
          return false;
        }
        ++index;
      }
      if (index === length) {
        this.#json = '';
        return true;
      }
      let nextNumber = 0;
      const startIndex = index;
      while (index < length) {
        const code = this.#json.charCodeAt(index);
        if (char0 > code || code > char9) {
          break;
        }
        nextNumber *= 10;
        nextNumber += (code - char0);
        ++index;
      }
      if (index === length) {
        this.#json = this.#json.slice(startIndex);
        return true;
      }
      if (!this.#array) {
        throw new Error('Array not instantiated');
      }
      this.#array.setValue(this.#arrayIndex++, nextNumber);
    }
  }

  #parseStringsArray(): void {
    this.#progress.updateStatus('Parsing strings…');
    const closingBracketIndex = this.#json.lastIndexOf(']');
    if (closingBracketIndex === -1) {
      throw new Error('Incomplete JSON');
    }
    this.#json = this.#json.slice(0, closingBracketIndex + 1);

    if (!this.#snapshot) {
      throw new Error('No snapshot in parseStringsArray');
    }
    this.#snapshot.strings = JSON.parse(this.#json);
  }

  write(chunk: string): void {
    this.#buffer.push(chunk);
    if (!this.#dataCallback) {
      return;
    }
    this.#dataCallback(this.#buffer.shift() as string);
    this.#dataCallback = null;
  }

  #fetchChunk(): Promise<string> {
    // This method shoudln't be entered more than once since parsing happens
    // sequentially. This means it's fine to stash away a single #dataCallback
    // instead of an array of them.
    if (this.#buffer.length > 0) {
      return Promise.resolve(this.#buffer.shift() as string);
    }

    const {promise, resolve} = Promise.withResolvers<string>();
    this.#dataCallback = resolve;
    return promise;
  }

  async #findToken(token: string, startIndex?: number): Promise<number> {
    while (true) {
      const pos = this.#json.indexOf(token, startIndex || 0);
      if (pos !== -1) {
        return pos;
      }
      startIndex = this.#json.length - token.length + 1;
      this.#json += await this.#fetchChunk();
    }
  }

  async #parseArray(name: string, title: string, length?: number):
      Promise<Platform.TypedArrayUtilities.BigUint32Array> {
    const nameIndex = await this.#findToken(name);
    const bracketIndex = await this.#findToken('[', nameIndex);
    this.#json = this.#json.slice(bracketIndex + 1);
    this.#array = length === undefined ? Platform.TypedArrayUtilities.createExpandableBigUint32Array() :
                                         Platform.TypedArrayUtilities.createFixedBigUint32Array(length);
    this.#arrayIndex = 0;
    while (this.#parseUintArray()) {
      if (length) {
        this.#progress.updateProgress(title, this.#arrayIndex, this.#array.length);
      } else {
        this.#progress.updateStatus(title);
      }
      this.#json += await this.#fetchChunk();
    }
    const result = this.#array;
    this.#array = null;
    return result;
  }

  async #parseInput(): Promise<void> {
    const snapshotToken = '"snapshot"';
    const snapshotTokenIndex = await this.#findToken(snapshotToken);
    if (snapshotTokenIndex === -1) {
      throw new Error('Snapshot token not found');
    }

    this.#progress.updateStatus('Loading snapshot info…');
    const json = this.#json.slice(snapshotTokenIndex + snapshotToken.length + 1);
    let jsonTokenizerDone = false;
    const jsonTokenizer = new TextUtils.TextUtils.BalancedJSONTokenizer(metaJSON => {
      this.#json = jsonTokenizer.remainder();
      jsonTokenizerDone = true;

      this.#snapshot = this.#snapshot || {};
      this.#snapshot.snapshot = (JSON.parse(metaJSON) as HeapSnapshotHeader);
    });
    jsonTokenizer.write(json);
    while (!jsonTokenizerDone) {
      jsonTokenizer.write(await this.#fetchChunk());
    }

    this.#snapshot = this.#snapshot || {};
    const nodes = await this.#parseArray(
        '"nodes"', 'Loading nodes… {PH1}%',
        this.#snapshot.snapshot.meta.node_fields.length * this.#snapshot.snapshot.node_count);
    this.#snapshot.nodes = nodes;

    const edges = await this.#parseArray(
        '"edges"', 'Loading edges… {PH1}%',
        this.#snapshot.snapshot.meta.edge_fields.length * this.#snapshot.snapshot.edge_count);
    this.#snapshot.edges = edges;

    if (this.#snapshot.snapshot.trace_function_count) {
      const traceFunctionInfos = await this.#parseArray(
          '"trace_function_infos"', 'Loading allocation traces… {PH1}%',
          this.#snapshot.snapshot.meta.trace_function_info_fields.length *
              this.#snapshot.snapshot.trace_function_count);
      this.#snapshot.trace_function_infos = traceFunctionInfos.asUint32ArrayOrFail();

      const thisTokenEndIndex = await this.#findToken(':');
      const nextTokenIndex = await this.#findToken('"', thisTokenEndIndex);
      const openBracketIndex = this.#json.indexOf('[');
      const closeBracketIndex = this.#json.lastIndexOf(']', nextTokenIndex);
      this.#snapshot.trace_tree = JSON.parse(this.#json.substring(openBracketIndex, closeBracketIndex + 1));
      this.#json = this.#json.slice(closeBracketIndex + 1);
    }

    if (this.#snapshot.snapshot.meta.sample_fields) {
      const samples = await this.#parseArray('"samples"', 'Loading samples…');
      this.#snapshot.samples = samples.asArrayOrFail();
    }

    if (this.#snapshot.snapshot.meta['location_fields']) {
      const locations = await this.#parseArray('"locations"', 'Loading locations…');
      this.#snapshot.locations = locations.asArrayOrFail();
    } else {
      this.#snapshot.locations = [];
    }

    this.#progress.updateStatus('Loading strings…');
    const stringsTokenIndex = await this.#findToken('"strings"');
    const bracketIndex = await this.#findToken('[', stringsTokenIndex);
    this.#json = this.#json.slice(bracketIndex);
    while (this.#buffer.length > 0 || !this.#done) {
      this.#json += await this.#fetchChunk();
    }
    this.#parseStringsArray();
  }
}
