/* eslint max-lines: ["error", { "max": 500 }] */
import { HighlightStyle } from '@codemirror/language';
import { tags } from '@lezer/highlight';
import { legacyTokenizeCustomMarkdown } from './legacyCustomHighlight';
import {
	Decoration,
	ViewPlugin,
} from '@codemirror/view';
import {
	syntaxTree,
	ensureSyntaxTree
} from '@codemirror/language';

// Making the tokens
const customTags = {
	pageLine        : 'pageLine', 		// .cm-pageLine
	snippetLine     : 'snippetLine', 	// .cm-snippetLine
	columnSplit     : 'columnSplit', 	// .cm-columnSplit
	block           : 'block', 			// .cm-block
	inlineBlock     : 'inline-block', 	// .cm-inline-block
	injection       : 'injection', 		// .cm-injection
	emoji           : 'emoji', 			// .cm-emoji
	superscript     : 'superscript', 	// .cm-superscript
	subscript       : 'subscript', 		// .cm-subscript
	definitionList  : 'definitionList', // .cm-definitionList
	definitionTerm  : 'definitionTerm', // .cm-definitionTerm
	definitionDesc  : 'definitionDesc', // .cm-definitionDesc
	definitionColon : 'definitionColon', // .cm-definitionColon
	strikethrough   : 'strikethrough', // .cm-strikethrough

	//CSS

	variable : 'variable',
};

function tokenizeCustomMarkdown(text) {
	const tokens = [];
	const lines = text.split('\n');

	//tokens without a `from` or `to` are interpreted by the custom plugin as line tokens

	lines.forEach((lineText, lineNumber)=>{
		// --- Page / snippet lines ---
		if(/^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m.test(lineText)) tokens.push({ line: lineNumber, type: customTags.pageLine });
		if(/^\\snippet\ .*$/.test(lineText)) tokens.push({ line: lineNumber, type: customTags.snippetLine });
		if(/^\\column(?:break)?$/.test(lineText)) tokens.push({ line: lineNumber, type: customTags.columnSplit });

		// --- Emoji ---
		if(/:.\w+?:/.test(lineText)) {
			const emojiRegex = /(:\w+?:)/g;
			let match;
			while ((match = emojiRegex.exec(lineText)) !== null) {
				tokens.push({
					line : lineNumber,
					type : customTags.emoji,
					from : match.index,
					to   : match.index + match[0].length,
				});
			}
		}

		// --- Superscript / Subscript ---
		if(/\^/.test(lineText)) {
			let startIndex = lineText.indexOf('^');
			const superRegex = /\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^/gy;
			const subRegex = /\^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^\^/gy;

			while (startIndex >= 0) {
				superRegex.lastIndex = subRegex.lastIndex = startIndex;

				let match = subRegex.exec(lineText);
				let type = customTags.subscript;

				if(!match) {
					match = superRegex.exec(lineText);
					type = customTags.superscript;
				}

				if(match) {
					tokens.push({
						line : lineNumber,
						type,
						from : match.index,
						to   : match.index + match[0].length,
					});
				}

				startIndex = lineText.indexOf(
					'^',
					Math.max(startIndex + 1, superRegex.lastIndex || 0, subRegex.lastIndex || 0),
				);
			}
		}

		// --- Strikethrough ---
		if(/\~/.test(lineText)) {
			const strikethroughRegex = /~(?!\s)(.+?)(?<!\s)~/g;

			const match = strikethroughRegex.exec(lineText);
			const type = customTags.strikethrough;

			if(match) {
				tokens.push({
					line : lineNumber,
					type,
					from : match.index,
					to   : match.index + match[0].length,
				});
			}
		}

		// --- single line def list ---
		const singleLineRegex = /^(?=.*[^:])(.+?)(\s*)(::)([^\n]*)$/dmy;
		const match = singleLineRegex.exec(lineText);

		if(match) {
			const [full, term, spaces, colons, desc] = match;

			let offset = 0;

			tokens.push({
				line : lineNumber,
				type : customTags.definitionList,
			});

			// Term
			tokens.push({
				line : lineNumber,
				type : customTags.definitionTerm,
				from : offset,
				to   : offset + term.length,
			});
			offset += term.length;

			// Spaces before ::
			if(spaces) {
				offset += spaces.length;
			}

			// :: colons
			tokens.push({
				line : lineNumber,
				type : customTags.definitionColon,
				from : offset,
				to   : offset + colons.length,
			});
			offset += colons.length;

			// Definition
			tokens.push({
				line : lineNumber,
				type : customTags.definitionDesc,
				from : offset,
				to   : offset + desc.length,
			});
		}

		//  --- multiline def list ---
		if(!/^::/.test(lines[lineNumber]) && lineNumber + 1 < lines.length && /^::/.test(lines[lineNumber + 1])) {
			const startLine = lineNumber;
			const defs = [];

			// collect all following :: definitions
			for (let i = lineNumber + 1; i < lines.length; i++) {
				const nextLine = lines[i];
				const onlyColonsMatch = /^:*$/.test(nextLine);
				const defMatch = /^(::)(.+)$/.exec(nextLine);
				if(!onlyColonsMatch && defMatch) {
					defs.push({ colons: defMatch[1], desc: defMatch[2], line: i });
				} else break;
			}

			if(defs.length > 0 && lineText.trim().length > 0) {
				tokens.push({
					line : startLine,
					type : customTags.definitionList,
				});

				// term
				tokens.push({
					line : startLine,
					type : customTags.definitionTerm,
					from : 0,
					to   : lineText.length,
				});

				// definitions
				defs.forEach((d)=>{
					tokens.push({
						line : d.line,
						type : customTags.definitionList,
					});

					tokens.push({
						line : d.line,
						type : customTags.definitionColon,
						from : 0,
						to   : d.colons.length,
					});
					tokens.push({
						line : d.line,
						type : customTags.definitionDesc,
						from : d.colons.length,
						to   : d.colons.length + d.desc?.length,
					});
				});
			}
		}

		if(lineText.includes('{') && lineText.includes('}')) {
			const injectionRegex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gmd;
			let match;

			while ((match = injectionRegex.exec(lineText)) !== null) {
				tokens.push({
					line : lineNumber,
					from : match.indices[1][0],
					to   : match.indices[1][1],
					type : customTags.injection,
				});
			}
		}
		if(lineText.includes('{{') && lineText.includes('}}')) {
			// Inline blocks: single-line {{…}}
			const spanRegex = /{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *|}}/g;
			let match;
			let blockCount = 0;
			while ((match = spanRegex.exec(lineText)) !== null) {
				if(match[0].startsWith('{{')) {
					blockCount += 1;
				} else {
					blockCount -= 1;
				}
				if(blockCount < 0) {
					blockCount = 0;
					continue;
				}
				tokens.push({
					line : lineNumber,
					from : match.index,
					to   : match.index + match[0].length,
					type : customTags.inlineBlock,
				});
			}
		} else if(lineText.trimLeft().startsWith('{{') || lineText.trimLeft().startsWith('}}')) {
			// Highlight block divs {{\n Content \n}}
			let endCh = lineText.length + 1;

			const match = lineText.match(
				/^ *{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *$|^ *}}$/,
			);
			if(match) endCh = match.index + match[0].length;
			const closingMatch = lineText.match(/ *(}})/d);
			
			if(closingMatch) {
				tokens.push({ line: lineNumber, from: closingMatch.indices[1][0], to: closingMatch.indices[1][1], type: customTags.block });
			} else {
				tokens.push({ line: lineNumber, type: customTags.block });
			}
			
		}
	});

	return tokens;
}

function tokenizeCustomCSS(text) {
	const tokens = [];
	const lines = text.split('\n');

	lines.forEach((lineText, lineNumber)=>{

		if(/--[a-zA-Z0-9-_]+/gm.test(lineText)) {
			const varRegex =/--[a-zA-Z0-9-_]+/gm;
			let match;
			while ((match = varRegex.exec(lineText)) !== null) {
				tokens.push({
					line : lineNumber,
					from : match.index +1,
					to   : match.index + match.length[1] +1,
					type : customTags.varProperty,
				});
			}
		}
	});

	return tokens;
}

//assign classes to tags provided by lezer, not unlike the function above
export const customHighlightStyle = HighlightStyle.define([
	{ tag: tags.heading,  class: 'cm-header' },
	{ tag: tags.heading1,  class: 'cm-header cm-header-1' },
	{ tag: tags.heading2,  class: 'cm-header cm-header-2' },
	{ tag: tags.heading3,  class: 'cm-header cm-header-3' },
	{ tag: tags.heading4,  class: 'cm-header cm-header-4' },
	{ tag: tags.heading5,  class: 'cm-header cm-header-5' },
	{ tag: tags.heading6,  class: 'cm-header cm-header-6' },
	{ tag: tags.link,  class: 'cm-link' },
	{ tag: tags.string,  class: 'cm-string' },
	{ tag: tags.url,  class: 'cm-string cm-url' },
	{ tag: tags.list,  class: 'cm-list' },
	{ tag: tags.strong,  class: 'cm-strong' },
	{ tag: tags.emphasis,  class: 'cm-em' },
	{ tag: tags.quote,  class: 'cm-quote' },
	{ tag: tags.comment,  class: 'cm-comment' },
	{ tag: tags.monospace,  class: 'cm-comment' },

	//css tags

	{ tag: tags.tagName,  class: 'cm-tag' },
	{ tag: tags.className,  class: 'cm-class' },
	{ tag: tags.propertyName,  class: 'cm-property' },
	{ tag: tags.attributeValue,  class: 'cm-value' },
	{ tag: tags.keyword,  class: 'cm-keyword' },
	{ tag: tags.atom,  class: 'cm-atom' },
	{ tag: tags.integer,  class: 'cm-integer' },
	{ tag: tags.unit,  class: 'cm-unit' },
	{ tag: tags.color,  class: 'cm-color' },
	{ tag: tags.paren,  class: 'cm-paren' },
	{ tag: tags.variableName,  class: 'cm-variable' },
	{ tag: tags.invalid,  class: 'cm-error' },

]);

function getUrl(node, doc) {
	let url = null;

	const cursor = node.node.cursor();

	if(cursor.firstChild()) {
		do {
			if(cursor.name === 'URL') {
				url = doc.sliceString(cursor.from, cursor.to);
				break;
			}
		} while (cursor.nextSibling());
	}

	return url;
}

import { WidgetType } from '@codemirror/view';

class ImageWidget extends WidgetType {
	constructor(url) {
		super();
		this.url = url;
	}

	toDOM() {
		const img = document.createElement('img');
		img.loading = "lazy";
		img.className = 'cm-preview';
		img.src = this.url;
		

		img.onerror = ()=>{
			img.src  = 'client/icons/broken-image.jpg';
		};

		return img;
	}

	eq(other) {
		return other.url === this.url;
	}
}

export function customHighlightPlugin(renderer, tab) {
	//this function takes the custom tokens created in the tokenize function in customhighlight files
	//takes the tokens defined by that function and assigns classes to them
	//it also creates page number and snippet number widgets

	let tokenize;

	if(tab === 'brewStyles') {
		tokenize = tokenizeCustomCSS;
	} else {
		tokenize = renderer === 'V3' ? tokenizeCustomMarkdown : legacyTokenizeCustomMarkdown;
	}

	return ViewPlugin.fromClass(
		class {
			constructor(view) {
				this.decorations = this.buildDecorations(view);
			}
			update(update) {
				if(update.docChanged) {
					this.decorations = this.buildDecorations(update.view);
				}
			}
			buildDecorations(view) {
				const decos = [];
				const tokens = tokenize(view.state.doc.toString());
				let pageCount = 1;
				let snippetCount = 0;

				const tree = ensureSyntaxTree(view.state, view.state.doc.length, 50) || syntaxTree(view.state);
				tree.iterate({
					enter : (node)=>{
						if(node.name === 'Image') {
							const url = getUrl(node, view.state.doc);

							const widgetPosition = node.node.lastChild.from;
							//this is not exactly standard, but should hold, 
							//and is the shortest way i could find of positioning 
							//the image inside the cm-image node

							if(!url) return;

							decos.push(
								Decoration.mark({
									class : 'cm-image'
								}).range(node.from, node.to)
							);
							decos.push(
								Decoration.widget({
									widget : new ImageWidget(url),
									side   : 1
								}).range(widgetPosition)
							);
						}
					}
				});

				tokens.forEach((token)=>{
					const line = view.state.doc.line(token.line + 1);

					if(token.from != null && token.to != null && token.from < token.to) {
						const from = line.from + token.from;
						const to = line.from + token.to;

						const attrs = {};
						if(token.type === 'Image' && token.url) {

							attrs['data-url'] = token.url;
						}

						decos.push(
							Decoration.mark({
								class : `cm-${token.type}`,
								...(Object.keys(attrs).length
									? { attributes: attrs }
									: {})
							}).range(from, to)
						);
					} else {
						decos.push(
							Decoration.line({
								class : `cm-${token.type}`
							}).range(line.from)
						);
						if(token.type === 'pageLine' && tab === 'brewText') {
							pageCount++;
							if(line.from === 0) pageCount--;
							decos.push(Decoration.line({ attributes: { 'data-page-number': pageCount } }).range(line.from));
						}
						if(token.type === 'snippetLine' && tab === 'brewSnippets') {
							snippetCount++;
							decos.push(Decoration.line({ attributes: { 'data-page-number': snippetCount } }).range(line.from));
						}
					}
				});

				decos.sort((a, b)=>a.from - b.from || a.to - b.to);
				return Decoration.set(decos);
			}
		},
		{ decorations: (v)=>v.decorations }
	);
};