import { Editor } from '@editor'
import { MessageBus } from '@mbus'

export class App {
	constructor() {// {{{
		window.mbus = new MessageBus()
		this.editor = null
		this.typesList = null
		this.currentNode = null
		this.currentNodeID = null
		this.types = []
		this.currentPage = null
		this.tree = new Tree(document.getElementById('nodes'))

		const events = [
			'EDITOR_NODE_SAVE',
			'MENU_ITEM_SELECTED',
			'NODE_CREATE_DIALOG',
			'NODE_DELETE',
			'NODE_EDIT_NAME',
			'NODE_MOVE',
			'NODE_REMOVED',
			'NODE_SELECTED',
			'TREE_RELOAD_NODE',
			'TYPES_LIST_FETCHED',
		]
		for (const eventName of events)
			mbus.subscribe(eventName, event => this.eventHandler(event))

		document.addEventListener('keydown', event => this.keyHandler(event))

		mbus.dispatch('MENU_ITEM_SELECTED', 'node')
	}// }}}

	eventHandler(event) {// {{{
		switch (event.type) {
			case 'MENU_ITEM_SELECTED':
				const item = document.querySelector(`#menu [data-section="${event.detail}"]`)
				this.page(item, event.detail)
				break

			case 'NODE_SELECTED':
				for (const n of document.querySelectorAll('#nodes .node.selected'))
					n.classList.remove('selected')

				if (event.detail !== null)
					for (const n of document.querySelectorAll(`#nodes .node[data-node-id="${event.detail}"]`))
						n.classList?.add('selected')

				this.edit(event.detail)
				break

			case 'NODE_DELETE':
				if (!confirm('Are you sure you want to delete this node?'))
					return
				this.nodeDelete(this.currentNode.ID)
				break

			case 'NODE_MOVE':
				const nodes = this.tree.markedNodes()
				if (!confirm(`Are you sure you want to move ${nodes.length} nodes here?`))
					return
				this.nodesMove(nodes, this.currentNode.ID)
				break

			case 'NODE_REMOVED':
				// Event dispatched when a tree node is removed after an update.
				if (this.currentNode.ID !== event.detail)
					return
				mbus.dispatch('NODE_SELECTED', null)
				break

			case 'EDITOR_NODE_SAVE':
				this.nodeUpdate()
				break

			case 'TYPES_LIST_FETCHED':
				const types = document.getElementById('types')
				types.replaceChildren(this.typesList.render())
				break

			case 'NODE_CREATE_DIALOG':
				if (this.currentPage !== 'node' || this.currentNode === null)
					return

				new NodeCreateDialog(this.currentNode.ID)
				break

			case 'NODE_EDIT_NAME':
				const newName = prompt('Rename node', this.currentNode.Name)
				if (newName === null)
					return

				this.nodeRename(this.currentNode.ID, newName)
					.then(() => mbus.dispatch('TREE_RELOAD_NODE', { parentNodeID: this.currentNode.ParentID }))
				break

			case 'TREE_RELOAD_NODE':
				this.tree.updateNode(parseInt(event.detail.parentNodeID))
					.then(() => {
						if (event.detail.callback)
							event.detail.callback()
					})
					.catch(err => showError(err))
				break

			default:
				alert(`Unhandled event: ${event.type}`)
				console.log(event)
		}
	}// }}}
	keyHandler(event) {// {{{
		if (!event.shiftKey || !event.altKey)
			return

		let handled = true
		switch (event.key.toUpperCase()) {
			case 'D':
				mbus.dispatch('NODE_DELETE')
				break

			case 'M':
				mbus.dispatch('NODE_MOVE')
				break

			case 'N':
				mbus.dispatch('NODE_CREATE_DIALOG')
				break

			default:
				handled = false
		}

		if (handled) {
			event.stopPropagation()
			event.preventDefault()
		}
	}// }}}
	page(item, name) {// {{{
		for (const el of document.querySelectorAll('#menu .item'))
			el.classList.remove('selected')
		item.classList.add('selected')

		for (const el of document.querySelectorAll('.page.show'))
			el.classList.remove('show')

		this.currentPage = name

		switch (name) {
			case 'node':
				document.getElementById('nodes').classList.add('show')
				document.getElementById('editor-node').classList.add('show')
				break

			case 'type':
				document.getElementById('types').classList.add('show')
				document.getElementById('editor-type-schema').classList.add('show')

				if (this.typesList === null)
					this.typesList = new TypesList()
				this.typesList.fetchTypes().then(() => {
					mbus.dispatch('TYPES_LIST_FETCHED')
				})

				break
		}
	}// }}}

	edit(nodeID) {// {{{
		if (nodeID === null) {
			document.getElementById('editor-node').style.display = 'none'
			this.currentNode = null
			return
		}

		fetch(`/nodes/${nodeID}`)
			.then(data => data.json())
			.then(json => {
				if (!json.OK) {
					showError(json.Error)
					return
				}

				this.currentNode = json.Node

				// The JSON editor is created each time. Could probably be reused.
				const editorEl = document.querySelector('#editor-node .editor')
				this.editor = new Editor(json.Node.TypeSchema)

				if (json.Node.Data['x-new'])
					editorEl.replaceChildren(this.editor.render(null))
				else
					editorEl.replaceChildren(this.editor.render(json.Node.Data))

				// Name is separate from the JSON node.
				const name = document.getElementById('editor-node-name')
				name.innerText = json.Node.Name

				// The editor-node div is hidden from the start as a lot of the elements
				// doesn't make any sense before a node is selected.
				document.getElementById('editor-node').style.display = 'grid'
			})
	}// }}}
	async nodeRename(nodeID, name) {// {{{
		return new Promise((resolve, reject) => {
			name = name.trim()
			if (name.length === 0) {
				alert('A name must be provided.')
				return
			}

			fetch(`/nodes/rename/${nodeID}`, {
				method: 'POST',
				body: JSON.stringify({
					Name: name,
				}),
			})
				.then(data => data.json())
				.then(json => {
					if (!json.OK) {
						showError(json.Error)
						return
					}

					this.edit(nodeID)
					resolve()
				})
				.catch(err => reject(err))
		})
	}// }}}
	nodeUpdate() {// {{{
		if (this.editor === null)
			return

		const btn = document.querySelector('#editor-node .controls button')
		btn.disabled = true
		const buttonPressed = Date.now()

		const nodeData = this.editor.data()

		fetch(`/nodes/update/${this.currentNode.ID}`, {
			method: 'POST',
			body: JSON.stringify(nodeData),
		})
			.then(data => data.json())
			.then(json => {
				if (!json.OK) {
					showError(json.Error)
					return
				}

				const timePassed = Date.now() - buttonPressed
				if (timePassed < 250)
					setTimeout(() => btn.disabled = false, 250 - timePassed)
				else
					btn.disabled = false
			})
	}// }}}
	nodeDelete(nodeID) {// {{{
		fetch(`/nodes/delete/${nodeID}`)
			.then(data => data.json())
			.then(json => {
				if (!json.OK) {
					showError(json.Error)
					return
				}
			})
			.catch(err => showError(err))
	}// }}}
	nodesMove(nodes, newParentID) {// {{{
		const req = {
			NewParentID: parseInt(newParentID),
			NodeIDs: nodes.map(n => n.ID),
		}
		fetch(`/nodes/move`, {
			method: 'POST',
			body: JSON.stringify(req),
		})
			.then(data => data.json())
			.then(async json => {
				if (!json.OK) {
					showError(json.Error)
					return
				}

				const parentTreenode = this.tree.treeNodes.get(newParentID)
				const node = await this.tree.fetchNodes(newParentID)
				parentTreenode.node = node
				parentTreenode.updateExpandImages()

				const newParentElement = this.tree.treeNodes.get(newParentID).children
				for (const n of nodes) {
					const movedTreeNode = this.tree.treeNodes.get(n.ID)
					newParentElement.append(movedTreeNode.element)

					// Moved nodes' parents are updated to remove the moved nodes from their children.
					const treenode = this.tree.treeNodes.get(n.ParentID)
					const node = await this.tree.fetchNodes(n.ParentID)
					treenode.node = node
					treenode.updateExpandImages()

					// Children are resorted to get the moved node into correct order.
					/*
					this.tree.sortChildren(parentTreenode.node.Children)
					for (const c of parentTreenode.node.Children) {
						const treenode = this.tree.treeNodes.get(c.ID)
						if (treenode)
							parentTreenode.children.append(treenode.element)
					}
					*/

					// The moved node is updated with its new parent ID for future moves.
					movedTreeNode.node.ParentID = newParentID
				}

			})
			.catch(err => showError(err))
	}// }}}
}

class NodeCreateDialog {
	constructor(parentNodeID) {// {{{
		this.parentNodeID = parentNodeID
		this.dialog = null
		this.types = null
		this.select = null
		this.input = null

		this.createElements()

		this.fetchTypes()
			.then(() => {
				const st = new SelectType(this.types)
				this.select.replaceChildren(st.render())
			})

		this.dialog.showModal()
		this.select.focus()
	}// }}}
	createElements() {// {{{
		this.dialog = document.createElement('dialog')
		this.dialog.id = 'create-type'
		this.dialog.innerHTML = `
			<div style="padding: 16px">
				<select></select>
				<input type="text" placeholder="Name">
				<button>Create</button>
			</div>
		`

		this.dialog.querySelector('button').addEventListener('click', () => this.commit())
		this.select = this.dialog.querySelector('select')
		this.input = this.dialog.querySelector('input')
		this.input.addEventListener('keydown', event => {
			if (event.key === 'Enter')
				this.commit()
		})

		document.body.appendChild(this.dialog)
	}// }}}
	commit() {// {{{
		if (this.input.value.trim().length === 0) {
			alert('Give a name.')
			return
		}

		const req = {
			ParentNodeID: this.parentNodeID,
			TypeID: parseInt(this.select.value),
			Name: this.input.value.trim(),
		}

		fetch('/nodes/create', {
			method: 'POST',
			body: JSON.stringify(req),
		})
			.then(data => data.json())
			.then(json => {
				if (!json.OK) {
					showError(json.Error)
					return
				}
				mbus.dispatch('TREE_RELOAD_NODE', {
					parentNodeID: this.parentNodeID,
					callback: () => mbus.dispatch('NODE_SELECTED', json.NodeID)
				})
				this.dialog.close()
			})
			.catch(err => showError(err))
	}// }}}
	async fetchTypes() {// {{{
		return new Promise((resolve, reject) => {
			fetch('/types/')
				.then(data => data.json())
				.then(json => {
					if (!json.OK) {
						showError(json.Error)
						return
					}
					this.types = json.Types
					resolve()
				})
				.catch(err => reject(err))
		})
	}// }}}
}


class SelectType {
	constructor(types) {// {{{
		this.types = types
	}// }}}
	render() {// {{{
		const tmpl = document.createElement('template')

		this.types.sort(typeSort)
		let prevGroup = null
		for (const t of this.types) {
			if (t.Name == 'root_node')
				continue

			if (t.Schema['x-group'] != prevGroup) {
				prevGroup = t.Schema['x-group']
				const group = document.createElement('optgroup')
				group.setAttribute('label', t.Schema['x-group'])
				tmpl.content.appendChild(group)
			}

			const opt = document.createElement('option')
			opt.setAttribute('value', t.ID)
			opt.innerText = t.Schema.title || t.Name
			tmpl.content.appendChild(opt)
		}

		return tmpl.content
	}// }}}
}

export class Tree {
	constructor() {// {{{
		this.treeNodes = new Map()

		const events = [
			'NODE_EXPAND',
		]
		for (const e of events)
			mbus.subscribe(e, event => this.eventHandler(event))


		// click on the empty tree list to unmark all nodes.
		const nodesEl = document.getElementById('nodes')
		nodesEl.addEventListener('click', event => {
			// To prevent accidentally removing all node marks,
			// shift is required to be unpressed, since it is required to
			// be pressed when marking nodes.
			if (event.shiftKey)
				return

			const markedElements = document.querySelectorAll('#nodes .node.marked')
			for (const e of markedElements)
				e.classList.remove('marked')
		})

		// Fetch the top node to start
		this.fetchNodes(0)
			.then(node => {
				const top = document.getElementById('nodes')
				const topNode = new TreeNode(node)
				this.treeNodes.set(node.ID, topNode)
				top.appendChild(topNode.render())
				this.updateNode(0)
			})
			.catch(err => showError(err))
	}// }}}
	eventHandler(event) {// {{{
		switch (event.type) {
			case 'NODE_EXPAND':
				this.updateNode(event.detail.node.ID)
				break

			default:
				alert(`Unhandled event: ${event.type}`)
				console.log(event)
		}
	}// }}}
	async fetchNodes(topNode) {// {{{
		return new Promise((resolve, reject) => {
			fetch(`/nodes/tree/${topNode}?depth=1`)
				.then(data => data.json())
				.then(json => {
					if (!json.OK) {
						reject(json.Error)
						return
					}
					resolve(json.Nodes)
				})
		})
	}// }}}
	updateNode(nodeID) {// {{{
		return new Promise((resolve, reject) => {
			// updateNode retrieves a node and its' immediate children.
			// Node and each child is found in the treeNodes map and the names are updated.
			// If not found, created and added.
			//
			// Newly created nodes are found and added, existing but renamed nodes are modified, and unchanged are left as is.
			this.fetchNodes(nodeID)
				.then(node => {
					const thisTreeNode = this.treeNodes.get(nodeID)
					thisTreeNode.childrenFetched = true
					thisTreeNode.node = node
					thisTreeNode.updateExpandImages()
					thisTreeNode.toggleExpand(true)

					// Children are sorted according to type and name.
					this.sortChildren(node.Children)

					// Deleted or moved children
					for (const c of thisTreeNode.children.children) {
						const nodeID = parseInt(c.dataset.nodeId)
						const nodeStillExist = node.Children.some(n => n.ID === nodeID)
						if (!nodeStillExist) {
							c.remove()
							mbus.dispatch('NODE_REMOVED', nodeID)
						}

					}

					// Update or add children
					for (const n of node.Children) {
						if (this.treeNodes.has(n.ID)) {
							const treenode = this.treeNodes.get(n.ID)
							treenode.node = n
							treenode.element.querySelector('.name').innerText = n.Name
							treenode.updateExpandImages()
						} else {
							const treenode = new TreeNode(n)
							this.treeNodes.set(n.ID, treenode)
							thisTreeNode.children.appendChild(treenode.render())
						}
					}

					resolve()

				})
				.catch(err => { showError(err); reject(err) })
		})
	}// }}}
	sortChildren(children) {// {{{
		children.sort((a, b) => {
			if (a.TypeName < b.TypeName) return -1
			if (a.TypeName > b.TypeName) return 1

			if (a.Name < b.Name) return -1
			if (a.Name > b.Name) return 1

			return 0
		})
	}// }}}
	markedNodes() {// {{{
		const markedElements = document.querySelectorAll('#nodes .node.marked')
		const marked = []
		for (const n of markedElements) {
			const nodeID = n.getAttribute('data-node-id')
			marked.push(this.treeNodes.get(parseInt(nodeID)).node)
		}
		return marked
	}// }}}
}

export class TreeNode {
	constructor(data) {// {{{
		this.node = data
		this.childrenFetched = false
		this.element = null
		this.children = null
		this.expandEventListenerAdded = false
	}// }}}

	render() {// {{{
		const nodeHTML = `
			<div class="expand-status"><img /></div>
			<div class="type-icon"><img /></div>
			<div class="name">${this.name()}</div>
			<div class="children"></div>
		`

		const div = document.createElement('div')
		div.classList.add('node')
		div.setAttribute('data-node-id', this.node.ID)
		div.innerHTML = nodeHTML

		this.children = div.querySelector('.children')
		this.expandImg = div.querySelector('.expand-status img')

		div.querySelector('.name').addEventListener('click', event => {
			if (!event.shiftKey)
				mbus.dispatch('NODE_SELECTED', this.node.ID)
			else
				this.element.classList.toggle('marked')
			event.stopPropagation()
		})

		// data.NumChildren is set regardless of having fetched the children or not.
		this.expandStatus = div.querySelector('.expand-status img')
		this.updateExpandImages()

		if (this.node.TypeIcon) {
			const img = div.querySelector('.type-icon img')
			img.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/${this.node.TypeIcon}.svg`)
		}

		this.element = div
		return div
	}// }}}
	name() {// {{{
		if (this.node.TypeName === 'root_node')
			return 'Start'
		return this.node.Name
	}// }}}
	hasChildren() {// {{{
		return this.node.NumChildren > 0
	}// }}}
	updateExpandImages() {// {{{
		if (this.hasChildren()) {
			this.expandStatus.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/plus-box-outline.svg`)

			if (!this.expandEventListenerAdded) {
				this.expandStatus.addEventListener('click', () => this.toggleExpand())
				this.expandEventListenerAdded = true
			}
		} else {
			this.expandStatus.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/circle-medium.svg`)
		}
	}// }}}
	toggleExpand(expanded) {// {{{
		const node = this.element

		if (expanded === undefined)
			node?.classList.toggle('expanded')
		else if (expanded === true)
			node?.classList.add('expanded')
		else
			node?.classList.remove('expanded')

		const img = node?.classList.contains('expanded') ? 'minus-box-outline' : 'plus-box-outline'
		this.expandStatus.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/${img}.svg`)

		if (!this.childrenFetched && this.node.NumChildren > 0 && this.node.Children.length == 0) {
			mbus.dispatch('NODE_EXPAND', this)
		}
	}// }}}
	async fetchChildren() {// {{{
		return new Promise((resolve, reject) => {
			fetch(`/nodes/tree/${this.node.ID}?depth=1`)
				.then(data => data.json())
				.then(json => {
					if (json.OK)
						resolve(json.Nodes)
					else
						reject(json.Error)
				})
				.catch(err => reject(err))
		})
	}// }}}
}

export class TypesList {
	constructur() {// {{{
		this.types = []
	}// }}}
	async fetchTypes() {// {{{
		return new Promise((resolve, reject) => {
			fetch('/types/')
				.then(data => data.json())
				.then(json => {
					if (!json.OK) {
						showError(json.Error)
						return
					}
					this.types = json.Types
					resolve()
				})
				.catch(err => reject(err))
		})
	}// }}}
	render() {// {{{
		const div = document.createElement('div')

		this.types.sort(typeSort)

		let prevGroup = null

		for (const t of this.types) {
			if (t.Name == 'root_node')
				continue

			if (t.Schema['x-group'] != prevGroup) {
				prevGroup = t.Schema['x-group']
				const group = document.createElement('div')
				group.classList.add('group')
				group.innerText = t.Schema['x-group']
				div.appendChild(group)
			}

			const tDiv = document.createElement('div')
			tDiv.classList.add('type')
			tDiv.innerHTML = `
				<div class="img"><img src="/images/${_VERSION}/node_modules/@mdi/svg/svg/file-document-check-outline.svg" /></div>
				<div class="title">${t.Schema.title || t.Name}</div>
				`

			div.appendChild(tDiv)
		}

		return div
	}// }}}
}

function typeSort(a, b) {// {{{
	if (a.Schema['x-group'] === undefined)
		a.Schema['x-group'] = 'No group'

	if (b.Schema['x-group'] === undefined)
		b.Schema['x-group'] = 'No group'

	if (a.Schema['x-group'] < b.Schema['x-group']) return -1
	if (a.Schema['x-group'] > b.Schema['x-group']) return 1

	if ((a.Schema.title || a.Name) < (b.Schema.title || b.Name)) return -1
	if ((a.Schema.title || a.Name) > (b.Schema.title || b.Name)) return 1

	return 0
}// }}}

// vim: foldmethod=marker
