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

import { findLast } from 'vs/base/common/arraysFind';
import * as strings from 'vs/base/common/strings';
import { CursorColumns } from 'vs/editor/common/core/cursorColumns';
import { IPosition, Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import type { TextModel } from 'vs/editor/common/model/textModel';
import { TextModelPart } from 'vs/editor/common/model/textModelPart';
import { computeIndentLevel } from 'vs/editor/common/model/utils';
import { ILanguageConfigurationService, ResolvedLanguageConfiguration } from 'vs/editor/common/languages/languageConfigurationRegistry';
import { BracketGuideOptions, HorizontalGuidesState, IActiveIndentGuideInfo, IGuidesTextModelPart, IndentGuide, IndentGuideHorizontalLine } from 'vs/editor/common/textModelGuides';
import { BugIndicatingError } from 'vs/base/common/errors';

export class GuidesTextModelPart extends TextModelPart implements IGuidesTextModelPart {
	constructor(
		private readonly textModel: TextModel,
		private readonly languageConfigurationService: ILanguageConfigurationService
	) {
		super();
	}

	private getLanguageConfiguration(
		languageId: string
	): ResolvedLanguageConfiguration {
		return this.languageConfigurationService.getLanguageConfiguration(
			languageId
		);
	}

	private _computeIndentLevel(lineIndex: number): number {
		return computeIndentLevel(
			this.textModel.getLineContent(lineIndex + 1),
			this.textModel.getOptions().tabSize
		);
	}

	public getActiveIndentGuide(
		lineNumber: number,
		minLineNumber: number,
		maxLineNumber: number
	): IActiveIndentGuideInfo {
		this.assertNotDisposed();
		const lineCount = this.textModel.getLineCount();

		if (lineNumber < 1 || lineNumber > lineCount) {
			throw new BugIndicatingError('Illegal value for lineNumber');
		}

		const foldingRules = this.getLanguageConfiguration(
			this.textModel.getLanguageId()
		).foldingRules;
		const offSide = Boolean(foldingRules && foldingRules.offSide);

		let up_aboveContentLineIndex =
			-2; /* -2 is a marker for not having computed it */
		let up_aboveContentLineIndent = -1;
		let up_belowContentLineIndex =
			-2; /* -2 is a marker for not having computed it */
		let up_belowContentLineIndent = -1;
		const up_resolveIndents = (lineNumber: number) => {
			if (
				up_aboveContentLineIndex !== -1 &&
				(up_aboveContentLineIndex === -2 ||
					up_aboveContentLineIndex > lineNumber - 1)
			) {
				up_aboveContentLineIndex = -1;
				up_aboveContentLineIndent = -1;

				// must find previous line with content
				for (let lineIndex = lineNumber - 2; lineIndex >= 0; lineIndex--) {
					const indent = this._computeIndentLevel(lineIndex);
					if (indent >= 0) {
						up_aboveContentLineIndex = lineIndex;
						up_aboveContentLineIndent = indent;
						break;
					}
				}
			}

			if (up_belowContentLineIndex === -2) {
				up_belowContentLineIndex = -1;
				up_belowContentLineIndent = -1;

				// must find next line with content
				for (let lineIndex = lineNumber; lineIndex < lineCount; lineIndex++) {
					const indent = this._computeIndentLevel(lineIndex);
					if (indent >= 0) {
						up_belowContentLineIndex = lineIndex;
						up_belowContentLineIndent = indent;
						break;
					}
				}
			}
		};

		let down_aboveContentLineIndex =
			-2; /* -2 is a marker for not having computed it */
		let down_aboveContentLineIndent = -1;
		let down_belowContentLineIndex =
			-2; /* -2 is a marker for not having computed it */
		let down_belowContentLineIndent = -1;
		const down_resolveIndents = (lineNumber: number) => {
			if (down_aboveContentLineIndex === -2) {
				down_aboveContentLineIndex = -1;
				down_aboveContentLineIndent = -1;

				// must find previous line with content
				for (let lineIndex = lineNumber - 2; lineIndex >= 0; lineIndex--) {
					const indent = this._computeIndentLevel(lineIndex);
					if (indent >= 0) {
						down_aboveContentLineIndex = lineIndex;
						down_aboveContentLineIndent = indent;
						break;
					}
				}
			}

			if (
				down_belowContentLineIndex !== -1 &&
				(down_belowContentLineIndex === -2 ||
					down_belowContentLineIndex < lineNumber - 1)
			) {
				down_belowContentLineIndex = -1;
				down_belowContentLineIndent = -1;

				// must find next line with content
				for (let lineIndex = lineNumber; lineIndex < lineCount; lineIndex++) {
					const indent = this._computeIndentLevel(lineIndex);
					if (indent >= 0) {
						down_belowContentLineIndex = lineIndex;
						down_belowContentLineIndent = indent;
						break;
					}
				}
			}
		};

		let startLineNumber = 0;
		let goUp = true;
		let endLineNumber = 0;
		let goDown = true;
		let indent = 0;

		let initialIndent = 0;

		for (let distance = 0; goUp || goDown; distance++) {
			const upLineNumber = lineNumber - distance;
			const downLineNumber = lineNumber + distance;

			if (distance > 1 && (upLineNumber < 1 || upLineNumber < minLineNumber)) {
				goUp = false;
			}
			if (
				distance > 1 &&
				(downLineNumber > lineCount || downLineNumber > maxLineNumber)
			) {
				goDown = false;
			}
			if (distance > 50000) {
				// stop processing
				goUp = false;
				goDown = false;
			}

			let upLineIndentLevel: number = -1;
			if (goUp && upLineNumber >= 1) {
				// compute indent level going up
				const currentIndent = this._computeIndentLevel(upLineNumber - 1);
				if (currentIndent >= 0) {
					// This line has content (besides whitespace)
					// Use the line's indent
					up_belowContentLineIndex = upLineNumber - 1;
					up_belowContentLineIndent = currentIndent;
					upLineIndentLevel = Math.ceil(
						currentIndent / this.textModel.getOptions().indentSize
					);
				} else {
					up_resolveIndents(upLineNumber);
					upLineIndentLevel = this._getIndentLevelForWhitespaceLine(
						offSide,
						up_aboveContentLineIndent,
						up_belowContentLineIndent
					);
				}
			}

			let downLineIndentLevel = -1;
			if (goDown && downLineNumber <= lineCount) {
				// compute indent level going down
				const currentIndent = this._computeIndentLevel(downLineNumber - 1);
				if (currentIndent >= 0) {
					// This line has content (besides whitespace)
					// Use the line's indent
					down_aboveContentLineIndex = downLineNumber - 1;
					down_aboveContentLineIndent = currentIndent;
					downLineIndentLevel = Math.ceil(
						currentIndent / this.textModel.getOptions().indentSize
					);
				} else {
					down_resolveIndents(downLineNumber);
					downLineIndentLevel = this._getIndentLevelForWhitespaceLine(
						offSide,
						down_aboveContentLineIndent,
						down_belowContentLineIndent
					);
				}
			}

			if (distance === 0) {
				initialIndent = upLineIndentLevel;
				continue;
			}

			if (distance === 1) {
				if (
					downLineNumber <= lineCount &&
					downLineIndentLevel >= 0 &&
					initialIndent + 1 === downLineIndentLevel
				) {
					// This is the beginning of a scope, we have special handling here, since we want the
					// child scope indent to be active, not the parent scope
					goUp = false;
					startLineNumber = downLineNumber;
					endLineNumber = downLineNumber;
					indent = downLineIndentLevel;
					continue;
				}

				if (
					upLineNumber >= 1 &&
					upLineIndentLevel >= 0 &&
					upLineIndentLevel - 1 === initialIndent
				) {
					// This is the end of a scope, just like above
					goDown = false;
					startLineNumber = upLineNumber;
					endLineNumber = upLineNumber;
					indent = upLineIndentLevel;
					continue;
				}

				startLineNumber = lineNumber;
				endLineNumber = lineNumber;
				indent = initialIndent;
				if (indent === 0) {
					// No need to continue
					return { startLineNumber, endLineNumber, indent };
				}
			}

			if (goUp) {
				if (upLineIndentLevel >= indent) {
					startLineNumber = upLineNumber;
				} else {
					goUp = false;
				}
			}
			if (goDown) {
				if (downLineIndentLevel >= indent) {
					endLineNumber = downLineNumber;
				} else {
					goDown = false;
				}
			}
		}

		return { startLineNumber, endLineNumber, indent };
	}

	public getLinesBracketGuides(
		startLineNumber: number,
		endLineNumber: number,
		activePosition: IPosition | null,
		options: BracketGuideOptions
	): IndentGuide[][] {
		const result: IndentGuide[][] = [];
		for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
			result.push([]);
		}

		// If requested, this could be made configurable.
		const includeSingleLinePairs = true;

		const bracketPairs =
			this.textModel.bracketPairs.getBracketPairsInRangeWithMinIndentation(
				new Range(
					startLineNumber,
					1,
					endLineNumber,
					this.textModel.getLineMaxColumn(endLineNumber)
				)
			).toArray();

		let activeBracketPairRange: Range | undefined = undefined;
		if (activePosition && bracketPairs.length > 0) {
			const bracketsContainingActivePosition = (
				startLineNumber <= activePosition.lineNumber &&
					activePosition.lineNumber <= endLineNumber
					// We don't need to query the brackets again if the cursor is in the viewport
					? bracketPairs
					: this.textModel.bracketPairs.getBracketPairsInRange(
						Range.fromPositions(activePosition)
					).toArray()
			).filter((bp) => Range.strictContainsPosition(bp.range, activePosition));

			activeBracketPairRange = findLast(
				bracketsContainingActivePosition,
				(i) => includeSingleLinePairs || i.range.startLineNumber !== i.range.endLineNumber
			)?.range;
		}

		const independentColorPoolPerBracketType = this.textModel.getOptions().bracketPairColorizationOptions.independentColorPoolPerBracketType;
		const colorProvider = new BracketPairGuidesClassNames();

		for (const pair of bracketPairs) {
			/*


					{
					|
					}

					{
					|
					----}

				____{
				|test
				----}

				renderHorizontalEndLineAtTheBottom:
					{
					|
					|x}
					--
				renderHorizontalEndLineAtTheBottom:
				____{
				|test
				| x }
				----
			*/

			if (!pair.closingBracketRange) {
				continue;
			}

			const isActive = activeBracketPairRange && pair.range.equalsRange(activeBracketPairRange);

			if (!isActive && !options.includeInactive) {
				continue;
			}

			const className =
				colorProvider.getInlineClassName(pair.nestingLevel, pair.nestingLevelOfEqualBracketType, independentColorPoolPerBracketType) +
				(options.highlightActive && isActive
					? ' ' + colorProvider.activeClassName
					: '');


			const start = pair.openingBracketRange.getStartPosition();
			const end = pair.closingBracketRange.getStartPosition();

			const horizontalGuides = options.horizontalGuides === HorizontalGuidesState.Enabled || (options.horizontalGuides === HorizontalGuidesState.EnabledForActive && isActive);

			if (pair.range.startLineNumber === pair.range.endLineNumber) {
				if (includeSingleLinePairs && horizontalGuides) {

					result[pair.range.startLineNumber - startLineNumber].push(
						new IndentGuide(
							-1,
							pair.openingBracketRange.getEndPosition().column,
							className,
							new IndentGuideHorizontalLine(false, end.column),
							-1,
							-1,
						)
					);

				}
				continue;
			}

			const endVisibleColumn = this.getVisibleColumnFromPosition(end);
			const startVisibleColumn = this.getVisibleColumnFromPosition(
				pair.openingBracketRange.getStartPosition()
			);
			const guideVisibleColumn = Math.min(startVisibleColumn, endVisibleColumn, pair.minVisibleColumnIndentation + 1);

			let renderHorizontalEndLineAtTheBottom = false;


			const firstNonWsIndex = strings.firstNonWhitespaceIndex(
				this.textModel.getLineContent(
					pair.closingBracketRange.startLineNumber
				)
			);
			const hasTextBeforeClosingBracket = firstNonWsIndex < pair.closingBracketRange.startColumn - 1;
			if (hasTextBeforeClosingBracket) {
				renderHorizontalEndLineAtTheBottom = true;
			}


			const visibleGuideStartLineNumber = Math.max(start.lineNumber, startLineNumber);
			const visibleGuideEndLineNumber = Math.min(end.lineNumber, endLineNumber);

			const offset = renderHorizontalEndLineAtTheBottom ? 1 : 0;

			for (let l = visibleGuideStartLineNumber; l < visibleGuideEndLineNumber + offset; l++) {
				result[l - startLineNumber].push(
					new IndentGuide(
						guideVisibleColumn,
						-1,
						className,
						null,
						l === start.lineNumber ? start.column : -1,
						l === end.lineNumber ? end.column : -1
					)
				);
			}

			if (horizontalGuides) {
				if (start.lineNumber >= startLineNumber && startVisibleColumn > guideVisibleColumn) {
					result[start.lineNumber - startLineNumber].push(
						new IndentGuide(
							guideVisibleColumn,
							-1,
							className,
							new IndentGuideHorizontalLine(false, start.column),
							-1,
							-1,
						)
					);
				}

				if (end.lineNumber <= endLineNumber && endVisibleColumn > guideVisibleColumn) {
					result[end.lineNumber - startLineNumber].push(
						new IndentGuide(
							guideVisibleColumn,
							-1,
							className,
							new IndentGuideHorizontalLine(!renderHorizontalEndLineAtTheBottom, end.column),
							-1,
							-1,
						)
					);
				}
			}
		}

		for (const guides of result) {
			guides.sort((a, b) => a.visibleColumn - b.visibleColumn);
		}

		return result;
	}

	private getVisibleColumnFromPosition(position: Position): number {
		return (
			CursorColumns.visibleColumnFromColumn(
				this.textModel.getLineContent(position.lineNumber),
				position.column,
				this.textModel.getOptions().tabSize
			) + 1
		);
	}

	public getLinesIndentGuides(
		startLineNumber: number,
		endLineNumber: number
	): number[] {
		this.assertNotDisposed();
		const lineCount = this.textModel.getLineCount();

		if (startLineNumber < 1 || startLineNumber > lineCount) {
			throw new Error('Illegal value for startLineNumber');
		}
		if (endLineNumber < 1 || endLineNumber > lineCount) {
			throw new Error('Illegal value for endLineNumber');
		}

		const options = this.textModel.getOptions();
		const foldingRules = this.getLanguageConfiguration(
			this.textModel.getLanguageId()
		).foldingRules;
		const offSide = Boolean(foldingRules && foldingRules.offSide);

		const result: number[] = new Array<number>(
			endLineNumber - startLineNumber + 1
		);

		let aboveContentLineIndex =
			-2; /* -2 is a marker for not having computed it */
		let aboveContentLineIndent = -1;

		let belowContentLineIndex =
			-2; /* -2 is a marker for not having computed it */
		let belowContentLineIndent = -1;

		for (
			let lineNumber = startLineNumber;
			lineNumber <= endLineNumber;
			lineNumber++
		) {
			const resultIndex = lineNumber - startLineNumber;

			const currentIndent = this._computeIndentLevel(lineNumber - 1);
			if (currentIndent >= 0) {
				// This line has content (besides whitespace)
				// Use the line's indent
				aboveContentLineIndex = lineNumber - 1;
				aboveContentLineIndent = currentIndent;
				result[resultIndex] = Math.ceil(currentIndent / options.indentSize);
				continue;
			}

			if (aboveContentLineIndex === -2) {
				aboveContentLineIndex = -1;
				aboveContentLineIndent = -1;

				// must find previous line with content
				for (let lineIndex = lineNumber - 2; lineIndex >= 0; lineIndex--) {
					const indent = this._computeIndentLevel(lineIndex);
					if (indent >= 0) {
						aboveContentLineIndex = lineIndex;
						aboveContentLineIndent = indent;
						break;
					}
				}
			}

			if (
				belowContentLineIndex !== -1 &&
				(belowContentLineIndex === -2 || belowContentLineIndex < lineNumber - 1)
			) {
				belowContentLineIndex = -1;
				belowContentLineIndent = -1;

				// must find next line with content
				for (let lineIndex = lineNumber; lineIndex < lineCount; lineIndex++) {
					const indent = this._computeIndentLevel(lineIndex);
					if (indent >= 0) {
						belowContentLineIndex = lineIndex;
						belowContentLineIndent = indent;
						break;
					}
				}
			}

			result[resultIndex] = this._getIndentLevelForWhitespaceLine(
				offSide,
				aboveContentLineIndent,
				belowContentLineIndent
			);
		}
		return result;
	}

	private _getIndentLevelForWhitespaceLine(
		offSide: boolean,
		aboveContentLineIndent: number,
		belowContentLineIndent: number
	): number {
		const options = this.textModel.getOptions();

		if (aboveContentLineIndent === -1 || belowContentLineIndent === -1) {
			// At the top or bottom of the file
			return 0;
		} else if (aboveContentLineIndent < belowContentLineIndent) {
			// we are inside the region above
			return 1 + Math.floor(aboveContentLineIndent / options.indentSize);
		} else if (aboveContentLineIndent === belowContentLineIndent) {
			// we are in between two regions
			return Math.ceil(belowContentLineIndent / options.indentSize);
		} else {
			if (offSide) {
				// same level as region below
				return Math.ceil(belowContentLineIndent / options.indentSize);
			} else {
				// we are inside the region that ends below
				return 1 + Math.floor(belowContentLineIndent / options.indentSize);
			}
		}
	}
}

export class BracketPairGuidesClassNames {
	public readonly activeClassName = 'indent-active';

	getInlineClassName(nestingLevel: number, nestingLevelOfEqualBracketType: number, independentColorPoolPerBracketType: boolean): string {
		return this.getInlineClassNameOfLevel(independentColorPoolPerBracketType ? nestingLevelOfEqualBracketType : nestingLevel);
	}

	getInlineClassNameOfLevel(level: number): string {
		// To support a dynamic amount of colors up to 6 colors,
		// we use a number that is a lcm of all numbers from 1 to 6.
		return `bracket-indent-guide lvl-${level % 30}`;
	}
}
