/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import { CharCode } from 'vs/base/common/charCode';

export class ReplacePattern {

	public static fromStaticValue(value: string): ReplacePattern {
		return new ReplacePattern([ReplacePiece.staticValue(value)]);
	}

	/**
	 * Assigned when the replace pattern is entirely static.
	 */
	private readonly _staticValue: string;

	public get hasReplacementPatterns(): boolean {
		return this._staticValue === null;
	}

	/**
	 * Assigned when the replace pattern has replacemend patterns.
	 */
	private readonly _pieces: ReplacePiece[];

	constructor(pieces: ReplacePiece[]) {
		if (!pieces || pieces.length === 0) {
			this._staticValue = '';
			this._pieces = null;
		} else if (pieces.length === 1 && pieces[0].staticValue !== null) {
			this._staticValue = pieces[0].staticValue;
			this._pieces = null;
		} else {
			this._staticValue = null;
			this._pieces = pieces;
		}
	}

	public buildReplaceString(matches: string[]): string {
		if (this._staticValue !== null) {
			return this._staticValue;
		}

		let result = '';
		for (let i = 0, len = this._pieces.length; i < len; i++) {
			let piece = this._pieces[i];
			if (piece.staticValue !== null) {
				// static value ReplacePiece
				result += piece.staticValue;
				continue;
			}

			// match index ReplacePiece
			result += ReplacePattern._substitute(piece.matchIndex, matches);
		}

		return result;
	}

	private static _substitute(matchIndex: number, matches: string[]): string {
		if (matchIndex === 0) {
			return matches[0];
		}

		let remainder = '';
		while (matchIndex > 0) {
			if (matchIndex < matches.length) {
				// A match can be undefined
				let match = (matches[matchIndex] || '');
				return match + remainder;
			}
			remainder = String(matchIndex % 10) + remainder;
			matchIndex = Math.floor(matchIndex / 10);
		}
		return '$' + remainder;
	}
}

/**
 * A replace piece can either be a static string or an index to a specific match.
 */
export class ReplacePiece {

	public static staticValue(value: string): ReplacePiece {
		return new ReplacePiece(value, -1);
	}

	public static matchIndex(index: number): ReplacePiece {
		return new ReplacePiece(null, index);
	}

	public readonly staticValue: string;
	public readonly matchIndex: number;

	private constructor(staticValue: string, matchIndex: number) {
		this.staticValue = staticValue;
		this.matchIndex = matchIndex;
	}
}

class ReplacePieceBuilder {

	private readonly _source: string;
	private _lastCharIndex: number;
	private readonly _result: ReplacePiece[];
	private _resultLen: number;
	private _currentStaticPiece: string;

	constructor(source: string) {
		this._source = source;
		this._lastCharIndex = 0;
		this._result = [];
		this._resultLen = 0;
		this._currentStaticPiece = '';
	}

	public emitUnchanged(toCharIndex: number): void {
		this._emitStatic(this._source.substring(this._lastCharIndex, toCharIndex));
		this._lastCharIndex = toCharIndex;
	}

	public emitStatic(value: string, toCharIndex: number): void {
		this._emitStatic(value);
		this._lastCharIndex = toCharIndex;
	}

	private _emitStatic(value: string): void {
		if (value.length === 0) {
			return;
		}
		this._currentStaticPiece += value;
	}

	public emitMatchIndex(index: number, toCharIndex: number): void {
		if (this._currentStaticPiece.length !== 0) {
			this._result[this._resultLen++] = ReplacePiece.staticValue(this._currentStaticPiece);
			this._currentStaticPiece = '';
		}
		this._result[this._resultLen++] = ReplacePiece.matchIndex(index);
		this._lastCharIndex = toCharIndex;
	}


	public finalize(): ReplacePattern {
		this.emitUnchanged(this._source.length);
		if (this._currentStaticPiece.length !== 0) {
			this._result[this._resultLen++] = ReplacePiece.staticValue(this._currentStaticPiece);
			this._currentStaticPiece = '';
		}
		return new ReplacePattern(this._result);
	}
}

/**
 * \n			=> inserts a LF
 * \t			=> inserts a TAB
 * \\			=> inserts a "\".
 * $$			=> inserts a "$".
 * $& and $0	=> inserts the matched substring.
 * $n			=> Where n is a non-negative integer lesser than 100, inserts the nth parenthesized submatch string
 * everything else stays untouched
 *
 * Also see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_string_as_a_parameter
 */
export function parseReplaceString(replaceString: string): ReplacePattern {
	if (!replaceString || replaceString.length === 0) {
		return new ReplacePattern(null);
	}

	let result = new ReplacePieceBuilder(replaceString);

	for (let i = 0, len = replaceString.length; i < len; i++) {
		let chCode = replaceString.charCodeAt(i);

		if (chCode === CharCode.Backslash) {

			// move to next char
			i++;

			if (i >= len) {
				// string ends with a \
				break;
			}

			let nextChCode = replaceString.charCodeAt(i);
			// let replaceWithCharacter: string = null;

			switch (nextChCode) {
				case CharCode.Backslash:
					// \\ => inserts a "\"
					result.emitUnchanged(i - 1);
					result.emitStatic('\\', i + 1);
					break;
				case CharCode.n:
					// \n => inserts a LF
					result.emitUnchanged(i - 1);
					result.emitStatic('\n', i + 1);
					break;
				case CharCode.t:
					// \t => inserts a TAB
					result.emitUnchanged(i - 1);
					result.emitStatic('\t', i + 1);
					break;
			}

			continue;
		}

		if (chCode === CharCode.DollarSign) {

			// move to next char
			i++;

			if (i >= len) {
				// string ends with a $
				break;
			}

			let nextChCode = replaceString.charCodeAt(i);

			if (nextChCode === CharCode.DollarSign) {
				// $$ => inserts a "$"
				result.emitUnchanged(i - 1);
				result.emitStatic('$', i + 1);
				continue;
			}

			if (nextChCode === CharCode.Digit0 || nextChCode === CharCode.Ampersand) {
				// $& and $0 => inserts the matched substring.
				result.emitUnchanged(i - 1);
				result.emitMatchIndex(0, i + 1);
				continue;
			}

			if (CharCode.Digit1 <= nextChCode && nextChCode <= CharCode.Digit9) {
				// $n

				let matchIndex = nextChCode - CharCode.Digit0;

				// peek next char to probe for $nn
				if (i + 1 < len) {
					let nextNextChCode = replaceString.charCodeAt(i + 1);
					if (CharCode.Digit0 <= nextNextChCode && nextNextChCode <= CharCode.Digit9) {
						// $nn

						// move to next char
						i++;
						matchIndex = matchIndex * 10 + (nextNextChCode - CharCode.Digit0);

						result.emitUnchanged(i - 2);
						result.emitMatchIndex(matchIndex, i + 1);
						continue;
					}
				}

				result.emitUnchanged(i - 1);
				result.emitMatchIndex(matchIndex, i + 1);
				continue;
			}
		}
	}

	return result.finalize();
}
