import { Node } from 'node'

export const ROOT_NODE = '00000000-0000-0000-0000-000000000000'

export class NodeStore {
	constructor() {//{{{
		if (!('indexedDB' in window)) {
			throw 'Missing IndexedDB'
		}

		this.db = null
		this.nodes = {}
		this.sendQueue = null
		this.nodesHistory = null
		this.files = null
	}//}}}
	initializeDB() {//{{{
		return new Promise((resolve, reject) => {
			const req = indexedDB.open('notes', 8)

			// Schema upgrades for IndexedDB.
			// These can start from different points depending on updates to Notes2 since a device was online.
			req.onupgradeneeded = (event) => {
				let nodes
				let appState
				let sendQueue
				let nodesHistory
				let files
				const db = event.target.result
				const trx = event.target.transaction

				for (let i = event.oldVersion + 1; i <= event.newVersion; i++) {
					console.log(`Upgrade to schema ${i}`)

					// The schema transformations.
					switch (i) {
						case 1:
							nodes = db.createObjectStore('nodes', { keyPath: 'UUID' })
							nodes.createIndex('byName', 'Name', { unique: false })
							break

						case 2:
							trx.objectStore('nodes').createIndex('byParent', 'ParentUUID', { unique: false })
							break

						case 3:
							appState = db.createObjectStore('app_state', { keyPath: 'key' })
							break

						case 4:
							trx.objectStore('nodes').createIndex('byModified', 'modified', { unique: false })
							break

						case 5:
							sendQueue = db.createObjectStore('send_queue', { keyPath: 'ClientSequence', autoIncrement: true })
							sendQueue.createIndex('updated', 'Updated', { unique: false })
							break

						case 6:
							nodesHistory = db.createObjectStore('nodes_history', { keyPath: ['UUID', 'Updated'] })
							break

						case 7:
							trx.objectStore('nodes_history').createIndex('byUUID', 'UUID', { unique: false })
							break

						case 8:
							files = db.createObjectStore('files', { keyPath: 'UUID' })
							break
					}
				}
			}

			req.onsuccess = (event) => {
				this.db = event.target.result
				this.sendQueue = new SimpleNodeStore(this.db, 'send_queue')
				this.nodesHistory = new NodeHistoryStore(this.db, 'nodes_history')
				this.files = new SimpleNodeStore(this.db, 'files')
				this.initializeRootNode()
					.then(() => resolve())
			}

			req.onerror = (event) => {
				reject(event.target.error)
			}
		})
	}//}}}
	initializeRootNode() {//{{{
		return new Promise((resolve, reject) => {
			// The root node is a magical node which displays as the first node if none is specified.
			// If not already existing, it will be created.
			const trx = this.db.transaction('nodes', 'readwrite')
			const nodes = trx.objectStore('nodes')
			const getRequest = nodes.get(ROOT_NODE)
			getRequest.onsuccess = (event) => {
				// Root node exists - nice!
				if (event.target.result !== undefined) {
					resolve(event.target.result)
					return
				}

				const putRequest = nodes.put({
					UUID: ROOT_NODE,
					Name: 'Notes2',
					Content: 'Hello, World!',
					Updated: new Date().toISOString(),
					ParentUUID: '',
				})
				putRequest.onsuccess = (event) => {
					resolve(event.target.result)
				}
				putRequest.onerror = (event) => {
					reject(event.target.error)
				}
			}
			getRequest.onerror = (event) => reject(event.target.error)
		})
	}//}}}
	purgeCache() {//{{{
		this.nodes = {}
	}//}}}

	node(uuid, dataIfUndefined, newLevel) {//{{{
		let n = this.nodes[uuid]
		if (n === undefined && dataIfUndefined !== undefined)
			n = this.nodes[uuid] = new Node(dataIfUndefined, newLevel)
		return n
	}//}}}

	getAppState(key) {//{{{
		return new Promise((resolve, reject) => {
			const trx = this.db.transaction('app_state', 'readonly')
			const appState = trx.objectStore('app_state')
			const getRequest = appState.get(key)
			getRequest.onsuccess = (event) => {
				if (event.target.result !== undefined) {
					resolve(event.target.result)
				} else {
					resolve(null)
				}
			}
			getRequest.onerror = (event) => reject(event.target.error)
		})
	}//}}}
	setAppState(key, value) {//{{{
		return new Promise((resolve, reject) => {
			try {
				const t = this.db.transaction('app_state', 'readwrite')
				const appState = t.objectStore('app_state')
				t.onerror = (event) => {
					console.log('transaction error', event.target.error)
					reject(event.target.error)
				}
				t.oncomplete = () => {
					resolve()
				}

				const record = { key, value }
				const addReq = appState.put(record)
				addReq.onerror = (event) => {
					console.log('Error!', event.target.error, key, value)
				}
			} catch (e) {
				reject(e)
			}
		})
	}//}}}

	getTreeNodes(parent, newLevel) {//{{{
		return new Promise((resolve, reject) => {
			// Parent of toplevel nodes is ROOT_NODE in indexedDB.
			// Only the root node has '' as parent.
			let storeParent = parent

			const trx = this.db.transaction('nodes', 'readonly')
			const nodeStore = trx.objectStore('nodes')
			const index = nodeStore.index('byParent')
			const req = index.getAll(storeParent)

			const hasChildrenPromises = []
			req.onsuccess = (event) => {
				const nodes = []
				for (const i in event.target.result) {
					const nodeData = event.target.result[i]
					const node = this.node(nodeData.UUID, nodeData, newLevel)

					// Look for the key of any children, a hopefully fast way
					// to tell if any children exists at all and this node is a
					// "folder". Needed quite early on for sorting.
					const promise = new Promise((resolve, reject) => {
						const countReq = index.getKey(nodeData.UUID)
						countReq.onsuccess = event => {
							node.setHasChildren(event.target.result !== undefined)
							resolve()
						}
					})
					hasChildrenPromises.push(promise)
					nodes.push(node)
				}

				Promise.all(hasChildrenPromises)
					.then(() => {
						resolve(nodes)
					})
			}
			req.onerror = (event) => reject(event.target.error)
		})
	}//}}}
	search(searchfor, parent) {//{{{
		return new Promise((resolve, reject) => {
			const trx = this.db.transaction('nodes', 'readonly')
			const nodeStore = trx.objectStore('nodes')
			const index = nodeStore.index('byParent')
			const req = index.getAll(parent)
			req.onsuccess = async (event) => {
				let nodes = []
				for (const i in event.target.result) {
					const nodeData = event.target.result[i]
					const children = await this.search(searchfor, nodeData.UUID)

					// Data is searched for the provided text.
					// TODO: implement an array of words to search?
					if (nodeData.Content.toLowerCase().includes(searchfor))
						nodes.push({
							uuid: nodeData.UUID,
							name: nodeData.Name,
							ancestry: await this.getNodeAncestry(nodeData, []),
						})

					nodes = nodes.concat(children)
				}
				resolve(nodes)
				return
			}
			req.onerror = (event) => reject(event.target.error)
		})
	}//}}}

	add(records, objstore) {//{{{
		return new Promise((resolve, reject) => {
			try {
				// A nodestore can be provided in order to
				// avoid creating new transactions.
				let nodeStore = objstore
				let t

				if (nodeStore === undefined) {
					t = this.db.transaction('nodes', 'readwrite')
					nodeStore = t.objectStore('nodes')

					t.oncomplete = (_event) => {
						resolve()
					}

					t.onerror = (event) => {
						console.error('transaction error', event.target.error)
						reject(event.target.error)
					}
				}

				// records is an object, not an array.
				for (const recordIdx in records) {
					const record = records[recordIdx]
					nodeStore.put(record.data)
				}

				resolve()
			} catch (e) {
				console.error(e)
				reject(e)
			}
		})
	}//}}}
	get(uuid, suppliedNodestore) {//{{{
		return new Promise((resolve, reject) => {
			// A nodestore can be provided in order to
			// avoid creating new transactions.
			let trx
			let nodeStore = suppliedNodestore

			if (nodeStore === undefined) {
				trx = this.db.transaction('nodes', 'readonly')
				nodeStore = trx.objectStore('nodes')
			}

			const getRequest = nodeStore.get(uuid)

			getRequest.onsuccess = (event) => {
				// Node not found in IndexedDB.
				if (event.target.result === undefined) {
					reject("No such node")
					return
				}
				const node = this.node(uuid, event.target.result, -1)
				resolve(node)
			}
		})
	}//}}}
	getNodeAncestry(node, accumulated) {//{{{
		return new Promise((resolve, reject) => {
			if (accumulated === undefined)
				accumulated = []

			const nodeParentIndex = this.db
				.transaction('nodes', 'readonly')
				.objectStore('nodes')

			if (node.UUID === ROOT_NODE || node.ParentUUID === ROOT_NODE) {
				resolve(accumulated)
				return
			}

			const getRequest = nodeParentIndex.get(node.ParentUUID)
			getRequest.onsuccess = (event) => {
				// Node not found in IndexedDB.
				// Not expected to happen.
				const parentNodeData = event.target.result
				if (parentNodeData === undefined) {
					reject("No such node")
					return
				}

				const parentNode = this.node(parentNodeData.UUID, parentNodeData, -1)
				this.getNodeAncestry(parentNode, accumulated.concat(parentNode))
					.then(accumulated => resolve(accumulated))
			}
		})

	}//}}}
	newTransaction(objectStore, mode) {// {{{
		return this.db.transaction(objectStore, mode)
	}// }}}

	nodeCount() {//{{{
		return new Promise((resolve, reject) => {
			const t = this.db.transaction('nodes', 'readwrite')
			const nodeStore = t.objectStore('nodes')
			const countReq = nodeStore.count()
			countReq.onsuccess = event => {
				resolve(event.target.result)
			}
		})
	}//}}}
}

class SimpleNodeStore {
	constructor(db, storeName) {//{{{
		this.db = db
		this.storeName = storeName
	}//}}}
	add(node) {//{{{
		return new Promise((resolve, reject) => {
			const t = this.db.transaction(['nodes', this.storeName], 'readwrite')
			const store = t.objectStore(this.storeName)
			t.onerror = (event) => {
				console.log('transaction error', event.target.error)
				reject(event.target.error)
			}

			// Node to be moved is first stored in the new queue.
			const req = store.put(node.data)
			req.onsuccess = () => {
				resolve()
			}
			req.onerror = (event) => {
				console.log(`Error adding ${node.UUID}`, event.target.error)
				reject(event.target.error)
			}
		})
	}//}}}
	get(key) {//{{{
		return new Promise((resolve, _reject) => {
			const req = this.db
				.transaction(['nodes', this.storeName], 'readonly')
				.objectStore(this.storeName)
				.get(key)

			req.onsuccess = (event) => {
				resolve(event.target.result)
			}
		})
	}//}}}
	retrieve(limit) {//{{{
		return new Promise((resolve, _reject) => {
			const cursorReq = this.db
				.transaction(['nodes', this.storeName], 'readonly')
				.objectStore(this.storeName)
				.index('updated')
				.openCursor()

			let retrieved = 0
			const nodes = []

			cursorReq.onsuccess = (event) => {
				const cursor = event.target.result
				if (!cursor) {
					resolve(nodes)
					return
				}
				retrieved++
				nodes.push(cursor.value)
				if (retrieved === limit) {
					resolve(nodes)
					return
				}

				cursor.continue()
			}
		})
	}//}}}
	delete(keys) {//{{{
		const store = this.db
			.transaction(['nodes', this.storeName], 'readwrite')
			.objectStore(this.storeName)

		const promises = []
		for (const key of keys) {
			const p = new Promise((resolve, reject) => {
				// TODO - implement a way to add an error to a page-global error log.
				const request = store.delete(key)
				request.onsuccess = (event) => resolve(event)
				request.onerror = (event) => reject(event)
			})
			promises.push(p)
		}
		return Promise.all(promises)
	}//}}}
	count() {//{{{
		const store = this.db
			.transaction(['nodes', this.storeName], 'readonly')
			.objectStore(this.storeName)
		return new Promise((resolve, reject) => {
			const request = store.count()
			request.onsuccess = (event) => resolve(event.target.result)
			request.onerror = (event) => reject(event.target.error)
		})
	}//}}}
}

class NodeHistoryStore extends SimpleNodeStore {
	constructor(db, storeName) {//{{{
		super(db, storeName)
	}//}}}
	count(uuid) {//{{{
		if (uuid === undefined)
			return super.count()

		const index = this.db
			.transaction(['nodes', this.storeName], 'readonly')
			.objectStore(this.storeName)
			.index('byUUID')

		return new Promise((resolve, reject) => {
			const request = index.count(uuid)
			request.onsuccess = (event) => resolve(event.target.result)
			request.onerror = (event) => reject(event.target.error)
		})
	}//}}}
	hasNode(uuid, updated) {// {{{
		return new Promise((resolve, reject) => {
			const req = this.db
				.transaction(['nodes', this.storeName], 'readonly')
				.objectStore(this.storeName)
				.getKey([uuid, updated])

			req.onsuccess = (event) => {
				resolve(event.target.result !== undefined)
			}

			req.onerror = (event) => {
				console.log(event.target.error)
				reject(event.target.error)
			}
		})
	}// }}}
	retrievePage(uuid, perPage, page) {// {{{
		return new Promise((resolve, _reject) => {
			const cursor = this.db
				.transaction(['nodes', this.storeName], 'readonly')
				.objectStore(this.storeName)
				.index('byUUID')
				.openCursor(uuid, 'prev')

			let retrieved = 0
			let first = true
			const nodes = []

			cursor.onsuccess = (event) => {
				const cursor = event.target.result
				if (!cursor) {
					resolve(nodes)
					return
				}

				// openCursor returns the first value which is only useful
				// if the first page is requested.
				if (page == 1 || !first) {
					retrieved++
					nodes.push(new Node(cursor.value))
					if (retrieved === perPage) {
						resolve(nodes)
						return
					}
					cursor.continue()
					return
				}

				// Jump to the start of the requested page.
				// Minus one since the first record was already returned.
				if (page > 1 && first) {
					first = false
					cursor.advance((perPage * (page - 1)))
					return
				}
			}
		})
	}// }}}
}

export function uuidv7() {// {{{
	// random bytes
	const value = new Uint8Array(16)
	crypto.getRandomValues(value)

	// current timestamp in ms
	const timestamp = BigInt(Date.now())

	// timestamp
	value[0] = Number((timestamp >> 40n) & 0xffn)
	value[1] = Number((timestamp >> 32n) & 0xffn)
	value[2] = Number((timestamp >> 24n) & 0xffn)
	value[3] = Number((timestamp >> 16n) & 0xffn)
	value[4] = Number((timestamp >> 8n) & 0xffn)
	value[5] = Number(timestamp & 0xffn)

	// version and variant
	value[6] = (value[6] & 0x0f) | 0x70
	value[8] = (value[8] & 0x3f) | 0x80

	const str = Array.from(value)
		.map((b) => b.toString(16).padStart(2, "0"))
		.join("")
	return `${str.slice(0, 8)}-${str.slice(8, 12)}-${str.slice(12, 16)}-${str.slice(16, 20)}-${str.slice(20)}`
}// }}}

// vim: foldmethod=marker
