import { ROOT_NODE, uuidv7 } from 'node_store'
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
import { MarkedPosition } from './marked_position.mjs'

export class N2PageNodeUI extends CustomHTMLElement {
	static {// {{{
		this.tmpl = document.createElement('template')
		this.tmpl.innerHTML = `
			<style>
				.el-functions {
					display: grid;
					grid-template-columns: 1fr repeat(3, min-content);
					grid-gap: 8px;
					align-items: center;
					justify-items: end;

					img {
						height: 24px;
					}
				}
			</style>
			<div data-el="name"></div>

			<textarea data-el="node-content" required rows=1></textarea>
			<div data-el="node-markdown" tabindex=1></div>

			<div data-el="functions">
				<img data-el="icon-save" src="/images/${_VERSION}/icon_save_disabled.svg">
				<img data-el="icon-markdown">
				<img data-el="icon-table-format" class="colorize" src="/images/${_VERSION}/icon_table.svg">
				<img data-el="icon-history"      class="colorize" src="/images/${_VERSION}/icon_history.svg">
			</div>
		`
	}// }}}

	constructor() {// {{{
		super()
		this.node = null

		this.style.display = 'contents'
		this.classList.add('show-markdown') // TODO Should probably be moved to settings.
		this.marked = new MarkedPosition()

		_mbus.subscribe('NODE_UI_OPEN', event => {
			this.node = event.detail.data
			this.showMarkdown(true)
			this.render()
		})

		_mbus.subscribe('NODE_MODIFIED', () => {
			this.classList.add('node-modified')
			this.elIconSave.src = `/images/${_VERSION}/icon_save.svg`
			this.elIconSave.classList.add('colorize')
			this.renderName()
		})

		_mbus.subscribe('NODE_UNMODIFIED', () => {
			this.classList.remove('node-modified')
			this.elIconSave.src = `/images/${_VERSION}/icon_save_disabled.svg`
			this.elIconSave.classList.remove('colorize')
		})

		_mbus.subscribe('MARKDOWN_TOGGLE', () => this.showMarkdown(!this.showMarkdown()))
		_mbus.subscribe('MARKDOWN_EDIT', ({ detail }) => this.editMarkdown(detail.data))

		this.elName.addEventListener('click', () => {
			const name = prompt('Change title', this.node.data.Name)
			if (name === null)
				return

			try {
				this.node.setName(name)
			} catch (err) {
				console.error(err)
				alert(err)
			}
		})
		this.elNodeContent.addEventListener('input', event => this.contentChanged(event))
		this.elNodeContent.addEventListener('paste', async (event) => this.pasteHandler(event))
		this.elIconMarkdown.addEventListener('click', () => this.showMarkdown(!this.showMarkdown()))
		this.elIconTableFormat.addEventListener('click', event => {
			if (!event.shiftKey)
				this.elNodeContent.value = this.formatAllTables(this.elNodeContent.value)
			else {
				const from = this.elNodeContent.selectionStart
				const to = this.elNodeContent.selectionEnd
				const text = this.elNodeContent.value.slice(from, to)

				const formatted = this.formatAllTables(text)
				this.elNodeContent.setRangeText(formatted, from, to, 'select');
			}

			this.node.setContent(this.elNodeContent.value)
		})
		this.elIconHistory.addEventListener('click', () => _mbus.dispatch('SHOW_PAGE', { page: 'history' }))
		this.elIconSave.addEventListener('click', ()=>this.saveNode())

		this.showMarkdown(true)
	}// }}}
	renderName() {// {{{
		this.elName.innerText = this.node?.get('Name') ?? ''
	}// }}}
	render() {// {{{
		this.elName.innerText = this.node?.get('Name') ?? ''
		this.elNodeContent.value = this.node?.get('Content') ?? ''
		this.elNodeMarkdown.innerHTML = this.marked.parse(this.elNodeContent.value)
	}// }}}
	takeFocus() {// {{{
		if (this.showMarkdown()) {
			this.elNodeMarkdown.focus({ preventScroll: true })
		} else
			this.elNodeContent.focus({ preventScroll: true })
	}// }}}
	async saveNode() {// {{{
		if (!this.node.isModified())
			return

		/* The node history is a local store for node history.
		 * This could be provisioned from the server or cleared if
		 * deemed unnecessary.
		 *
		 * The send queue is what will be sent back to the server
		 * to have a recorded history of the notes.
		   *
		 * A setting to be implemented in the future could be to
		 * not save the history locally at all. */

		// The node is still in its old state and will present
		// the unmodified content to the node store.
		const history = nodeStore.nodesHistory.add(this.node)

		// Prepares the node object for saving.
		// Sets Updated value to current date and time.
		await this.node.save()

		// Updated node is added to the send queue to be stored on server.
		const sendQueue = nodeStore.sendQueue.add(this.node)

		// Updated node is saved to the primary node store.
		const nodeStoreAdding = nodeStore.add([this.node])

		await Promise.all([history, sendQueue, nodeStoreAdding])
	}// }}}

	contentChanged(event) {//{{{
		this.node.setContent(event.target.value)
	}//}}}
	isModified() {// {{{
		return this.node?.isModified()
	}// }}}
	showMarkdown(state) {// {{{
		// No point in showing markdown if there is no data.
		// If there is no data, it will show a blank page regardless, and the user will most
		// likely want to edit content, which can't be done in markdown.
		const show = this.node?.content().trim() !== '' && state

		switch (show) {
			case true:
				this.elNodeMarkdown.innerHTML = this.marked.parse(this.elNodeContent.value)
				this.elIconMarkdown.src = `/images/${_VERSION}/icon_markdown.svg`
				this.elIconMarkdown.classList.add('colorize')
				this.classList.add('show-markdown')
				break
			case false:
				this.elIconMarkdown.src = `/images/${_VERSION}/icon_markdown_hollow.svg`
				this.elIconMarkdown.classList.remove('colorize')
				this.classList.remove('show-markdown')
				break
			case null:
			case undefined:
				return this.classList.contains('show-markdown')
		}
	}// }}}
	async pasteHandler(event) {// {{{
		const clipboardItems = event.clipboardData?.items
		if (!clipboardItems)
			return

		for (const item of clipboardItems) {
			switch (item.kind) {
				case 'string':
					continue

				case 'file':
					const file = item.getAsFile()
					if (!file)
						throw new Error("Couldn't convert image to file object.")
					const uuid = uuidv7()
					await globalThis.nodeStore.files.add({ data: { UUID: uuid, file: file } })

					const [start, end] = [this.elNodeContent.selectionStart, this.elNodeContent.selectionEnd]
					this.elNodeContent.setRangeText(`![${file.name}](db://${uuid})`, start, end, 'select');

					// Editing the textarea programatically doesn't generate the events it usually gets when edited interactively.
					this.node.setContent(this.elNodeContent.value)

					break

				default:
					alert(`Unknown paste type of '${item.kind}'`)
			}
		}
	}// }}}
	editMarkdown(data) {// {{{
		this.showMarkdown(false)
		this.elNodeContent.selectionStart = data.position.start
		this.elNodeContent.selectionEnd = data.position.end
		this.elNodeContent.focus()
	}// }}}

	findTables(lines) {// {{{
		let tables = []
		let curr = { from: -1, to: -1 }
		for (let i = 0; i < lines.length; i++) {
			const linecols = lines[i].split('|').length - 2 // Gives empty value in front of first pipe and after last one.

			if (linecols >= 1) {
				if (curr.from == -1)
					curr.from = i
				curr.to = i
			} else if (linecols < 1 && curr.to > -1) {
				tables.push(curr)
				curr = { from: -1, to: -1 }
			}
		}

		if (curr.from > -1)
			tables.push(curr)

		return tables
	}// }}}
	formatAllTables(text) {// {{{
		const lines = text.split(/\r?\n/)
		const tables = this.findTables(lines)
		for (const table of tables) {
			const formattedLines = this.formatTable(lines.slice(table.from, table.to + 1))
			lines.splice(table.from, formattedLines.length, ...formattedLines)
		}

		return lines.join("\n")
	}// }}}
	formatTable(lines) {// {{{
		let numColumns = 0
		let colwidth = []

		for (let i = 0; i < lines.length; i++) {
			// -1 for split, -1 because number of columns are one less than number of pipes.
			const columns = lines[i].split('|').slice(1)
			const linecols = columns.length - 2
			numColumns = Math.max(numColumns, linecols)

			// Keep count of column width.
			for (let j = 0; j < columns.length - 1; j++) {
				colwidth[j] = Math.max(colwidth[j] || 0, columns[j].trim().length)
			}
		}

		// Build up each line correct.
		let extendHeader
		for (let i = 0; i < lines.length; i++) {
			// Build lines with columns.
			const cols = lines[i].split('|').slice(1, -1)

			// Second line should be headers.
			if (i === 1) {
				extendHeader = true
				for (let j = 0; j < colwidth.length; j++) {
					extendHeader &= ((cols[j] || '').match(/^\s*[-]*\s*$/) !== null)
				}


			}

			if (i === 1 && extendHeader) {
				for (let j = 0; j < colwidth.length; j++)
					cols[j] = '-'.repeat(colwidth[j])

			} else {
				for (let j = 0; j < colwidth.length; j++) {
					cols[j] = (cols[j] || '').trim()
					const cw = colwidth[j]
					const padWidth = cw - (cols[j]?.length || 0) // may be a column that doesn't exist on this line.
					cols[j] = cols[j] + ' '.repeat(padWidth > 0 ? padWidth : 0)
				}
			}

			lines[i] = '| ' + cols.join(' | ') + ' |'
		}

		return lines
	}// }}}
}
customElements.define('n2-nodeui', N2PageNodeUI)

export class Node {
	static sort(a, b) {//{{{
		// Nodes with children ("folders") are sorted first.
		if (a._has_children && !b._has_children) return -1
		if (!a._has_children && b._has_children) return 1

		// Otherwise sort by lowercased name.
		const an = a.data.Name.toLowerCase()
		const bn = b.data.Name.toLowerCase()
		if (an < bn) return -1
		if (an > bn) return 1
		return 0
	}//}}}
	static create(name, parentUUID) {// {{{
		return new Node({
			UUID: uuidv7(),
			Created: (new Date()).toISOString(),
			Content: '',
			Name: name,
			ParentUUID: parentUUID,
			Markdown: false,
			History: false,
		})
	}// }}}

	constructor(nodeData, level) {//{{{
		this.Level = level
		this.data = nodeData
		this.UUID = nodeData.UUID

		// Toplevel nodes are normalized to have the ROOT_NODE as parent.
		if (nodeData.UUID !== ROOT_NODE && nodeData.ParentUUID === '') {
			this.ParentUUID = ROOT_NODE
			this.data.ParentUUID = ROOT_NODE
		} else
			this.ParentUUID = nodeData.ParentUUID

		this._children_fetched = false
		this._has_children = null // this will be set by nodeStore.getTreeNodes
		this.Children = []
		this.Ancestors = []

		this._sibling_before = null
		this._sibling_after = null
		this._parent = null

		this.reset()
	}//}}}

	reset() {// {{{
		this._content = this.data.Content
		this._modified = false
	}// }}}
	get(prop) {//{{{
		return this.data[prop]
	}//}}}
	updated() {//{{{
		// '2024-12-17T17:33:48.85939Z
		return new Date(Date.parse(this.data.Updated))
	}//}}}
	isModified() {// {{{
		return this._modified
	}// }}}
	hasFetchedChildren() {//{{{
		return this._children_fetched
	}//}}}
	async fetchChildren() {//{{{
		this.Children = await nodeStore.getTreeNodes(this.UUID, this.Level + 1)
		this._children_fetched = true

		// Children are sorted to allow for storing siblings befare and after.
		// These are used with keyboard navigation in the tree.
		this.Children.sort(Node.sort)

		const numChildren = this.Children.length
		this.setHasChildren(numChildren > 0)
		for (let i = 0; i < numChildren; i++) {
			if (i > 0)
				this.Children[i]._sibling_before = this.Children[i - 1]
			if (i < numChildren - 1)
				this.Children[i]._sibling_after = this.Children[i + 1]
			this.Children[i]._parent = this
		}

		// Notify the tree that all children are fetched and ready to process.
		//_notes2.current.tree.fetchChildrenOn(this.UUID)
		_mbus.dispatch(`NODE_CHILDREN_FETCHED_${this.UUID}`)

		return this.Children
	}//}}}
	setHasChildren(v) {// {{{
		this._has_children = v
	}// }}}
	hasChildren() {//{{{
		return this._has_children
	}//}}}
	getSiblingBefore() {// {{{
		return this._sibling_before
	}// }}}
	getSiblingAfter() {// {{{
		return this._sibling_after
	}// }}}
	getParent() {//{{{
		return this._parent
	}//}}}
	isLastSibling() {//{{{
		return this._sibling_after === null
	}//}}}
	isFirstSibling() {//{{{
		return this._sibling_before === null
	}//}}}
	content() {//{{{
		/* TODO - implement crypto
		if (this.CryptoKeyID != 0 && !this._decrypted)
			this.#decrypt()
		*/
		return this._content
	}//}}}
	setContent(new_content) {//{{{
		this._content = new_content
		this._modified = true
		_mbus.dispatch('NODE_MODIFIED', { node: this })
	}//}}}
	setName(new_name) {// {{{
		if (new_name.trim() === '')
			throw new Error(`The name can't be empty`)

		this.data.Name = new_name
		this._modified = true
		_mbus.dispatch('NODE_MODIFIED', { node: this })
	}// }}}
	async save() {//{{{
		this.data.Content = this._content
		this.data.Updated = new Date().toISOString()
		this._modified = false

		_mbus.dispatch('NODE_UNMODIFIED')

		// When stored into database and ancestry was changed,
		// the ancestry path could be interesting.
		const ancestors = await nodeStore.getNodeAncestry(this)
		this.data.Ancestors = ancestors.map(a => a.get('Name')).reverse()
	}//}}}
}

// vim: foldmethod=marker
