import { Marked } from './lib/node_modules/marked/lib/marked.esm.js'
import markedTokenPosition from './lib/node_modules/marked-token-position/lib/index.esm.js'

const other = {// {{{
	codeRemoveIndent: /^(?: {1,4}| {0,3}\t)/gm,
	outputLinkReplace: /\\([\[\]])/g,
	indentCodeCompensation: /^(\s+)(?:```)/,
	beginningSpace: /^\s+/,
	endingHash: /#$/,
	startingSpaceChar: /^ /,
	endingSpaceChar: / $/,
	nonSpaceChar: /[^ ]/,
	newLineCharGlobal: /\n/g,
	tabCharGlobal: /\t/g,
	multipleSpaceGlobal: /\s+/g,
	blankLine: /^[ \t]*$/,
	doubleBlankLine: /\n[ \t]*\n[ \t]*$/,
	blockquoteStart: /^ {0,3}>/,
	blockquoteSetextReplace: /\n {0,3}((?:=+|-+) *)(?=\n|$)/g,
	blockquoteSetextReplace2: /^ {0,3}>[ \t]?/gm,
	listReplaceNesting: /^ {1,4}(?=( {4})*[^ ])/g,
	listIsTask: /^\[[ xX]\] +\S/,
	listReplaceTask: /^\[[ xX]\] +/,
	listTaskCheckbox: /\[[ xX]\]/,
	anyLine: /\n.*\n/,
	hrefBrackets: /^<(.*)>$/,
	tableDelimiter: /[:|]/,
	tableAlignChars: /^\||\| *$/g,
	tableRowBlankLine: /\n[ \t]*$/,
	tableAlignRight: /^ *-+: *$/,
	tableAlignCenter: /^ *:-+: *$/,
	tableAlignLeft: /^ *:-+ *$/,
	startATag: /^<a /i,
	endATag: /^<\/a>/i,
	startPreScriptTag: /^<(pre|code|kbd|script)(\s|>)/i,
	endPreScriptTag: /^<\/(pre|code|kbd|script)(\s|>)/i,
	startAngleBracket: /^</,
	endAngleBracket: />$/,
	pedanticHrefTitle: /^([^'"]*[^\s])\s+(['"])(.*)\2/,
	unicodeAlphaNumeric: /[\p{L}\p{N}]/u,
	escapeTest: /[&<>"']/,
	escapeReplace: /[&<>"']/g,
	escapeTestNoEncode: /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,
	escapeReplaceNoEncode: /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,
	caret: /(^|[^\[])\^/g,
	percentDecode: /%25/g,
	findPipe: /\|/g,
	splitPipe: / \|/,
	slashPipe: /\\\|/g,
	carriageReturn: /\r\n|\r/g,
	spaceLine: /^ +$/gm,
	notSpaceStart: /^\S*/,
	endingNewline: /\n$/,
	listItemRegex: (bull) => new RegExp(`^( {0,3}${bull})((?:[\t ][^\\n]*)?(?:\\n|$))`),
	nextBulletRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),
	hrRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),
	fencesBeginRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:\`\`\`|~~~)`),
	headingBeginRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}#`),
	htmlBeginRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}<(?:[a-z].*>|!--)`, 'i'),
	blockquoteBeginRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}>`),
}// }}}
const escapeReplacements = {// {{{
	'&': '&amp;',
	'<': '&lt;',
	'>': '&gt;',
	'"': '&quot;',
	"'": '&#39;',
}// }}}
const getEscapeReplacement = (ch) => escapeReplacements[ch]

function cleanUrl(href) {// {{{
	try {
		href = encodeURI(href).replace(other.percentDecode, '%')
	} catch {
		return null
	}
	return href
}// }}}
function escapeHtmlEntities(html, encode) {// {{{
	if (encode) {
		if (other.escapeTest.test(html)) {
			return html.replace(other.escapeReplace, getEscapeReplacement)
		}
	} else {
		if (other.escapeTestNoEncode.test(html)) {
			return html.replace(other.escapeReplaceNoEncode, getEscapeReplacement)
		}
	}

	return html
}// }}}

export class MarkedPosition {
	constructor() {// {{{
		window.marked_setpos = (event) => this.setpos(event)
		window.marked_changecheckbox = (event) => this.changecheckbox(event)
		window.marked_copy_to_clipboard = (event, tagname) => this.copy_to_clipboard(event, tagname)
		this.render()
	}// }}}
	setpos(event) {// {{{
		event.stopPropagation()
		event.preventDefault()

		_mbus.dispatch('MARKDOWN_EDIT', {
			position: {
				start: event.target.closest('[data-offset-start]').dataset.offsetStart,
				end: event.target.closest('[data-offset-start]').dataset.offsetEnd,
			}
		})
	}// }}}
	changecheckbox(event) {// {{{
		event.stopPropagation()
		event.preventDefault()

		_mbus.dispatch('MARKDOWN_CHANGE_CHECKBOX', {
			checkbox: event.target,
			position: {
				start: event.target.closest('[data-offset-start]').dataset.offsetStart,
				end: event.target.closest('[data-offset-start]').dataset.offsetEnd,
			}
		})
	}// }}}
	async copy_to_clipboard(event, tagname) {// {{{
		if (!event.shiftKey)
			return

		try {
			// Stop text selections on the page to the mouse pointer.
			// Old selections are remove as well to give a cleaner view
			// of the copied text/highlighting.
			event.preventDefault()
			event.stopPropagation()
			window.getSelection().removeAllRanges()

			const text = event.target.innerText
			await navigator.clipboard.writeText(text)

			const tagClasslist = event.target.closest(tagname).classList
			tagClasslist.add('copy')
			setTimeout(()=>tagClasslist.remove('copy'), 250)

		} catch (err) {
			console.error('Failed to copy: ', err)
			alert('Failed to copy: ', err)
		}
	}// }}}

	render() {// {{{
		this.marked = new Marked()
		this.marked.use(markedTokenPosition())
		this.marked.use({
			renderer: {
				heading(token) {
					const content = this.parser.parseInline(token.tokens)
					return `
						<div class="heading-container" data-heading="${token.depth}">
							<h${token.depth} ondblclick="marked_setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${content}</h${token.depth}>\n
							<div class="line"></div>\n
						</div>
					`
				},

				paragraph(token) {
					const content = this.parser.parseInline(token.tokens)
					return `<p ondblclick="marked_setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${content}</p>\n`
				},

				list(token) {
					const ordered = token.ordered
					const start = token.start

					let body = ''
					for (let j = 0; j < token.items.length; j++) {
						const item = token.items[j]
						body += this.listitem(item)
					}

					const type = ordered ? 'ol' : 'ul'
					const startAttr = (ordered && start !== 1) ? (' start="' + start + '"') : ''
					return '<' + type + startAttr + '>\n' + body + '</' + type + '>\n'
				},

				listitem(token) {
					return `<li ondblclick="marked_setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parse(token.tokens)}</li>\n`
				},

				code(token) {
					const langString = (token.lang || '').match(other.notSpaceStart)?.[0]

					const code = token.text.replace(other.endingNewline, '') + '\n'

					if (!langString) {
						return `<pre ondblclick="marked_setpos(event)" onmousedown="marked_copy_to_clipboard(event, 'pre')" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}"><code>`
							+ (token.escaped ? code : escapeHtmlEntities(code, true))
							+ '</code></pre>\n'
					}

					return `<pre ondblclick="marked_setpos(event)" onmousedown="marked_copy_to_clipboard(event, 'pre')" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}"><code class="language-`
						+ escapeHtmlEntities(langString)
						+ '">'
						+ (token.escaped ? code : escapeHtmlEntities(code, true))
						+ '</code></pre>\n'
				},

				blockquote(token) {
					const body = this.parser.parse(token.tokens)
					return `<blockquote ondblclick="marked_setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">\n${body}</blockquote>\n`
				},

				html(token) {
					return token.text
				},

				def(token) {
					return ''
				},

				hr(token) {
					return `<hr ondblclick="marked_setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">\n`
				},

				checkbox(token) {
					return `<input ondblclick="marked_setpos(event)" onchange="marked_changecheckbox(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}"`
						+ (token.checked ? 'checked="" ' : '')
						+ 'type="checkbox"> '
				},

				table(token) {
					let header = '';

					// header
					let cell = '';
					for (let j = 0; j < token.header.length; j++) {
						cell += this.tablecell(token.header[j]);
					}
					header += this.tablerow({ text: cell });

					let body = '';
					for (let j = 0; j < token.rows.length; j++) {
						const row = token.rows[j];

						cell = '';
						for (let k = 0; k < row.length; k++) {
							cell += this.tablecell(row[k]);
						}

						body += this.tablerow({ text: cell });
					}
					if (body) body = `<tbody>${body}</tbody>`;

					return '<table>\n'
						+ '<thead>\n'
						+ header
						+ '</thead>\n'
						+ body
						+ '</table>\n'
				},

				tablerow(token) {
					return `<tr>\n${token.text}</tr>\n`
				},

				tablecell(token) {
					let ofs = ''
					if (token.tokens.length > 0) {
						const start = token.tokens[0].position.start.offset
						const end = token.tokens[0].position.end.offset
						ofs = `ondblclick="marked_setpos(event)" data-offset-start="${start}" data-offset-end="${end}"`
					}

					const content = this.parser.parseInline(token.tokens);
					const type = token.header ? 'th' : 'td';
					const tag = token.align
						? `<${type} ${ofs} align="${token.align}">`
						: `<${type} ${ofs}>`;
					return tag + content + `</${type}>\n`
				},

				strong(token) {
					return `<strong ondblclick="marked_setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parseInline(token.tokens)}</strong>`
				},

				em(token) {
					return `<em ondblclick="marked_setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parseInline(token.tokens)}</em>`
				},

				codespan(token) {
					return `<code ondblclick="marked_setpos(event)" onmousedown="marked_copy_to_clipboard(event, 'code')" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${escapeHtmlEntities(token.text, true)}</code>`
				},

				br(token) {
					return `<br ondblclick="marked_setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">`
				},

				del(token) {
					return `<del ondblclick="marked_setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parseInline(token.tokens)}</del>`
				},

				link(token) {
					const text = this.parser.parseInline(token.tokens)
					const cleanHref = cleanUrl(token.href)
					if (cleanHref === null) {
						return text
					}
					token.href = cleanHref
					let out = '<a ondblclick="marked_setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}" href="' + token.href + '"'
					if (token.title) {
						out += ' title="' + (escapeHtmlEntities(token.title)) + '"'
					}
					out += '>' + text + '</a>'
					return out
				},

				image(token) {
					if (token.tokens) {
						token.text = this.parser.parseInline(token.tokens, this.parser.textRenderer)
					}
					const cleanHref = cleanUrl(token.href)
					if (cleanHref === null) {
						return escapeHtmlEntities(token.text)
					}
					token.href = cleanHref
					let out = `<n2-file ondblclick="marked_setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}" src="${token.href}" alt="${escapeHtmlEntities(token.text)}"`
					if (token.title) {
						out += ` title="${escapeHtmlEntities(token.title)}"`
					}
					out += '></n2-file>'
					return out
				},

				text(token) {
					return 'tokens' in token && token.tokens
						? this.parser.parseInline(token.tokens)
						: ('escaped' in token && token.escaped ? token.text : escapeHtmlEntities(token.text))
				},

			}
		})
	}// }}}}}}
	parse(text) {// {{{
		return this.marked.parse(text)
	}// }}}
	async whenElementExist(id) {// {{{
		// The element could have already been created.
		const element = document.getElementById(id)
		if (element) {
			return element
		}

		const observer = new MutationObserver((_mutations, observer) => {
			const target = document.getElementById(id)
			if (target) {
				observer.disconnect()
				return target
			}
		})

		observer.observe(document.documentElement, {
			childList: true,
			subtree: true
		})
	}// }}}
	async populateImg(fileID, elementID) {// {{{
		let img = await globalThis.nodeStore.files.get(fileID)
		const el = await this.whenElementExist(elementID)

		el.src = URL.createObjectURL(img.file)
	}// }}}
}
