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

export class FormattedContentBuilder {
  #lastOriginalPosition = 0;
  #formattedContent: string[] = [];
  #formattedContentLength = 0;
  #lastFormattedPosition = 0;
  #nestingLevel = 0;
  #newLines = 0;
  #enforceSpaceBetweenWords = true;
  #softSpace = false;
  #hardSpaces = 0;
  #cachedIndents = new Map<number, string>();
  #canBeIdentifierOrNumber = /[$\u200C\u200D\p{ID_Continue}]/u;

  mapping = {original: [0], formatted: [0]};

  constructor(private indentString: string) {
  }

  setEnforceSpaceBetweenWords(value: boolean): boolean {
    const oldValue = this.#enforceSpaceBetweenWords;
    this.#enforceSpaceBetweenWords = value;
    return oldValue;
  }

  addToken(token: string, offset: number): void {
    // Skip the regex check if `addSoftSpace` will be a no-op.
    if (this.#enforceSpaceBetweenWords && !this.#hardSpaces && !this.#softSpace) {
      const lastCharOfLastToken = this.#formattedContent.at(-1)?.at(-1) ?? '';
      if (this.#canBeIdentifierOrNumber.test(lastCharOfLastToken) && this.#canBeIdentifierOrNumber.test(token)) {
        this.addSoftSpace();
      }
    }

    this.#appendFormatting();

    // Insert token.
    this.#addMappingIfNeeded(offset);
    this.#addText(token);
  }

  addSoftSpace(): void {
    if (!this.#hardSpaces) {
      this.#softSpace = true;
    }
  }

  addHardSpace(): void {
    this.#softSpace = false;
    ++this.#hardSpaces;
  }

  addNewLine(noSquash?: boolean): void {
    // Avoid leading newlines.
    if (!this.#formattedContentLength) {
      return;
    }
    if (noSquash) {
      ++this.#newLines;
    } else {
      this.#newLines = this.#newLines || 1;
    }
  }

  increaseNestingLevel(): void {
    this.#nestingLevel += 1;
  }

  decreaseNestingLevel(): void {
    if (this.#nestingLevel > 0) {
      this.#nestingLevel -= 1;
    }
  }

  content(): string {
    return this.#formattedContent.join('') + (this.#newLines ? '\n' : '');
  }

  #appendFormatting(): void {
    if (this.#newLines) {
      for (let i = 0; i < this.#newLines; ++i) {
        this.#addText('\n');
      }
      this.#addText(this.#indent());
    } else if (this.#softSpace) {
      this.#addText(' ');
    }
    if (this.#hardSpaces) {
      for (let i = 0; i < this.#hardSpaces; ++i) {
        this.#addText(' ');
      }
    }
    this.#newLines = 0;
    this.#softSpace = false;
    this.#hardSpaces = 0;
  }

  #indent(): string {
    const cachedValue = this.#cachedIndents.get(this.#nestingLevel);
    if (cachedValue) {
      return cachedValue;
    }

    let fullIndent = '';
    for (let i = 0; i < this.#nestingLevel; ++i) {
      fullIndent += this.indentString;
    }

    // Cache a maximum of 20 nesting level indents.
    if (this.#nestingLevel <= 20) {
      this.#cachedIndents.set(this.#nestingLevel, fullIndent);
    }
    return fullIndent;
  }

  #addText(text: string): void {
    this.#formattedContent.push(text);
    this.#formattedContentLength += text.length;
  }

  #addMappingIfNeeded(originalPosition: number): void {
    if (originalPosition - this.#lastOriginalPosition === this.#formattedContentLength - this.#lastFormattedPosition) {
      return;
    }
    this.mapping.original.push(originalPosition);
    this.#lastOriginalPosition = originalPosition;
    this.mapping.formatted.push(this.#formattedContentLength);
    this.#lastFormattedPosition = this.#formattedContentLength;
  }
}
