import { clearScreen, fillRect } from "./window.ts";
import { CHAR, font } from "./font.ts";
import { drawText, measureText } from "./builtins.ts";
import { COLOR } from "./colors.ts";
import { getCodeSheet, setSheet } from "./sheet.ts";
import { K, ctrlKeyDown, getKeyboardString, keyPressed, shiftKeyDown } from "./keyboard.ts";
import { clipboard, tokenize } from "./deps.ts";
import { getBuiltins } from "./runcode.ts";
import { page } from "./viewsheets.ts";
import { mouseDown, mouseHeld, mousePos } from "./mouse.ts";

const historyDebounceFrames = 20;

const fontHeight = font.height;

const keywords = [
    "break",
    "case",
    "catch",
    "class",
    "const",
    "continue",
    "debugger",
    "default",
    "delete",
    "do",
    "else",
    "export",
    "extends",
    "finally",
    "for",
    "function",
    "if",
    "import",
    "in",
    "instanceof",
    "new",
    "return",
    "super",
    "switch",
    "this",
    "throw",
    "try",
    "typeof",
    "var",
    "void",
    "while",
    "with",
	"let",
	"static",
	"yield",
	"await",
	"enum",
    "implements",
    "interface",
    "package",
    "private",
    "protected",
    "public",
	"=>",
];
const values = [
    "false",
    "null",
    "true",
	"undefined",
	"NaN",
	"Infinity",
	CHAR.PI,
];
const operator = [
	"&&",
	"||",
	"??",
	"--",
	"++",
	".",
	"?.",
	"<",
	"<=",
	">",
	">=",
	"!=",
	"!==",
	"==",
	"===",
	"+",
	"-",
	"%",
	"&",
	"|",
	"^",
	"/",
	"*",
	"**",
	"<<",
	">>",
	">>>",
	"=",
	"+=",
	"-=",
	"%=",
	"&=",
	"|=",
	"^=",
	"/=",
	"*=",
	"**=",
	"<<=",
	">>=",
	">>>=",
	"!",
	"?",
	"~",
	"...",
];
const punctuation = [
	"(",
	")",
	"[",
	"]",
	"{",
	"}",
	".",
	":",
	";",
	",",
];

const builtinColor = COLOR.BLUE;
const keywordColor = COLOR.PURPLE;
const operatorColor = COLOR.CYAN;
const valueColor = COLOR.ORANGE;
const stringColor = COLOR.GREEN;
const regexColor = COLOR.PINK;
const punctuationColor = COLOR.LIGHTGRAY;
const commentColor = COLOR.DARKGREEN;
const identifierColor = COLOR.YELLOW;
const invalidColor = COLOR.RED;

const caretColor = COLOR.WHITE;
const selectionColor = COLOR.DARKBLUE;

const backgroundColor = COLOR.DARKERBLUE;

const tokenColors = {
	"StringLiteral": stringColor,
	"NoSubstitutionTemplate": stringColor,
	"TemplateHead": stringColor,
	"TemplateMiddle": stringColor,
	"TemplateTail": stringColor,
	"RegularExpressionLiteral": regexColor,
	"MultiLineComment": commentColor,
	"SingleLineComment": commentColor,
	"IdentifierName": identifierColor,
	"PrivateIdentifier": identifierColor,
	"NumericLiteral": valueColor,
	"Punctuator": punctuationColor,
	"WhiteSpace": punctuationColor,
	"LineTerminatorSequence": punctuationColor,
	"Invalid": invalidColor,
}

const transformForCopy = (text: string) => {
	text = text.replaceAll(CHAR.UP, "⬆️");
	text = text.replaceAll(CHAR.LEFT, "⬅️");
	text = text.replaceAll(CHAR.DOWN, "⬇️");
	text = text.replaceAll(CHAR.RIGHT, "➡️");
	return text;
}

const transformForPaste = (text: string) => {
	let newstr = "";
	text = text.replaceAll("⬆️", CHAR.UP);
	text = text.replaceAll("⬅️", CHAR.LEFT);
	text = text.replaceAll("⬇️", CHAR.DOWN);
	text = text.replaceAll("➡️", CHAR.RIGHT);
	for (const char of text) {
		if (char in font.chars) {
			newstr += char;
		}
	}
	return newstr;
}

const state = {
	doubleClickTimer: 0,
	history: [] as Array<{code: string, anchor: number, focus: number}>,
	historyDebounce: 0,
	historyIndex: 0,
	undo() {
		console.log('undoing');
		if (this.historyIndex === this.history.length && this.historyDebounce > 0) {
			this.snapshot();
		}
		console.log('historyIndex', this.historyIndex);
		if (this.historyIndex > 0) {
			this.historyIndex -= 1;
			const snap = this.history[this.historyIndex];
			console.log('historyIndex', this.historyIndex);
			this.code = snap.code;
			this.setSelection(snap.anchor, snap.focus);
		}
	},
	redo() {
		console.log('redoing');
		if (this.historyIndex < this.history.length-1) {
			this.historyIndex += 1;
			const snap = this.history[this.historyIndex];
			this.code = snap.code;
			this.setSelection(snap.anchor, snap.focus);
		}
	},
	snapshot() {
		const snap = {
			code: this.code,
			anchor: this.anchor,
			focus: this.focus,
		};
		this.history.push(snap);
		console.log('took snapshot', this.historyIndex, snap);
	},
	startSnapping() {
		console.log('start snapping', this.historyIndex);
		if (this.historyDebounce <= 0) {
			this.historyIndex += 1;
		}
		if (this.history.length > this.historyIndex) {
			this.history.length = this.historyIndex;
		}
		this.historyDebounce = historyDebounceFrames;
	},
	wordMode: false,
	scrollX: 0,
	scrollY: 0,
	anchor: 0,
	focus: 0,
	get focusX() {return indexToGrid(this.code, this.focus).x;},
	get focusY() {return indexToGrid(this.code, this.focus).y;},
	get anchorX() {return indexToGrid(this.code, this.anchor).x;},
	get anchorY() {return indexToGrid(this.code, this.anchor).y;},
	get focusPixelX() {return indexToRect(this.code, this.focus).x;},
	get focusPixelY() {return indexToRect(this.code, this.focus).y;},
	get anchorPixelX() {return indexToRect(this.code, this.anchor).x;},
	get anchorPixelY() {return indexToRect(this.code, this.anchor).y;},
	isCollapsed() {
		return this.anchor === this.focus;
	},
	clampInRange(n: number) {
		return Math.max(0, Math.min(n, this.code.length))
	},
	findNearestWordBoundaryLeft(index: number) {
		if (index === this.code.length-1) {
			return index;
		}
		const words1 = this.code.slice(0, index+1).split(/\b/g);
		if (words1[words1.length-1].length === 1) {
			return index;
		}
		const words = this.code.slice(0, index).split(/\b/g);
		if (!words.length) {
			return 0;
		}
		return index-words[words.length-1].length;
	},
	findNearestWordBoundaryRight(index: number) {
		if (index === 0) {
			return index;
		}
		const words1 = this.code.slice(index-1).split(/\b/g);
		if (words1[0].length === 1) {
			return index;
		}
		const words = this.code.slice(index).split(/\b/g);
		if (!words.length) {
			return this.code.length;
		}
		return index+words[0].length;
	},
	setSelection(anchor: number | {x: number, y: number}, focus?: number | {x: number, y: number}) {
		if (typeof anchor !== "number") {
			anchor = gridToIndex(this.code, anchor.x, anchor.y);
		}
		focus = focus ?? anchor;
		if (typeof focus !== "number") {
			focus = gridToIndex(this.code, focus.x, focus.y);
		}
		this.anchor = this.clampInRange(anchor);
		this.focus = this.clampInRange(focus);
		if (this.wordMode) {
			console.log('word mode', this.anchor, this.focus, this.findNearestWordBoundaryLeft(this.anchor), this.findNearestWordBoundaryRight(this.focus));
			if (this.anchor <= this.focus) {
				this.anchor = this.findNearestWordBoundaryLeft(this.anchor);
				this.focus = this.findNearestWordBoundaryRight(this.focus);
			} else {
				this.anchor = this.findNearestWordBoundaryRight(this.anchor);
				this.focus = this.findNearestWordBoundaryLeft(this.focus);
			}
		}
		this.anchor = this.clampInRange(this.anchor);
		this.focus = this.clampInRange(this.focus);
	},
	setFocus(focus: number | {x: number, y: number}) {
		if (typeof focus !== "number") {
			focus = gridToIndex(this.code, focus.x, focus.y);
		}
		this.focus = this.clampInRange(focus);
		if (this.wordMode) {
			if (this.anchor <= this.focus) {
				this.anchor = this.findNearestWordBoundaryLeft(this.anchor);
				this.focus = this.findNearestWordBoundaryRight(this.focus);
			} else {
				this.anchor = this.findNearestWordBoundaryRight(this.anchor);
				this.focus = this.findNearestWordBoundaryLeft(this.focus);
			}
		}
		this.focus = this.clampInRange(this.focus);
	},
	insertText(text: string) {
		const {code, anchor, focus} = this;
		this.code = code.slice(0, Math.min(anchor, focus)) + text + code.slice(Math.max(anchor, focus));
		this.setSelection(Math.min(anchor, focus) + text.length);
		this.startSnapping();
	},
	toggleComment() {
		const lines = this.code.split("\n");
		const {focusX, focusY, anchorX, anchorY} = this;
		const lineInSelection = (i: number) => i >= Math.min(focusY, anchorY) && i <= Math.max(focusY, anchorY);
		const allLinesAreCommented = lines.every((line, i) => {
			if (lineInSelection(i) && !line.trim().startsWith("// ")) {
				return false;
			} else {
				return true;
			}
		});
		const newLines = lines.map((line, i) => {
			if (lineInSelection(i)) {
				if (allLinesAreCommented) {
					return line.slice(3);
				} else {
					return "// "+line;
				}
			} else {
				return line;
			}
		});
		this.code = newLines.join("\n");
		const shiftBy = allLinesAreCommented ? -3 : 3;
		this.setSelection({x: anchorX+shiftBy, y: anchorY}, {x: focusX+shiftBy, y: focusY});
		this.startSnapping();
	},
	indent(indentString: string) {
		const lines = this.code.split("\n");
		const {focusX, focusY, anchorX, anchorY} = this;
		const newLines = lines.map((line, i) => {
			if (i >= Math.min(focusY, anchorY) && i <= Math.max(focusY, anchorY)) {
				return indentString+line;
			} else {
				return line;
			}
		});
		this.code = newLines.join("\n");
		this.setSelection({x: anchorX+1, y: anchorY}, {x: focusX+1, y: focusY});
		this.startSnapping();
	},
	outdent(outdentRegex: RegExp) {
		const lines = this.code.split("\n");
		const {focusX, focusY, anchorX, anchorY} = this;
		const newLines = lines.map((line, i) => {
			const match = line.match(outdentRegex);
			if (i >= Math.min(focusY, anchorY) && i <= Math.max(focusY, anchorY) && match) {
				return line.slice(match[0].length);
			} else {
				return line;
			}
		});
		this.code = newLines.join("\n");
		this.setSelection({x: Math.max(0,anchorX-1), y: anchorY}, {x: Math.max(0,focusX-1), y: focusY});
		this.startSnapping();
	},
	backspace() {
		const {code, focus} = this;
		if (this.isCollapsed()) {
			if (focus > 0) {
				this.code = code.slice(0, focus-1) + code.slice(focus);
				this.setSelection(focus-1);
				this.startSnapping();
			}
		} else {
			this.insertText("");
		}
	},
	delete() {
		const {code, focus} = this;
		if (this.isCollapsed()) {
			if (focus < code.length) {
				this.code = code.slice(0, focus) + code.slice(1+focus);
				this.startSnapping();
			}
		} else {
			this.insertText("");
		}
	},
	async copy() {
		const {code, anchor, focus} = this;
		const selected = code.slice(Math.min(anchor,focus), Math.max(anchor,focus));
		await clipboard.writeText(transformForCopy(selected));
	},
	async cut() {
		await this.copy();
		this.insertText("");
	},
	async paste() {
		this.insertText(transformForPaste(await clipboard.readText()));
	},
	scrollToCursor() {
		const {focusY, scrollY, scrollX, focus} = this;
		const fh = fontHeight + 1;
		const rect = indexToRect(this.code, focus);
		if (focusY*fh < scrollY) {
			this.scrollY = focusY*fh;
		}
		if (focusY*fh > scrollY+112-fh) {
			this.scrollY = focusY*fh-112+fh;
		}
		if (rect.x < scrollX) {
			this.scrollX = rect.x;
		}
		if (rect.x+rect.w > scrollX+128) {
			this.scrollX = rect.x-128+rect.w+1;
		}
	},
	currentIndentation() {
		const lines = this.code.slice(0, this.focus).split("\n");
		const line = lines[lines.length-1];
		const match = line.match(/^\s*/);
		if (!match) {
			return "";
		}
		return match[0];
	},
	get code() {
		return getCodeSheet(page.activeSheet);
	},
	set code(val) {
		setSheet(page.activeSheet, "code", val);
	}
}

const indexToGrid = (str: string, index: number) => {
	const linesUpTo = str.slice(0,index).split("\n");
	return {
		x: linesUpTo[linesUpTo.length-1].length,
		y: linesUpTo.length - 1,
	}
}

const gridToIndex = (str: string, x: number, y: number) => {
	const lines = str.split("\n");
	if (y < 0) {
		return 0;
	}
	if (y >= lines.length) {
		return str.length;
	}
	return lines.slice(0, y).join("\n").length+Math.min(x, lines[y].length)+(y === 0 ? 0 : 1);
}

const indexToRect = (str: string, index: number) => {
	const linesUpTo = str.slice(0,index).split("\n");
	let extra = 0;
	if (linesUpTo[linesUpTo.length-1].length > 0) {
		extra = 1;
	}
	return {
		x: measureText(linesUpTo[linesUpTo.length-1]) + extra,
		y: (fontHeight + 1)*(linesUpTo.length - 1),
		w: measureText(str[index] ?? "\n"),
		h: fontHeight+1,
	}
}

const pixelToIndex = (str: string, x: number, y: number) => {
	const lines = str.split("\n");
	if (y < 0) {
		return 0;
	}
	if (y >= (fontHeight+1)*lines.length) {
		return str.length;
	}
	const yy = Math.floor(y/(fontHeight+1));
	const prefix = lines.slice(0, yy).join("\n").length+(yy === 0 ? 0 : 1);
	const line = lines[yy];
	let j = 0;
	while (measureText(line.slice(0, j)) < x && j < line.length) {
		j+=1;
	}
	if (measureText(line) < x) {
		j+=1;
	}
	return prefix + Math.max(0, j-1);
}

const update = async () => {
	const { focus } = state;
	if (state.history.length === 0) {
		state.snapshot();
	}
	if (state.historyDebounce > 0) {
		state.historyDebounce -= 1;
		if (state.historyDebounce <= 0) {
			state.snapshot();
		}
	}
	if (state.doubleClickTimer > 0) {
		state.doubleClickTimer -= 1;
	}

	if (mouseDown() && !shiftKeyDown()) {
		if (state.doubleClickTimer > 0) {
			state.wordMode = true;
		} else {
			state.doubleClickTimer = 10;
		}
		const {x, y} = mousePos();
		state.setSelection(pixelToIndex(state.code, x+state.scrollX, y+state.scrollY-8));
		state.scrollToCursor();
	} else if (mouseHeld()) {
		const {x, y} = mousePos();
		state.setFocus(pixelToIndex(state.code, x+state.scrollX, y+state.scrollY-8));
		state.scrollToCursor();
	} else {
		state.wordMode = false;
	}

	const keyboardString = getKeyboardString();
	if (keyboardString) {
		state.insertText(keyboardString);
		state.scrollToCursor();
	}

	if (keyPressed(K.ENTER)) {
		state.insertText("\n"+state.currentIndentation());
		state.scrollToCursor();
	}
	if (keyPressed(K.TAB)) {
		if (!shiftKeyDown()) {
			if (state.isCollapsed()) {
				state.insertText("\t");
			} else {
				state.indent("\t");
			}
		} else {
			state.outdent(/^(\t| )/);
		}
		state.scrollToCursor();
	}
	if (keyPressed(K.BACKSPACE)) {
		state.backspace();
		state.scrollToCursor();
	}
	if (keyPressed(K.DELETE)) {
		state.delete();
		state.scrollToCursor();
	}
	if (keyPressed(K.ARROW_RIGHT)) {
		if (shiftKeyDown()) {
			state.setFocus(focus+1);
		} else {
			state.setSelection(focus+1);
		}
		state.scrollToCursor();
	}
	if (keyPressed(K.ARROW_LEFT)) {
		if (shiftKeyDown()) {
			state.setFocus(focus-1);
		} else {
			state.setSelection(focus-1);
		}
		state.scrollToCursor();
	}
	if (keyPressed(K.ARROW_DOWN)) {
		const rect = indexToRect(state.code, focus);
		const newIndex = pixelToIndex(state.code, rect.x, rect.y+rect.h+1+1);
		if (shiftKeyDown()) {
			state.setFocus(newIndex);
		} else {
			state.setSelection(newIndex);
		}
		state.scrollToCursor();
	}
	if (keyPressed(K.ARROW_UP)) {
		const rect = indexToRect(state.code, focus);
		const newIndex = pixelToIndex(state.code, rect.x, rect.y-1-1);
		if (shiftKeyDown()) {
			state.setFocus(newIndex);
		} else {
			state.setSelection(newIndex);
		}
		state.scrollToCursor();
	}
	if (keyPressed("C") && ctrlKeyDown()) {
		await state.copy();
		state.scrollToCursor();
	}
	if (keyPressed("X") && ctrlKeyDown()) {
		await state.cut();
		state.scrollToCursor();
	}
	if (keyPressed("V") && ctrlKeyDown()) {
		await state.paste();
		state.scrollToCursor();
	}
	if (keyPressed("Z") && ctrlKeyDown()) {
		if (shiftKeyDown()) {
			state.redo();
		} else {
			state.undo();
		}
	}
	if (keyPressed("Y") && ctrlKeyDown()) {
		state.redo();
	}
	if (keyPressed("/") && ctrlKeyDown()) {
		state.toggleComment();
	}
}

const draw = () => {
	clearScreen();
	const {
		scrollX,
		scrollY,
		anchor,
		focus,
		code,
	} = state;
	const x = 0;
	const y = 8;
	const w = 128;
	const h = 112;
	fillRect(x, y, w, h, backgroundColor);
	if (anchor !== focus) {
		for (let i = Math.min(anchor, focus); i < Math.max(anchor, focus); i++) {
			const sel = indexToRect(code, i);
			fillRect(x+sel.x-scrollX, y+sel.y-scrollY, sel.w+2, sel.h, selectionColor);
		}
	}
	const rect = indexToRect(code, focus);
	fillRect(x+rect.x-scrollX, y+rect.y-scrollY, 1, rect.h, caretColor);

	const builtins = Object.keys(getBuiltins());
	const tokens = [...tokenize(code)];
	let cx = 0;
	let cy = 0;
	tokens.forEach((token) => {
		if (token.type === "LineTerminatorSequence") {
			cx=0;
			cy+=fontHeight+1;
			return;
		}
		const lines = token.value.split("\n");
		lines.forEach((line, i) => {
			let color = tokenColors[token.type];
			if (builtins.includes(token.value)) {
				color = builtinColor;
			}
			if (keywords.includes(token.value)) {
				color = keywordColor;
			}
			if (values.includes(token.value)) {
				color = valueColor;
			}
			if (operator.includes(token.value)) {
				color = operatorColor;
			}
			if (punctuation.includes(token.value)) {
				color = punctuationColor;
			}
			drawText(1+x+cx-scrollX, 1+y+cy-scrollY, line, color);
			if (i === lines.length-1) {
				cx += measureText(line)+1;
			} else {
				cx=0;
				cy+=fontHeight+1;
			}
		});
	})
}

export const codetab = {
	update,
	draw,
}