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

import * as editorCommon from 'vs/editor/common/editorCommon';
import { LineToken, StandardTokenType } from 'vs/editor/common/core/lineTokens';
import { Position } from 'vs/editor/common/core/position';

class TokenInfo implements editorCommon.ITokenInfo {
	_tokenInfoBrand: void;

	readonly _actual: LineToken;
	public readonly lineNumber: number;
	public readonly startColumn: number;
	public readonly endColumn: number;
	public readonly standardType: StandardTokenType;

	constructor(actual: LineToken, lineNumber: number) {
		this._actual = actual;
		this.lineNumber = lineNumber;
		this.startColumn = this._actual.startOffset + 1;
		this.endColumn = this._actual.endOffset + 1;
		this.standardType = this._actual.standardType;
	}
}

function findClosestNonEmptyLine(model: editorCommon.ITokenizedModel, position: Position): Position {
	const lineNumber = position.lineNumber;
	if (model.getLineMaxColumn(lineNumber) !== 1) {
		return position;
	}

	const lineCount = model.getLineCount();

	// we need to go up or down
	let distance = 1;
	while (true) {
		let aboveLineNumber = lineNumber - distance;
		let belowLineNumber = lineNumber + distance;

		if (aboveLineNumber < 1 && belowLineNumber > lineCount) {
			// No more lines above or below
			break;
		}

		if (aboveLineNumber >= 1) {
			let aboveMaxColumn = model.getLineMaxColumn(aboveLineNumber);
			if (aboveMaxColumn !== 1) {
				// bingo!
				return new Position(aboveLineNumber, aboveMaxColumn);
			}
		}

		if (belowLineNumber <= lineCount) {
			let belowMaxColumn = model.getLineMaxColumn(belowLineNumber);
			if (belowMaxColumn !== 1) {
				// bingo!
				return new Position(belowLineNumber, 1);
			}
		}

		distance++;
	}
	return null;
}

export class TokenIterator implements editorCommon.ITokenIterator {

	private _model: editorCommon.ITokenizedModel;
	private _lineCount: number;
	private _prev: TokenInfo;
	private _next: TokenInfo;

	constructor(model: editorCommon.ITokenizedModel, position: Position) {
		this._model = model;
		this._lineCount = this._model.getLineCount();
		this._prev = null;
		this._next = null;

		position = findClosestNonEmptyLine(model, position);
		if (position) {
			let lineTokens = this._model.getLineTokens(position.lineNumber);
			let currentToken = lineTokens.findTokenAtOffset(position.column - 1);
			if (currentToken) {
				this._prev = this._next = new TokenInfo(currentToken, position.lineNumber);
			}
		}
	}

	private _advanceNext(): void {
		if (!this._next) {
			return;
		}

		let lineNumber = this._next.lineNumber;
		let next = this._next._actual.next();
		while (!next && lineNumber < this._lineCount) {
			lineNumber++;
			let currentLineTokens = this._model.getLineTokens(lineNumber);
			next = currentLineTokens.firstToken();
		}

		this._prev = this._next;
		if (next) {
			this._next = new TokenInfo(next, lineNumber);
		} else {
			this._next = null;
		}
	}

	private _advancePrev(): void {
		if (!this._prev) {
			return;
		}

		let lineNumber = this._prev.lineNumber;
		let prev = this._prev._actual.prev();
		while (!prev && lineNumber > 1) {
			lineNumber--;
			let currentLineTokens = this._model.getLineTokens(lineNumber);
			prev = currentLineTokens.lastToken();
		}

		this._next = this._prev;
		if (prev) {
			this._prev = new TokenInfo(prev, lineNumber);
		} else {
			this._prev = null;
		}
	}

	public hasNext(): boolean {
		return this._next !== null;
	}

	public next(): editorCommon.ITokenInfo {
		const result = this._next;
		this._advanceNext();
		return result;
	}

	public hasPrev(): boolean {
		return this._prev !== null;
	}

	public prev(): editorCommon.ITokenInfo {
		const result = this._prev;
		this._advancePrev();
		return result;
	}

	public _invalidate() {
		// replace all public functions with errors
		var errorFn = function (): any {
			throw new Error('iteration isn\'t valid anymore');
		};
		this.hasNext = errorFn;
		this.next = errorFn;
		this.hasPrev = errorFn;
		this.prev = errorFn;
	}
}
