export const ROOT_NODE = '00000000-0000-0000-0000-000000000000'
export const ORPHANED_NODE = '00000000-0000-0000-0000-000000000001'
export const DELETED_NODE = '00000000-0000-0000-0000-000000000002'

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) {// {{{
		const node = new Node({
			UUID: uuidv7(),
			Created: (new Date()).toISOString(),
			Content: '',
			Name: name,
			ParentUUID: parentUUID,
			Markdown: false,
		})

		// Newly created node (not constructed from existing data) is considered modified
		// since node.save returns early if it isn't modified.
		node._modified = true

		return node
	}// }}}

	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 is the runtime content in the editor.
		// this.data.Content is the original data.
		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 globalThis.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
		}

		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
	}//}}}
	moveToParent(newParentUUID) {// {{{
		if (this.UUID === newParentUUID)
			throw new Error("New parent UUID is the same as node UUID. Can't be your own parent.")

		this.ParentUUID = newParentUUID
		this.data.ParentUUID = newParentUUID
		this._modified = true
	}// }}}
	isLastSibling() {//{{{
		return this._sibling_after === null
	}//}}}
	isFirstSibling() {//{{{
		return this._sibling_before === null
	}//}}}
	isSpecial() {// {{{
		return this.data.Special
	}// }}}
	content() {//{{{
		// TODO - implement crypto
		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() {//{{{
		try {
			const dblink = /db:\/\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/gi

			// Just safeguarding not using the root node,
			// which sort of exist but isn't supposed to communicate to server.
			if (this.UUID == ROOT_NODE)
				return

			// File links from the original, unmodified content are found to compare to links from the new content.
			// All files (images) needs to be indexed to nodes in order to track orphaned files.
			const before = new Set([...this.data.Content.matchAll(dblink)].map(m => m[1]))
			const after = new Set([...this._content.matchAll(dblink)].map(m => m[1]))
			const removed = before.difference(after)
			const added = after.difference(before)

			await globalThis.nodeStore.filesNodelink.removeFileUUIDs(this.UUID, removed)
			await globalThis.nodeStore.filesNodelink.addFileUUIDs(this.UUID, added)

			// Authoritative content and metadata is set to prepare it for IndexedDB update.
			this.data.Content = this._content
			this.data.Updated = new Date().toISOString()
			this.data.HistoryUUID = uuidv7() // every time the node is saved a new history UUID identifies the changed node.

			_mbus.dispatch('NODE_UNMODIFIED')
			this._modified = false

			// 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()
			*/

			/* 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. */

			// Current node is added to history. It will be duplicated with the "nodes" store
			// for simplicity, to hopefully avoid bugs.
			const history = globalThis.nodeStore.nodesHistory.add(this)

			// Updated node is added to the send queue to be stored on server.

			const sendQueue = globalThis.nodeStore.sendQueue.add(this)

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

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

		} catch (err) {
			console.error(err)
			alert(err.message)
		}

		return
	}//}}}
}

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

		this.db = null
		this.nodes = {}
		this.sendQueue = null
		this.nodesHistory = null
		this.files = null
		this.filesMetadata = null

		this.initializeSpecialNodes()
	}//}}}
	initializeDB() {//{{{
		return new Promise((resolve, reject) => {
			const req = indexedDB.open('notes', 14)

			// 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', 'HistoryUUID'] })
							break

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

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

						case 9:
							db.createObjectStore('files_metadata', { keyPath: 'UUID' })
							break

						case 10:
							trx.objectStore('files_metadata').createIndex('byUploaded', 'uploaded', { unique: false })
							break

						case 11:
							db.createObjectStore('files_nodelink', { keyPath: 'UUID' })
							break

						case 12:
							trx.objectStore('files_nodelink').createIndex('byNodeCount', 'NodeCount', { unique: false })
							break

						case 13:
							db.createObjectStore('file_segments', { keyPath: ['UUID', 'Sequence'] })
							break

						case 14:
							db.deleteObjectStore('files')
							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.fileSegments = new FileSegmentsStore(this.db, 'file_segments')
				this.filesMetadata = new FilesMetadataStore(this.db, 'files_metadata')
				this.filesNodelink = new FileNodesLinkStore(this.db, 'files_nodelink')
				resolve()
			}

			req.onerror = (event) => {
				reject(event.target.error)
			}
		})
	}//}}}
	initializeSpecialNodes() {// {{{
		this.nodes[ROOT_NODE] = new Node({ UUID: ROOT_NODE, Name: 'Start', Special: true }, -1)
		this.nodes[DELETED_NODE] = new Node({ UUID: DELETED_NODE, Name: 'Deleted nodes', Special: true }, -1)
		this.nodes[ORPHANED_NODE] = new Node({ UUID: ORPHANED_NODE, Name: 'Orphaned nodes', Special: true }, -1)
	}// }}}

	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) => {
			switch (uuid) {
				case ROOT_NODE:
				case DELETED_NODE:
				case ORPHANED_NODE:
					resolve(this.nodes[uuid])
					return
			}

			// 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
			}

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

			if (node.UUID === ORPHANED_NODE || node.ParentUUID === ORPHANED_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)
			}
		})
	}//}}}

	async storeFile(file, reader, progressCallback) {// {{{
		const metadata = new FileMetadata(file)
		let processedBytes = 0
		let stream

		try {
			// Do potentially larger blocks.
			const BLOCKSIZE = 1048576
			let buffer = new Uint8Array(BLOCKSIZE)

			// Files from server got through fetch() will bring its own
			// reader without going through a stream.
			if (!reader) {
				stream = file.stream()
				reader = stream.getReader({ mode: 'byob' })
			}

			let seq = 0
			while (true) {
				const { done, value } = await reader.read(buffer)
				if (done) break

				processedBytes += value.length
				if (progressCallback)
					progressCallback(processedBytes)

				// Block is added as a file segment.
				await this.fileSegments.add({
					data: {
						UUID: metadata.UUID,
						Sequence: seq,
						Data: value,
					}
				})
				buffer = new Uint8Array(value.buffer)
				seq++
			}

			reader.releaseLock()
		} catch (e) {
			console.error(e)
			alert(e.message)
		}

		await this.filesMetadata.add({ data: metadata })

		return metadata
	}// }}}
	nextFileToSync() {// {{{
		return new Promise((resolve, reject) => {
			const req = this.db
				.transaction('files_metadata', 'readonly')
				.objectStore('files_metadata')
				.index('byUploaded')
				.get('only_on_device')

			req.onerror = (e) => reject(e)
			req.onsuccess = (e) => {
				const data = e.target.result

				// No more files to sync.
				if (data === undefined) {
					resolve(null)
					return
				}

				resolve(new FileMetadata(data))
			}
		})
	}// }}}
}

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.error(event.target.error)
				reject(event.target.error)
			}
		})
	}// }}}
	retrievePage(uuid, perPage, page) {// {{{
		return new Promise((resolve, _reject) => {

			const lowerBound = [uuid, '00000000-0000-0000-0000-000000000000']
			const upperBound = [uuid, 'ffffffff-ffff-ffff-ffff-ffffffffffff']
			const range = IDBKeyRange.bound(lowerBound, upperBound)

			const cursor = this.db
				.transaction(['nodes', this.storeName], 'readonly')
				.objectStore(this.storeName)
				.openCursor(range, '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
				}
			}
		})
	}// }}}
}

class FileSegmentsStore extends SimpleNodeStore {
	constructor(db, storeName) {//{{{
		super(db, storeName)
	}//}}}
	async getStream(uuid) {// {{{
		let seq = 0
		const meself = this

		return new ReadableStream({
			type: 'bytes',

			async pull(controller) {
				try {
					while (true) {
						const chunk = await meself.get([uuid, seq])
						if (!chunk) {
							controller.close()
							return
						}

						controller.enqueue(chunk.Data)
						seq++
					}
				} catch (e) {
					console.error(e)
				}
			}
		})
	}// }}}
}

class FilesMetadataStore extends SimpleNodeStore {
	countToUpload() {// {{{
		const store = this.db
			.transaction(this.storeName, 'readonly')
			.objectStore(this.storeName)
			.index('byUploaded')
		return new Promise((resolve, reject) => {
			const request = store.count('only_on_device')
			request.onsuccess = (event) => resolve(event.target.result)
			request.onerror = (event) => reject(event.target.error)
		})
	}// }}}
}

class FileNodesLinkStore extends SimpleNodeStore {
	async removeFileUUIDs(nodeUUID, fileUUIDs) {// {{{
		for (const fileUUID of fileUUIDs) {
			let nodelink = await this.get(fileUUID)

			// Nothing to remove from.
			if (nodelink === undefined)
				continue

			const idx = nodelink.Nodes.indexOf(nodeUUID)
			if (idx > -1)
				nodelink.Nodes.splice(idx, 1)

			// Maintains an index to quickly find orphaned files.
			nodelink.NodeCount = nodelink.Nodes.length

			await this.add({ data: nodelink })
		}
	}// }}}
	async addFileUUIDs(nodeUUID, fileUUIDs) {// {{{
		for (const fileUUID of fileUUIDs) {
			let nodelink = await this.get(fileUUID)

			// Needs to be initialized if not existing already.
			if (nodelink === undefined)
				nodelink = {
					UUID: fileUUID,
					Nodes: [nodeUUID],
				}

			// Can get called with the same UUID as already there.
			// No duplicates, only append if missing.
			if (!nodelink.Nodes.includes(nodeUUID))
				nodelink.Nodes.push(nodeUUID)

			// Maintains an index to quickly find orphaned files.
			nodelink.NodeCount = nodelink.Nodes.length

			await this.add({ data: nodelink })
		}
	}// }}}
}

class FileMetadata {
	constructor(data) {// {{{
		this.UUID = data.UUID || uuidv7()
		this.uploaded = data.uploaded || 'only_on_device'

		this.name = data.name
		this.size = data.size
		this.type = data.type || 'application/octet-stream'
		this.modified = data.modified || data.lastModified || Date.now()
	}// }}}
	setUploaded() {// {{{
		this.uploaded = 'on_server'
	}// }}}
	get data() {// {{{
		return {
			UUID: this.UUID,
			uploaded: this.uploaded,
			name: this.name,
			size: this.size,
			type: this.type,
			modified: this.modified,
		}
	}// }}}
}

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
