import { ROOT_NODE, Node } from 'node_store'
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
import { N2Sidebar } from 'sidebar'
import { N2PreferenceSet } from './page_preferences.mjs'

export class App {
	static PAGES = ['node', 'history', 'storage']

	constructor() {// {{{
		this.currentNode = null
		this.sidebar = new N2Sidebar()
		this.crumbs = new N2Crumbs()
		this.crumbsElement = document.getElementById('crumbs')
		this.nodeUI = document.getElementById('note')
		this.dragIcon = new N2DragIcon()

		this.preferences = this.getPreferences()

		this.sidebar.render().then(sidebar => {
			document.getElementById('tree').append(sidebar)
			document.getElementById('tree-nodes')?.focus()
		})

		// Start node shows a system-wide page instead of node editing
		// since the start node is kind of magic and doesn't fit into
		// the syncing system.
		const determineNodePage = uuid => {
			const el = document.getElementById('notes2')
			if (uuid == ROOT_NODE)
				el.classList.add('root-node-override')
			else
				el.classList.remove('root-node-override')
		}

		_mbus.subscribe('TREE_RENDERED', async () => {
			// Subscribing to the start node existing after the tree trunk is
			// fetched since the NODE_COMPONENT_EXIST message isn't sent for the
			// root node itself, and the root node should be selected in the tree
			// after it is rendered when the site is shown without UUID in the URL.
			const startNode = await this.getStartNode()
			determineNodePage(startNode.UUID)
			this.goToNode(startNode.UUID, false, false)
		})

		_mbus.subscribe('TREE_NODE_SELECTED', event => {
			const node = event.detail.data
			determineNodePage(node.UUID)
			this.goToNode(node.UUID, false, false)
		})

		_mbus.subscribe('GO_TO_NODE', event => {
			const node = event.detail.data
			determineNodePage(node.nodeUUID)
			this.goToNode(node.nodeUUID, node.dontPush, node.dontExpand)
		})

		_mbus.subscribe('SHOW_PAGE', ({ detail: { data: { page } } }) => {
			const classList = document.getElementById('notes2').classList
			classList.forEach(e => {
				if (e.startsWith('page-'))
					classList.remove(e)
			})
			classList.add('page-' + page)
		})

		_mbus.subscribe('DEVICE_PREFERENCE_SET_UPDATED', ()=>{
			this.preferences = this.getPreferences()
		})

		globalThis.addEventListener('keydown', event => this.keyHandler(event))
		globalThis.addEventListener('popstate', event => this.popState(event))
		document.getElementById('notes2').addEventListener('click', event => {
			if (event.target.id === 'notes2')
				document.getElementById('node-content')?.focus()
		})

		document.querySelector('#page-root .create').addEventListener('click', () => this.createNode())
		document.body.append(this.dragIcon)

		_mbus.dispatch('SHOW_PAGE', { page: 'node' })

		globalThis._sync = new Sync()

		// I think it is uncomfortable having the sync running as soon as the page load.
		// I haven't gotten the time to look at the page before stuff jumps around.
		// There a slight delay to initiate sync seems reasonable.
		setTimeout(() => globalThis._sync.run(), 1000)
	}// }}}
	keyHandler(event) {//{{{
		let handled = true

		// Most keybindings is Alt+Shift, since the popular browsers at the time (2023) allows to override thees.
		// Ctrl+S is the exception to using Alt+Shift, since it is overridable and in such widespread use for saving.
		// Thus, the exception is acceptable to consequent use of alt+shift.
		const CTRL = !event.shiftKey && event.ctrlKey && !event.altKey
		const SHIFT_ALT = event.shiftKey && !event.ctrlKey && event.altKey
		const SHIFT_CTRL_ALT = event.shiftKey && event.ctrlKey && event.altKey

		switch (event.key.toUpperCase()) {
			case 'F2':
				this.nodeUI.renameNode()
				break
			case 'T':
				if (!SHIFT_ALT) { handled = false; break }
				if (document.activeElement.id === 'tree-nodes')
					this.nodeUI.takeFocus()
				else
					this.sidebar.focus()
				break

			case 'F':
				if (!SHIFT_ALT) { handled = false; break }
				_mbus.dispatch('op-search')
				break

			case 'M':
				if (!SHIFT_ALT) { handled = false; break }
				globalThis._mbus.dispatch('MARKDOWN_TOGGLE')
				break

			case 'N':
				if (SHIFT_ALT)
					this.createNode()
				else if (SHIFT_CTRL_ALT) {
					this.createNode(this.currentNode?.ParentUUID)
				} else {
					handled = false
				}
				break

			case 'S':
				if (!CTRL) { handled = false; break }
				this.nodeUI.saveNode()
				break

			default:
				handled = false
		}

		if (handled) {
			event.preventDefault()
			event.stopPropagation()
		}
	}//}}}
	popState(event) {// {{{
		_mbus.dispatch("GO_TO_NODE", { nodeUUID: event.state.nodeUUID, dontPush: true, dontExpand: true })
	}// }}}
	async getStartNode() {//{{{
		let nodeUUID = ROOT_NODE

		// Is a UUID provided on the URI as an anchor?
		const parts = document.URL.split('#')
		if (parts[1]?.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i))
			nodeUUID = parts[1]

		return await nodeStore.get(nodeUUID)
	}//}}}
	async saveNode() {//{{{

	}//}}}
	async moveNode(node, targetNodeUUID) {// {{{
		node.moveToParent(targetNodeUUID)
		await node.save()
	}// }}}
	async createNode(createUnderUUID) {//{{{
		const parentUUID = createUnderUUID ? createUnderUUID : this.currentNode.UUID
		const p = createUnderUUID ? 'Name for sibling document' : 'Name for sub-document'

		let name = prompt(p)
		if (!name)
			return

		const nn = Node.create(name, parentUUID)
		await nn.save()

		// Treenode is forcefully rerendered and children refetched to both show the new node
		// and to get it resorted.
		const parentTreenode = this.sidebar.getTreeNode(parentUUID)
		await parentTreenode.render(true, true)
		_mbus.dispatch('GO_TO_NODE', { nodeUUID: nn.UUID })
	}//}}}
	async goToNode(nodeUUID, dontPush, dontExpand) {//{{{
		if (nodeUUID === null || nodeUUID === undefined)
			return

		// Don't switch notes until saved.
		if (this.nodeUI.isModified()) {
			if (!confirm("Changes not saved. Do you want to discard changes?"))
				return
		}

		if (!dontPush)
			history.pushState({ nodeUUID }, '', `/#${nodeUUID}`)

		const node = nodeStore.node(nodeUUID)
		node.reset() // any modifications are discarded.

		this.currentNode = node
		this.sidebar.setSelected(node, dontExpand)

		const ancestors = await nodeStore.getNodeAncestry(node)

		// Scrolls node into view.
		// makeVisible normally expands all ancestor nodes to make the whole chain visible.
		// This is a bad idea when quickly navigating the tree, since the arrow navigation
		// has collapsed nodes which the event calling goToNode can come to undo, if the
		// event processing lags behind.
		await this.sidebar.makeVisible(node, ancestors, dontExpand)

		_mbus.dispatch('CRUMBS_SET', ancestors, () => this.crumbsElement.replaceChildren(this.crumbs.render()))
		_mbus.dispatch('NODE_UI_OPEN', node)
		_mbus.dispatch('TREE_EXPANSION', { expand: false, when: 'narrow' })
		_mbus.dispatch('NODE_UNMODIFIED')
		_mbus.dispatch('SHOW_PAGE', { page: 'node' })
	}//}}}
	pageIsVisible(page) {// {{{
		let classList = document.querySelector('#main-page').classList
		return classList.contains(page)
	}// }}}
	getPreferences() {// {{{
		const devPrefSet = localStorage.getItem('device_preference_set') || 'default'
		const userData = localStorage.getItem('user') || '{"default": {}}'
		const user = JSON.parse(userData)
		return new N2PreferenceSet(devPrefSet, user.Preferences[devPrefSet])
	}// }}}
}

class N2Crumbs extends CustomHTMLElement {
	static {// {{{
		this.tmpl = document.createElement('template')
		this.tmpl.innerHTML = `
		`
	}// }}}
	constructor() {// {{{
		super()
		this.classList.add('crumbs')

		this.crumbs = []

		_mbus.subscribe('CRUMBS_SET', event => {
			this.crumbs = event.detail.data
		})
	}// }}}
	render() {// {{{
		const crumbs = this.crumbs.map(node =>
			new N2Crumb(
				node.get('Name'),
				node.UUID,
			)
		)

		const start = new N2Crumb('', ROOT_NODE)
		crumbs.push(start)

		this.replaceChildren(...crumbs.reverse())
		return this
	}// }}}
}

class N2Crumb extends CustomHTMLElement {
	static {// {{{
		this.tmpl = document.createElement('template')
		this.tmpl.innerHTML = `
			<a data-el="link"></a>
		`
	}// }}}
	constructor(label, uuid) {// {{{
		super()

		// The house makes it a bit more graphical than just a bunch of text.
		if (uuid === ROOT_NODE) {
			const start = document.createElement('div')
			start.innerHTML = `<img src="/images/${_VERSION}/icon_home.svg">`
			start.addEventListener('click', () => _mbus.dispatch("GO_TO_NODE", { nodeUUID: ROOT_NODE, dontPush: false, dontExpand: true }))
			this.classList.add('home')
			this.replaceChildren(start)
			return
		}

		this.classList.add('crumb')
		this.label = label
		this.uuid = uuid

		this.elLink.href = `/#${this.uuid}`
		this.elLink.innerText = this.label
		this.elLink.addEventListener('click', () => _mbus.dispatch("GO_TO_NODE", { nodeUUID: this.uuid, dontPush: false, dontExpand: true }))
	}// }}}
}

function tmpl(html) {// {{{
	const el = document.createElement('template')
	el.innerHTML = html
	return el.content.children
}// }}}

class Op {
	constructor(id) {// {{{
		this.id = id
		_mbus.subscribe(this.id, p => this.render(p))
	}// }}}
	render(html) {// {{{
		const op = document.getElementById('op')
		const t = document.createElement('template')
		t.innerHTML = `<dialog id="${this.id}" class="op">${html}</dialog>`
		op.replaceChildren(t.content)
		document.getElementById(this.id).showModal()
	}// }}}
	get(selector) {// {{{
		return document.querySelector(`#${this.id} ${selector}`)
	}// }}}
	bind(selector, event, fn) {// {{{
		this.get(selector).addEventListener(event, evt => fn(evt))
	}// }}}
}

class OpSearch extends Op {
	constructor() {// {{{
		super('op-search')
	}// }}}
	render() {// {{{
		super.render(`
			<div class="header">Search</div>
			<div>
				<input type="text" />
			</div>
			<div class="header">Results</div>
			<div class="results"></div>
		`)

		this.bind('input[type="text"]', 'keydown', evt => this.search(evt))
	}// }}}
	search(event) {// {{{
		if (event.key !== 'Enter')
			return

		const searchFor = document.querySelector('#op-search input').value
		nodeStore.search(searchFor, ROOT_NODE)
			.then(res => this.displayResults(res))
	}// }}}
	displayResults(results) {// {{{
		const rs = []
		for (const r of results) {
			const ancestors = r.ancestry.reverse().map(a => {
				const div = tmpl(`<div class="ancestor">${a.data.Name}</div>`)
				div[0].addEventListener('click', () => _notes2.current.goToNode(a.UUID))
				return div[0]
			})


			const div = tmpl(`<div>${r.name}</div>`)
			div[0].addEventListener('click', () => _notes2.current.goToNode(r.uuid))
			rs.push(...div)

			const ancDev = tmpl('<div class="ancestors"></div>')
			ancDev[0].append(...ancestors)
			rs.push(ancDev[0])
		}
		this.get('.results').replaceChildren(...rs)
	}// }}}
}

class N2DragIcon extends CustomHTMLElement {
	static {// {{{
		this.tmpl = document.createElement('template')
		this.tmpl.innerHTML = `
			<style>
				:host {
					display: none;
					position: absolute;
					z-index: 16384;
					pointer-events: none;
				}
			</style>
			<img data-el="icon" src="/images/${_VERSION}/icon_drag.svg">
		`
	}// }}}
	constructor() {// {{{
		super(true)

		document.addEventListener('dragover', e => {
			this.style.left = `${e.clientX + 8}px`
			this.style.top = `${e.clientY}px`
		})

		this.dragSource = null
	}// }}}
	start() {// {{{
		this.style.display = 'block'
	}// }}}
	end() {// {{{
		this.style.display = 'none'
	}// }}}
	icon(name) {// {{{
		if (name != '')
			name = '_' + name
		this.elIcon.setAttribute('src', `/images/${_VERSION}/icon_drag${name}.svg`)
	}// }}}
	setSource(s) {// {{{
		this.dragSource = s
	}// }}}
	getSource() {// {{{
		return this.dragSource
	}// }}}
}

customElements.define('n2-crumbs', N2Crumbs)
customElements.define('n2-crumb', N2Crumb)
customElements.define('n2-dragicon', N2DragIcon)

// vim: foldmethod=marker
