/**
 * @fileoverview Utility functions to locate the source text of each code unit in the value of a string literal or template token.
 * @author Francesco Trotta
 */

"use strict";

/**
 * Represents a code unit produced by the evaluation of a JavaScript common token like a string
 * literal or template token.
 */
class CodeUnit {
	constructor(start, source) {
		this.start = start;
		this.source = source;
	}

	get end() {
		return this.start + this.length;
	}

	get length() {
		return this.source.length;
	}
}

/**
 * An object used to keep track of the position in a source text where the next characters will be read.
 */
class TextReader {
	constructor(source) {
		this.source = source;
		this.pos = 0;
	}

	/**
	 * Advances the reading position of the specified number of characters.
	 * @param {number} length Number of characters to advance.
	 * @returns {void}
	 */
	advance(length) {
		this.pos += length;
	}

	/**
	 * Reads characters from the source.
	 * @param {number} [offset=0] The offset where reading starts, relative to the current position.
	 * @param {number} [length=1] Number of characters to read.
	 * @returns {string} A substring of source characters.
	 */
	read(offset = 0, length = 1) {
		const start = offset + this.pos;

		return this.source.slice(start, start + length);
	}
}

const SIMPLE_ESCAPE_SEQUENCES = {
	__proto__: null,
	b: "\b",
	f: "\f",
	n: "\n",
	r: "\r",
	t: "\t",
	v: "\v",
};

/**
 * Reads a hex escape sequence.
 * @param {TextReader} reader The reader should be positioned on the first hexadecimal digit.
 * @param {number} length The number of hexadecimal digits.
 * @returns {string} A code unit.
 */
function readHexSequence(reader, length) {
	const str = reader.read(0, length);
	const charCode = parseInt(str, 16);

	reader.advance(length);
	return String.fromCharCode(charCode);
}

/**
 * Reads a Unicode escape sequence.
 * @param {TextReader} reader The reader should be positioned after the "u".
 * @returns {string} A code unit.
 */
function readUnicodeSequence(reader) {
	const regExp = /\{(?<hexDigits>[\dA-Fa-f]+)\}/uy;

	regExp.lastIndex = reader.pos;
	const match = regExp.exec(reader.source);

	if (match) {
		const codePoint = parseInt(match.groups.hexDigits, 16);

		reader.pos = regExp.lastIndex;
		return String.fromCodePoint(codePoint);
	}
	return readHexSequence(reader, 4);
}

/**
 * Reads an octal escape sequence.
 * @param {TextReader} reader The reader should be positioned after the first octal digit.
 * @param {number} maxLength The maximum number of octal digits.
 * @returns {string} A code unit.
 */
function readOctalSequence(reader, maxLength) {
	const [octalStr] = reader.read(-1, maxLength).match(/^[0-7]+/u);

	reader.advance(octalStr.length - 1);
	const octal = parseInt(octalStr, 8);

	return String.fromCharCode(octal);
}

/**
 * Reads an escape sequence or line continuation.
 * @param {TextReader} reader The reader should be positioned on the backslash.
 * @returns {string} A string of zero, one or two code units.
 */
function readEscapeSequenceOrLineContinuation(reader) {
	const char = reader.read(1);

	reader.advance(2);
	const unitChar = SIMPLE_ESCAPE_SEQUENCES[char];

	if (unitChar) {
		return unitChar;
	}
	switch (char) {
		case "x":
			return readHexSequence(reader, 2);
		case "u":
			return readUnicodeSequence(reader);
		case "\r":
			if (reader.read() === "\n") {
				reader.advance(1);
			}

		// fallthrough
		case "\n":
		case "\u2028":
		case "\u2029":
			return "";
		case "0":
		case "1":
		case "2":
		case "3":
			return readOctalSequence(reader, 3);
		case "4":
		case "5":
		case "6":
		case "7":
			return readOctalSequence(reader, 2);
		default:
			return char;
	}
}

/**
 * Reads an escape sequence or line continuation and generates the respective `CodeUnit` elements.
 * @param {TextReader} reader The reader should be positioned on the backslash.
 * @returns {Generator<CodeUnit>} Zero, one or two `CodeUnit` elements.
 */
function* mapEscapeSequenceOrLineContinuation(reader) {
	const start = reader.pos;
	const str = readEscapeSequenceOrLineContinuation(reader);
	const end = reader.pos;
	const source = reader.source.slice(start, end);

	switch (str.length) {
		case 0:
			break;
		case 1:
			yield new CodeUnit(start, source);
			break;
		default:
			yield new CodeUnit(start, source);
			yield new CodeUnit(start, source);
			break;
	}
}

/**
 * Parses a string literal.
 * @param {string} source The string literal to parse, including the delimiting quotes.
 * @returns {CodeUnit[]} A list of code units produced by the string literal.
 */
function parseStringLiteral(source) {
	const reader = new TextReader(source);
	const quote = reader.read();

	reader.advance(1);
	const codeUnits = [];

	for (;;) {
		const char = reader.read();

		if (char === quote) {
			break;
		}
		if (char === "\\") {
			codeUnits.push(...mapEscapeSequenceOrLineContinuation(reader));
		} else {
			codeUnits.push(new CodeUnit(reader.pos, char));
			reader.advance(1);
		}
	}
	return codeUnits;
}

/**
 * Parses a template token.
 * @param {string} source The template token to parse, including the delimiting sequences `` ` ``, `${` and `}`.
 * @returns {CodeUnit[]} A list of code units produced by the template token.
 */
function parseTemplateToken(source) {
	const reader = new TextReader(source);

	reader.advance(1);
	const codeUnits = [];

	for (;;) {
		const char = reader.read();

		if (char === "`" || (char === "$" && reader.read(1) === "{")) {
			break;
		}
		if (char === "\\") {
			codeUnits.push(...mapEscapeSequenceOrLineContinuation(reader));
		} else {
			let unitSource;

			if (char === "\r" && reader.read(1) === "\n") {
				unitSource = "\r\n";
			} else {
				unitSource = char;
			}
			codeUnits.push(new CodeUnit(reader.pos, unitSource));
			reader.advance(unitSource.length);
		}
	}
	return codeUnits;
}

module.exports = { parseStringLiteral, parseTemplateToken };
