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

import * as strings from 'vs/base/common/strings';
import { Constants } from 'vs/base/common/uint';
import { LinePartMetadata } from 'vs/editor/common/viewLayout/linePart';
import { InlineDecoration, InlineDecorationType } from 'vs/editor/common/viewModel';

export class LineDecoration {
	_lineDecorationBrand: void = undefined;

	constructor(
		public readonly startColumn: number,
		public readonly endColumn: number,
		public readonly className: string,
		public readonly type: InlineDecorationType
	) {
	}

	private static _equals(a: LineDecoration, b: LineDecoration): boolean {
		return (
			a.startColumn === b.startColumn
			&& a.endColumn === b.endColumn
			&& a.className === b.className
			&& a.type === b.type
		);
	}

	public static equalsArr(a: LineDecoration[], b: LineDecoration[]): boolean {
		const aLen = a.length;
		const bLen = b.length;
		if (aLen !== bLen) {
			return false;
		}
		for (let i = 0; i < aLen; i++) {
			if (!LineDecoration._equals(a[i], b[i])) {
				return false;
			}
		}
		return true;
	}

	public static extractWrapped(arr: LineDecoration[], startOffset: number, endOffset: number): LineDecoration[] {
		if (arr.length === 0) {
			return arr;
		}
		const startColumn = startOffset + 1;
		const endColumn = endOffset + 1;
		const lineLength = endOffset - startOffset;
		const r = [];
		let rLength = 0;
		for (const dec of arr) {
			if (dec.endColumn <= startColumn || dec.startColumn >= endColumn) {
				continue;
			}
			r[rLength++] = new LineDecoration(Math.max(1, dec.startColumn - startColumn + 1), Math.min(lineLength + 1, dec.endColumn - startColumn + 1), dec.className, dec.type);
		}
		return r;
	}

	public static filter(lineDecorations: InlineDecoration[], lineNumber: number, minLineColumn: number, maxLineColumn: number): LineDecoration[] {
		if (lineDecorations.length === 0) {
			return [];
		}

		const result: LineDecoration[] = [];
		let resultLen = 0;

		for (let i = 0, len = lineDecorations.length; i < len; i++) {
			const d = lineDecorations[i];
			const range = d.range;

			if (range.endLineNumber < lineNumber || range.startLineNumber > lineNumber) {
				// Ignore decorations that sit outside this line
				continue;
			}

			if (range.isEmpty() && (d.type === InlineDecorationType.Regular || d.type === InlineDecorationType.RegularAffectingLetterSpacing)) {
				// Ignore empty range decorations
				continue;
			}

			const startColumn = (range.startLineNumber === lineNumber ? range.startColumn : minLineColumn);
			const endColumn = (range.endLineNumber === lineNumber ? range.endColumn : maxLineColumn);

			result[resultLen++] = new LineDecoration(startColumn, endColumn, d.inlineClassName, d.type);
		}

		return result;
	}

	private static _typeCompare(a: InlineDecorationType, b: InlineDecorationType): number {
		const ORDER = [2, 0, 1, 3];
		return ORDER[a] - ORDER[b];
	}

	public static compare(a: LineDecoration, b: LineDecoration): number {
		if (a.startColumn !== b.startColumn) {
			return a.startColumn - b.startColumn;
		}

		if (a.endColumn !== b.endColumn) {
			return a.endColumn - b.endColumn;
		}

		const typeCmp = LineDecoration._typeCompare(a.type, b.type);
		if (typeCmp !== 0) {
			return typeCmp;
		}

		if (a.className !== b.className) {
			return a.className < b.className ? -1 : 1;
		}

		return 0;
	}
}

export class DecorationSegment {
	startOffset: number;
	endOffset: number;
	className: string;
	metadata: number;

	constructor(startOffset: number, endOffset: number, className: string, metadata: number) {
		this.startOffset = startOffset;
		this.endOffset = endOffset;
		this.className = className;
		this.metadata = metadata;
	}
}

class Stack {
	public count: number;
	private readonly stopOffsets: number[];
	private readonly classNames: string[];
	private readonly metadata: number[];

	constructor() {
		this.stopOffsets = [];
		this.classNames = [];
		this.metadata = [];
		this.count = 0;
	}

	private static _metadata(metadata: number[]): number {
		let result = 0;
		for (let i = 0, len = metadata.length; i < len; i++) {
			result |= metadata[i];
		}
		return result;
	}

	public consumeLowerThan(maxStopOffset: number, nextStartOffset: number, result: DecorationSegment[]): number {

		while (this.count > 0 && this.stopOffsets[0] < maxStopOffset) {
			let i = 0;

			// Take all equal stopping offsets
			while (i + 1 < this.count && this.stopOffsets[i] === this.stopOffsets[i + 1]) {
				i++;
			}

			// Basically we are consuming the first i + 1 elements of the stack
			result.push(new DecorationSegment(nextStartOffset, this.stopOffsets[i], this.classNames.join(' '), Stack._metadata(this.metadata)));
			nextStartOffset = this.stopOffsets[i] + 1;

			// Consume them
			this.stopOffsets.splice(0, i + 1);
			this.classNames.splice(0, i + 1);
			this.metadata.splice(0, i + 1);
			this.count -= (i + 1);
		}

		if (this.count > 0 && nextStartOffset < maxStopOffset) {
			result.push(new DecorationSegment(nextStartOffset, maxStopOffset - 1, this.classNames.join(' '), Stack._metadata(this.metadata)));
			nextStartOffset = maxStopOffset;
		}

		return nextStartOffset;
	}

	public insert(stopOffset: number, className: string, metadata: number): void {
		if (this.count === 0 || this.stopOffsets[this.count - 1] <= stopOffset) {
			// Insert at the end
			this.stopOffsets.push(stopOffset);
			this.classNames.push(className);
			this.metadata.push(metadata);
		} else {
			// Find the insertion position for `stopOffset`
			for (let i = 0; i < this.count; i++) {
				if (this.stopOffsets[i] >= stopOffset) {
					this.stopOffsets.splice(i, 0, stopOffset);
					this.classNames.splice(i, 0, className);
					this.metadata.splice(i, 0, metadata);
					break;
				}
			}
		}
		this.count++;
		return;
	}
}

export class LineDecorationsNormalizer {
	/**
	 * Normalize line decorations. Overlapping decorations will generate multiple segments
	 */
	public static normalize(lineContent: string, lineDecorations: LineDecoration[]): DecorationSegment[] {
		if (lineDecorations.length === 0) {
			return [];
		}

		const result: DecorationSegment[] = [];

		const stack = new Stack();
		let nextStartOffset = 0;

		for (let i = 0, len = lineDecorations.length; i < len; i++) {
			const d = lineDecorations[i];
			let startColumn = d.startColumn;
			let endColumn = d.endColumn;
			const className = d.className;
			const metadata = (
				d.type === InlineDecorationType.Before
					? LinePartMetadata.PSEUDO_BEFORE
					: d.type === InlineDecorationType.After
						? LinePartMetadata.PSEUDO_AFTER
						: 0
			);

			// If the position would end up in the middle of a high-low surrogate pair, we move it to before the pair
			if (startColumn > 1) {
				const charCodeBefore = lineContent.charCodeAt(startColumn - 2);
				if (strings.isHighSurrogate(charCodeBefore)) {
					startColumn--;
				}
			}

			if (endColumn > 1) {
				const charCodeBefore = lineContent.charCodeAt(endColumn - 2);
				if (strings.isHighSurrogate(charCodeBefore)) {
					endColumn--;
				}
			}

			const currentStartOffset = startColumn - 1;
			const currentEndOffset = endColumn - 2;

			nextStartOffset = stack.consumeLowerThan(currentStartOffset, nextStartOffset, result);

			if (stack.count === 0) {
				nextStartOffset = currentStartOffset;
			}
			stack.insert(currentEndOffset, className, metadata);
		}

		stack.consumeLowerThan(Constants.MAX_SAFE_SMALL_INTEGER, nextStartOffset, result);

		return result;
	}

}
