/*---------------------------------------------------------------------------------------------
 *  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 '../../../base/common/charCode.js';
import { CursorColumns } from '../core/cursorColumns.js';

export const enum Direction {
	Left,
	Right,
	Nearest,
}

export class AtomicTabMoveOperations {
	/**
	 * Get the visible column at the position. If we get to a non-whitespace character first
	 * or past the end of string then return -1.
	 *
	 * **Note** `position` and the return value are 0-based.
	 */
	public static whitespaceVisibleColumn(lineContent: string, position: number, tabSize: number): [number, number, number] {
		const lineLength = lineContent.length;
		let visibleColumn = 0;
		let prevTabStopPosition = -1;
		let prevTabStopVisibleColumn = -1;
		for (let i = 0; i < lineLength; i++) {
			if (i === position) {
				return [prevTabStopPosition, prevTabStopVisibleColumn, visibleColumn];
			}
			if (visibleColumn % tabSize === 0) {
				prevTabStopPosition = i;
				prevTabStopVisibleColumn = visibleColumn;
			}
			const chCode = lineContent.charCodeAt(i);
			switch (chCode) {
				case CharCode.Space:
					visibleColumn += 1;
					break;
				case CharCode.Tab:
					// Skip to the next multiple of tabSize.
					visibleColumn = CursorColumns.nextRenderTabStop(visibleColumn, tabSize);
					break;
				default:
					return [-1, -1, -1];
			}
		}
		if (position === lineLength) {
			return [prevTabStopPosition, prevTabStopVisibleColumn, visibleColumn];
		}
		return [-1, -1, -1];
	}

	/**
	 * Return the position that should result from a move left, right or to the
	 * nearest tab, if atomic tabs are enabled. Left and right are used for the
	 * arrow key movements, nearest is used for mouse selection. It returns
	 * -1 if atomic tabs are not relevant and you should fall back to normal
	 * behaviour.
	 *
	 * **Note**: `position` and the return value are 0-based.
	 */
	public static atomicPosition(lineContent: string, position: number, tabSize: number, direction: Direction): number {
		const lineLength = lineContent.length;

		// Get the 0-based visible column corresponding to the position, or return
		// -1 if it is not in the initial whitespace.
		const [prevTabStopPosition, prevTabStopVisibleColumn, visibleColumn] = AtomicTabMoveOperations.whitespaceVisibleColumn(lineContent, position, tabSize);

		if (visibleColumn === -1) {
			return -1;
		}

		// Is the output left or right of the current position. The case for nearest
		// where it is the same as the current position is handled in the switch.
		let left: boolean;
		switch (direction) {
			case Direction.Left:
				left = true;
				break;
			case Direction.Right:
				left = false;
				break;
			case Direction.Nearest:
				// The code below assumes the output position is either left or right
				// of the input position. If it is the same, return immediately.
				if (visibleColumn % tabSize === 0) {
					return position;
				}
				// Go to the nearest indentation.
				left = visibleColumn % tabSize <= (tabSize / 2);
				break;
		}

		// If going left, we can just use the info about the last tab stop position and
		// last tab stop visible column that we computed in the first walk over the whitespace.
		if (left) {
			if (prevTabStopPosition === -1) {
				return -1;
			}
			// If the direction is left, we need to keep scanning right to ensure
			// that targetVisibleColumn + tabSize is before non-whitespace.
			// This is so that when we press left at the end of a partial
			// indentation it only goes one character. For example '      foo' with
			// tabSize 4, should jump from position 6 to position 5, not 4.
			let currentVisibleColumn = prevTabStopVisibleColumn;
			for (let i = prevTabStopPosition; i < lineLength; ++i) {
				if (currentVisibleColumn === prevTabStopVisibleColumn + tabSize) {
					// It is a full indentation.
					return prevTabStopPosition;
				}

				const chCode = lineContent.charCodeAt(i);
				switch (chCode) {
					case CharCode.Space:
						currentVisibleColumn += 1;
						break;
					case CharCode.Tab:
						currentVisibleColumn = CursorColumns.nextRenderTabStop(currentVisibleColumn, tabSize);
						break;
					default:
						return -1;
				}
			}
			if (currentVisibleColumn === prevTabStopVisibleColumn + tabSize) {
				return prevTabStopPosition;
			}
			// It must have been a partial indentation.
			return -1;
		}

		// We are going right.
		const targetVisibleColumn = CursorColumns.nextRenderTabStop(visibleColumn, tabSize);

		// We can just continue from where whitespaceVisibleColumn got to.
		let currentVisibleColumn = visibleColumn;
		for (let i = position; i < lineLength; i++) {
			if (currentVisibleColumn === targetVisibleColumn) {
				return i;
			}

			const chCode = lineContent.charCodeAt(i);
			switch (chCode) {
				case CharCode.Space:
					currentVisibleColumn += 1;
					break;
				case CharCode.Tab:
					currentVisibleColumn = CursorColumns.nextRenderTabStop(currentVisibleColumn, tabSize);
					break;
				default:
					return -1;
			}
		}
		// This condition handles when the target column is at the end of the line.
		if (currentVisibleColumn === targetVisibleColumn) {
			return lineLength;
		}
		return -1;
	}
}
