/*---------------------------------------------------------------------------------------------
 *  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 strings from 'vs/base/common/strings';
import { WrappingIndent } from 'vs/editor/common/editorCommon';
import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer';
import { ILineMapperFactory, ILineMapping, OutputPosition } from 'vs/editor/common/viewModel/splitLinesCollection';
import { CharCode } from 'vs/base/common/charCode';
import { CharacterClassifier } from 'vs/editor/common/core/characterClassifier';

const enum CharacterClass {
	NONE = 0,
	BREAK_BEFORE = 1,
	BREAK_AFTER = 2,
	BREAK_OBTRUSIVE = 3,
	BREAK_IDEOGRAPHIC = 4 // for Han and Kana.
}

class WrappingCharacterClassifier extends CharacterClassifier<CharacterClass> {

	constructor(BREAK_BEFORE: string, BREAK_AFTER: string, BREAK_OBTRUSIVE: string) {
		super(CharacterClass.NONE);

		for (let i = 0; i < BREAK_BEFORE.length; i++) {
			this.set(BREAK_BEFORE.charCodeAt(i), CharacterClass.BREAK_BEFORE);
		}

		for (let i = 0; i < BREAK_AFTER.length; i++) {
			this.set(BREAK_AFTER.charCodeAt(i), CharacterClass.BREAK_AFTER);
		}

		for (let i = 0; i < BREAK_OBTRUSIVE.length; i++) {
			this.set(BREAK_OBTRUSIVE.charCodeAt(i), CharacterClass.BREAK_OBTRUSIVE);
		}
	}

	public get(charCode: number): CharacterClass {
		// Initialize CharacterClass.BREAK_IDEOGRAPHIC for these Unicode ranges:
		// 1. CJK Unified Ideographs (0x4E00 -- 0x9FFF)
		// 2. CJK Unified Ideographs Extension A (0x3400 -- 0x4DBF)
		// 3. Hiragana and Katakana (0x3040 -- 0x30FF)
		if (
			(charCode >= 0x3040 && charCode <= 0x30FF)
			|| (charCode >= 0x3400 && charCode <= 0x4DBF)
			|| (charCode >= 0x4E00 && charCode <= 0x9FFF)
		) {
			return CharacterClass.BREAK_IDEOGRAPHIC;
		}

		return super.get(charCode);
	}
}

export class CharacterHardWrappingLineMapperFactory implements ILineMapperFactory {

	private classifier: WrappingCharacterClassifier;

	constructor(breakBeforeChars: string, breakAfterChars: string, breakObtrusiveChars: string) {
		this.classifier = new WrappingCharacterClassifier(breakBeforeChars, breakAfterChars, breakObtrusiveChars);
	}

	// TODO@Alex -> duplicated in lineCommentCommand
	private static nextVisibleColumn(currentVisibleColumn: number, tabSize: number, isTab: boolean, columnSize: number): number {
		currentVisibleColumn = +currentVisibleColumn; //@perf
		tabSize = +tabSize; //@perf
		columnSize = +columnSize; //@perf

		if (isTab) {
			return currentVisibleColumn + (tabSize - (currentVisibleColumn % tabSize));
		}
		return currentVisibleColumn + columnSize;
	}

	public createLineMapping(lineText: string, tabSize: number, breakingColumn: number, columnsForFullWidthChar: number, hardWrappingIndent: WrappingIndent): ILineMapping {
		if (breakingColumn === -1) {
			return null;
		}

		tabSize = +tabSize; //@perf
		breakingColumn = +breakingColumn; //@perf
		columnsForFullWidthChar = +columnsForFullWidthChar; //@perf
		hardWrappingIndent = +hardWrappingIndent; //@perf

		let wrappedTextIndentVisibleColumn = 0;
		let wrappedTextIndent = '';

		let firstNonWhitespaceIndex = -1;
		if (hardWrappingIndent !== WrappingIndent.None) {
			firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineText);
			if (firstNonWhitespaceIndex !== -1) {
				wrappedTextIndent = lineText.substring(0, firstNonWhitespaceIndex);
				for (let i = 0; i < firstNonWhitespaceIndex; i++) {
					wrappedTextIndentVisibleColumn = CharacterHardWrappingLineMapperFactory.nextVisibleColumn(wrappedTextIndentVisibleColumn, tabSize, lineText.charCodeAt(i) === CharCode.Tab, 1);
				}
				if (hardWrappingIndent === WrappingIndent.Indent) {
					wrappedTextIndent += '\t';
					wrappedTextIndentVisibleColumn = CharacterHardWrappingLineMapperFactory.nextVisibleColumn(wrappedTextIndentVisibleColumn, tabSize, true, 1);
				}
				// Force sticking to beginning of line if indentColumn > 66% breakingColumn
				if (wrappedTextIndentVisibleColumn > 1 / 2 * breakingColumn) {
					wrappedTextIndent = '';
					wrappedTextIndentVisibleColumn = 0;
				}
			}
		}

		let classifier = this.classifier;
		let lastBreakingOffset = 0; // Last 0-based offset in the lineText at which a break happened
		let breakingLengths: number[] = []; // The length of each broken-up line text
		let breakingLengthsIndex: number = 0; // The count of breaks already done
		let visibleColumn = 0; // Visible column since the beginning of the current line
		let niceBreakOffset = -1; // Last index of a character that indicates a break should happen before it (more desirable)
		let niceBreakVisibleColumn = 0; // visible column if a break were to be later introduced before `niceBreakOffset`
		let obtrusiveBreakOffset = -1; // Last index of a character that indicates a break should happen before it (less desirable)
		let obtrusiveBreakVisibleColumn = 0; // visible column if a break were to be later introduced before `obtrusiveBreakOffset`
		let len = lineText.length;

		for (let i = 0; i < len; i++) {
			// At this point, there is a certainty that the character before `i` fits on the current line,
			// but the character at `i` might not fit

			let charCode = lineText.charCodeAt(i);
			let charCodeIsTab = (charCode === CharCode.Tab);
			let charCodeClass = classifier.get(charCode);

			if (charCodeClass === CharacterClass.BREAK_BEFORE) {
				// This is a character that indicates that a break should happen before it
				// Since we are certain the character before `i` fits, there's no extra checking needed,
				// just mark it as a nice breaking opportunity
				niceBreakOffset = i;
				niceBreakVisibleColumn = wrappedTextIndentVisibleColumn;
			}

			// CJK breaking : before break
			if (charCodeClass === CharacterClass.BREAK_IDEOGRAPHIC && i > 0) {
				let prevCode = lineText.charCodeAt(i - 1);
				let prevClass = classifier.get(prevCode);
				if (prevClass !== CharacterClass.BREAK_BEFORE) { // Kinsoku Shori: Don't break after a leading character, like an open bracket
					niceBreakOffset = i;
					niceBreakVisibleColumn = wrappedTextIndentVisibleColumn;
				}
			}

			let charColumnSize = 1;
			if (strings.isFullWidthCharacter(charCode)) {
				charColumnSize = columnsForFullWidthChar;
			}

			// Advance visibleColumn with character at `i`
			visibleColumn = CharacterHardWrappingLineMapperFactory.nextVisibleColumn(visibleColumn, tabSize, charCodeIsTab, charColumnSize);

			if (visibleColumn > breakingColumn && i !== 0) {
				// We need to break at least before character at `i`:
				//  - break before niceBreakLastOffset if it exists (and re-establish a correct visibleColumn by using niceBreakVisibleColumn + charAt(i))
				//  - otherwise, break before obtrusiveBreakLastOffset if it exists (and re-establish a correct visibleColumn by using obtrusiveBreakVisibleColumn + charAt(i))
				//  - otherwise, break before i (and re-establish a correct visibleColumn by charAt(i))

				let breakBeforeOffset: number;
				let restoreVisibleColumnFrom: number;

				if (niceBreakOffset !== -1) {

					// We will break before `niceBreakLastOffset`
					breakBeforeOffset = niceBreakOffset;
					restoreVisibleColumnFrom = niceBreakVisibleColumn;

				} else if (obtrusiveBreakOffset !== -1) {

					// We will break before `obtrusiveBreakLastOffset`
					breakBeforeOffset = obtrusiveBreakOffset;
					restoreVisibleColumnFrom = obtrusiveBreakVisibleColumn;

				} else {

					// We will break before `i`
					breakBeforeOffset = i;
					restoreVisibleColumnFrom = wrappedTextIndentVisibleColumn;

				}

				// Break before character at `breakBeforeOffset`
				breakingLengths[breakingLengthsIndex++] = breakBeforeOffset - lastBreakingOffset;
				lastBreakingOffset = breakBeforeOffset;

				// Re-establish visibleColumn by taking character at `i` into account
				visibleColumn = CharacterHardWrappingLineMapperFactory.nextVisibleColumn(restoreVisibleColumnFrom, tabSize, charCodeIsTab, charColumnSize);

				// Reset markers
				niceBreakOffset = -1;
				niceBreakVisibleColumn = 0;
				obtrusiveBreakOffset = -1;
				obtrusiveBreakVisibleColumn = 0;
			}

			// At this point, there is a certainty that the character at `i` fits on the current line

			if (niceBreakOffset !== -1) {
				// Advance niceBreakVisibleColumn
				niceBreakVisibleColumn = CharacterHardWrappingLineMapperFactory.nextVisibleColumn(niceBreakVisibleColumn, tabSize, charCodeIsTab, charColumnSize);
			}
			if (obtrusiveBreakOffset !== -1) {
				// Advance obtrusiveBreakVisibleColumn
				obtrusiveBreakVisibleColumn = CharacterHardWrappingLineMapperFactory.nextVisibleColumn(obtrusiveBreakVisibleColumn, tabSize, charCodeIsTab, charColumnSize);
			}

			if (charCodeClass === CharacterClass.BREAK_AFTER && (hardWrappingIndent === WrappingIndent.None || i >= firstNonWhitespaceIndex)) {
				// This is a character that indicates that a break should happen after it
				niceBreakOffset = i + 1;
				niceBreakVisibleColumn = wrappedTextIndentVisibleColumn;
			}

			// CJK breaking : after break
			if (charCodeClass === CharacterClass.BREAK_IDEOGRAPHIC && i < len - 1) {
				let nextCode = lineText.charCodeAt(i + 1);
				let nextClass = classifier.get(nextCode);
				if (nextClass !== CharacterClass.BREAK_AFTER) { // Kinsoku Shori: Don't break before a trailing character, like a period
					niceBreakOffset = i + 1;
					niceBreakVisibleColumn = wrappedTextIndentVisibleColumn;
				}
			}

			if (charCodeClass === CharacterClass.BREAK_OBTRUSIVE) {
				// This is an obtrusive character that indicates that a break should happen after it
				obtrusiveBreakOffset = i + 1;
				obtrusiveBreakVisibleColumn = wrappedTextIndentVisibleColumn;
			}
		}

		if (breakingLengthsIndex === 0) {
			return null;
		}

		// Add last segment
		breakingLengths[breakingLengthsIndex++] = len - lastBreakingOffset;

		return new CharacterHardWrappingLineMapping(new PrefixSumComputer(breakingLengths), wrappedTextIndent);
	}
}

export class CharacterHardWrappingLineMapping implements ILineMapping {

	private _prefixSums: PrefixSumComputer;
	private _wrappedLinesIndent: string;

	constructor(prefixSums: PrefixSumComputer, wrappedLinesIndent: string) {
		this._prefixSums = prefixSums;
		this._wrappedLinesIndent = wrappedLinesIndent;
	}

	public getOutputLineCount(): number {
		return this._prefixSums.getCount();
	}

	public getWrappedLinesIndent(): string {
		return this._wrappedLinesIndent;
	}

	public getInputOffsetOfOutputPosition(outputLineIndex: number, outputOffset: number): number {
		if (outputLineIndex === 0) {
			return outputOffset;
		} else {
			return this._prefixSums.getAccumulatedValue(outputLineIndex - 1) + outputOffset;
		}
	}

	public getOutputPositionOfInputOffset(inputOffset: number): OutputPosition {
		let r = this._prefixSums.getIndexOf(inputOffset);
		return new OutputPosition(r.index, r.remainder);
	}
}
