export class Diff {
	// Reuse container for diffs to avoid memory allocation
	static container: HTMLElement

	// innerHTML will diff the element with the given HTML string and apply DOM mutations.
	static innerHTML(aRoot: HTMLElement, html: string) {
		if(!Diff.container) {
			Diff.container = document.createElement("main")
		}
		
		Diff.container.innerHTML = html
		Diff.childNodes(aRoot, Diff.container)
	}

	// childNodes diffs the child nodes of 2 given elements and applies DOM mutations.
	static childNodes(aRoot: Node, bRoot: Node) {
		let aChild = [...aRoot.childNodes]
		let bChild = [...bRoot.childNodes]
		let numNodes = Math.max(aChild.length, bChild.length)
		
		for(let i = 0; i < numNodes; i++) {
			let a = aChild[i]

			if(i >= bChild.length) {
				aRoot.removeChild(a)
				continue
			}

			let b = bChild[i]

			if(i >= aChild.length) {
				aRoot.appendChild(b)
				continue
			}

			if(a.nodeName !== b.nodeName || a.nodeType !== b.nodeType) {
				aRoot.replaceChild(b, a)
				continue
			}

			if(a.nodeType === Node.TEXT_NODE) {
				a.textContent = b.textContent
				continue
			}

			if(a.nodeType === Node.ELEMENT_NODE) {
				let elemA = a as HTMLElement
				let elemB = b as HTMLElement

				// Skip iframes
				if(elemA.tagName === "IFRAME") {
					continue
				}

				// Ignore lazy images if they have the same source
				if(elemA.classList.contains("lazy") && elemA.dataset.src === elemB.dataset.src) {
					continue
				}

				let removeAttributes: Attr[] = []
				
				for(let x = 0; x < elemA.attributes.length; x++) {
					let attrib = elemA.attributes[x]

					if(attrib.specified) {
						if(!elemB.hasAttribute(attrib.name)) {
							removeAttributes.push(attrib)
						}
					}
				}

				for(let attr of removeAttributes) {
					elemA.removeAttributeNode(attr)
				}

				for(let x = 0; x < elemB.attributes.length; x++) {
					let attrib = elemB.attributes[x]

					if(attrib.specified) {
						// Skip mountables
						if(attrib.name == "class" && elemA.classList.contains("mounted")) {
							continue
						}

						elemA.setAttribute(attrib.name, elemB.getAttribute(attrib.name))
					}
				}

				// Special case: Apply state of input elements
				if(elemA !== document.activeElement && elemA instanceof HTMLInputElement && elemB instanceof HTMLInputElement) {
					elemA.value = elemB.value
				}
			}

			Diff.childNodes(a, b)
		}
	}
}