import { ApplicationState } from "./application_state.mjs"
import { NavigationState, NavigationError, navigation_objectify, navigation_deobjectify } from "./navigation_state.mjs"
import { NavigationStateProtected } from "./navigation_state_protected.mjs"
import { NavigationStateRegistry } from "./navigation_state_registry.mjs"
import { URLParser } from "./navigation_state_url.mjs"
import { RemoteState } from "./remote_state.mjs"
import { StateManagerBuilder } from "./state_manager_builder.mjs"
import { StateManagerProtected } from "./state_manager_protected.mjs"


//TODO: document privacy concerns
//TODO: maybe automatically add mountpoint to hb tags?
//TODO: review what happens with errors & history

class HistoryEntry {

	//base_state is the state encoded by the url path, without the search and the hash
	constructor(navigation_state, shorten_url, url_is_shareable, url_version, base_state, squashable_till) {
		Object.assign(this, { navigation_state, shorten_url, url_is_shareable, url_version, base_state, squashable_till })
		Object.freeze(this)
	}

	marshaled() {
		return { navigation_state: navigation_objectify(this.navigation_state), shorten_url: this.shorten_url, url_is_shareable: this.url_is_shareable, url_version: this.url_version, base_state: navigation_objectify(this.base_state) }
	}

	static unmarshaled(marshaled) {
		return new HistoryEntry(navigation_deobjectify(marshaled.navigation_state), marshaled.shorten_url, marshaled.url_is_shareable, marshaled.url_version, navigation_deobjectify(marshaled.base_state), null)
	}
}


export class StateManager {

	#url_parser

	#next_application_state_change
	#next_application_state_change_resolve

	#navigation_state  //NavigationState or NavigationError or undefined
	#navigation_state_registry
	#navigation_state_sanitizer
	#navigation_state_promises_gc_protector = {}

	#delayed_history_commit_at
	#delayed_history_commit_timer_id
	#current_history_entry //aka. comitted state. This lags behind #navigation_state on delayed commits.
	#current_history_entry_url_resolve_promise_abort_controller
	#current_history_entry_url_resolve_promise_gc_protector

	#cache
	#remote

	static build() {
		return new StateManagerBuilder(StateManager, undefined, undefined, undefined, null, undefined, false)
	}

	constructor(hakuban_exchange, mountpoint, expose, shortener_url, navigation_state_sanitizer, dynamic_pathname) {
		this.#navigation_state_sanitizer = navigation_state_sanitizer

		NavigationState.manager = this
		RemoteState.manager = this

		this.#url_parser = new URLParser(window.location.origin, (mountpoint || URLParser.guess_mountpoint(window.location.pathname)), "b", dynamic_pathname, (expose || []))
		this.#navigation_state_registry = new NavigationStateRegistry((shortener_url === true) ? (window.location.origin + this.mountpoint) : shortener_url, this.#url_parser)
		this.#cache = {}
		this.#remote = new RemoteState(hakuban_exchange)

		document.addEventListener("click", (event) => {
			const A_ancestor = event.composedPath().find(node => node.tagName === 'A')
			const href = A_ancestor?.getAttribute("data-internal-href") || A_ancestor?.getAttribute("href")
			//TODO maybe ignore routes which don't start with mountpoint[/][?|/-/]
			if (!A_ancestor || !href || !href.match(/^[#\?/]/) || event.metaKey || event.shiftKey || event.altKey || event.ctrlKey || A_ancestor.hasAttribute("data-hands-off")) return true
			console.debug('Ramistat: A-element clicked, navigating to:', href)
			this.#navigate_to_href(href, { commit_delay: A_ancestor?.getAttribute("data-commit-delay"), squashable: A_ancestor?.getAttribute("data-squashable-for") }) //TODO: squashable-for-max
			event.preventDefault()
			event.stopPropagation()
		})

		window.onpopstate = (event) => {
			console.debug('Ramistat: History navigation event. Setting location state to:', event.state)
			this.#history_popped(event.state)
			// TODO: should we really do that?
			event.preventDefault()
			event.stopPropagation()
		}

		this.#history_popped(window.history.state)
	}

	#history_replace_url_with_shortened() {
		const state_to_generate_short_url_for = this.#navigation_state
		this.#current_history_entry_url_resolve_promise_abort_controller?.abort()
		this.#current_history_entry_url_resolve_promise_abort_controller = new AbortController()
		this.#current_history_entry_url_resolve_promise_gc_protector = this.#navigation_state_registry.navigation_state_to_shortened_url(state_to_generate_short_url_for, this.#current_history_entry_url_resolve_promise_abort_controller.signal).then((shortened_url) => {
			if (this.#navigation_state === state_to_generate_short_url_for && history.state.shorten_url) {
				this.#current_history_entry = new HistoryEntry(this.#navigation_state, false, shortened_url.is_shareable, shortened_url.url_version, shortened_url.base_state, this.#current_history_entry.squashable_till)
				window.history.replaceState(this.#current_history_entry.marshaled(), "", shortened_url.url)
				this.#current_history_entry_url_resolve_promise_gc_protector = null
				//TODO: do we want to trigger state_change here to regenerate hrefs?
			}
		}).catch((exception) => {
			if (exception.name != 'AbortError') console.error(exception)
			if (this.#navigation_state === state_to_generate_short_url_for && history.state.shorten_url) {
				this.#current_history_entry_url_resolve_promise_gc_protector = null
			}
		})
	}

	//only for tests
	async [StateManagerProtected.shortened]() {
		while (this.#current_history_entry_url_resolve_promise_gc_protector != null) {
			await this.#current_history_entry_url_resolve_promise_gc_protector
		}
	}

	#history_push(squashable_for_ms) {
		if (this.#delayed_history_commit_timer_id != null) clearTimeout(this.#delayed_history_commit_timer_id)
		this.#delayed_history_commit_at = null
		this.#delayed_history_commit_timer_id = null
		const new_url = this.#navigation_state_registry.navigation_state_to_url(this.#navigation_state)
		const squashable_till = squashable_for_ms == null ? null : (Date.now() + squashable_for_ms)
		const previous_squashable_till = this.#current_history_entry.squashable_till
		this.#current_history_entry = new HistoryEntry(this.#navigation_state, !new_url.is_perfect, new_url.is_shareable, new_url.url_version, new_url.base_state, squashable_till)
		if (squashable_till == null || previous_squashable_till == null || previous_squashable_till < Date.now()) {
			window.history.pushState(this.#current_history_entry.marshaled(), "", new_url.url)
		} else {
			window.history.replaceState(this.#current_history_entry.marshaled(), "", new_url.url)
		}
		if (!new_url.is_perfect && (this.#navigation_state instanceof NavigationState) && this.#navigation_state_registry.can_shorten) this.#history_replace_url_with_shortened()
	}

	#history_push_with_delay(history_commit_delay, squashable_for_ms) {
		const commit_at = new Date() * 1.0 + history_commit_delay
		if (this.#delayed_history_commit_at != null && this.#delayed_history_commit_at <= commit_at) return
		if (this.#delayed_history_commit_timer_id != null) clearTimeout(this.#delayed_history_commit_timer_id)
		this.#delayed_history_commit_at = commit_at
		this.#delayed_history_commit_timer_id = setTimeout(() => this.#history_push(squashable_for_ms), history_commit_delay)
	}

	#history_popped(history_entry) {
		if (history_entry != undefined) {
			this.#current_history_entry = HistoryEntry.unmarshaled(history_entry)
			this.#navigation_state = this.#current_history_entry.navigation_state
			this.state_changed()
			if (history_entry.shorten_url && (this.#navigation_state instanceof NavigationState) && this.#navigation_state_registry.can_shorten) this.#history_replace_url_with_shortened()
		} else {
			this.#current_history_entry = undefined
			this.#navigation_state = undefined
			this.state_changed()

			const url_to_resolve_into_state = window.location.href
			if (! (url_to_resolve_into_state in this.#navigation_state_promises_gc_protector)) {
				this.#navigation_state_promises_gc_protector[url_to_resolve_into_state] = this.#navigation_state_registry.url_to_navigation_state(new URL(url_to_resolve_into_state)).then((resolved_state) => {
					delete this.#navigation_state_promises_gc_protector[url_to_resolve_into_state]
					if (window.location.href != url_to_resolve_into_state) return;  // we resolved a state that is not current any more
					const filtered = this.#sanitized_state(resolved_state.navigation_state)
					this.#current_history_entry = new HistoryEntry(filtered, false, (resolved_state.navigation_state instanceof NavigationState), resolved_state.url_version, resolved_state.base_state, null)
					history.replaceState(this.#current_history_entry.marshaled(), "")
					this.#navigation_state = this.#current_history_entry.navigation_state
					this.state_changed()
				})
			}
		}
	}

	#navigate_to_href(href, options) {
		try {
			const parsed_href = this.#url_parser.parse_href(href)
			const base_state = parsed_href.relative ? this.#navigation_state : NavigationState.create()
			if (base_state instanceof NavigationError) {
				throw new NavigationError("NavigateFromErroneousState", "Attempt to do relative navigation from invalid state")
			} else {
				this.#navigate(parsed_href.diff.patch(base_state), options)
			}
		} catch (error) {
			this.#navigate(NavigationError.from(error), options)
		}
	}

	[StateManagerProtected.navigate_to_state](new_navigation_state, options) {
		this.#navigate(new_navigation_state.root_scope(), options)
	}

	#navigate(new_navigation_state, options) {
		this.#navigation_state = this.#sanitized_state(new_navigation_state)
		const squashable_for_ms = options.squashable == null ? null : parseInt((options.squashable === true ? 5000 : options.squashable), 10);
		if ((this.#navigation_state instanceof NavigationError) || options.commit_delay == null) {
			this.#history_push(squashable_for_ms)
		} else {
			this.#history_push_with_delay(options.commit_delay, squashable_for_ms)
		}
		this.state_changed()
	}

	#sanitized_state(input_state) {
		if (input_state instanceof NavigationError) return input_state
		const filtered = input_state.editable()
		if (this.#navigation_state_sanitizer != null) this.#navigation_state_sanitizer(filtered)
		filtered[NavigationStateProtected.trim_empty_branches]()
		filtered.freeze()
		return filtered
	}

	[StateManagerProtected.generate_hrefs](target_navigation_state, options) {
		target_navigation_state = this.#sanitized_state(target_navigation_state)
		const option_attrs = {}
		if (options.commit_delay != null) option_attrs["data-commit-delay"] = options.commit_delay
		if (options.squashable != null) option_attrs["data-squashable-for"] = options.squashable === true ? 5000 : options.squashable
		//TODO: what errors may occur here?
		if (this.#current_history_entry != null && this.#current_history_entry.navigation_state instanceof NavigationState) {
			return { ...option_attrs, ...this.#url_parser.generate_hrefs(this.#navigation_state, this.#current_history_entry.base_state, this.#current_history_entry.url_is_shareable, this.#current_history_entry.url_version, target_navigation_state) }
		} else {
			return { ...option_attrs, ...this.#url_parser.generate_hrefs(null, null, null, null, target_navigation_state) }
		}
	}

	[StateManagerProtected.generate_url](target_navigation_state) {
		target_navigation_state = this.#sanitized_state(target_navigation_state)
		const url = this.#navigation_state_registry.navigation_state_to_url(target_navigation_state)
		return url.is_shareable ? url.url : undefined
	}

	get mountpoint() {
		return this.#url_parser.mountpoint
	}

	state_changed() {
		console.debug('Ramistat: state changed', this.#navigation_state)
		const resolve = this.#next_application_state_change_resolve
		this.#next_application_state_change = new Promise((next_resolve) => this.#next_application_state_change_resolve = next_resolve)
		resolve?.({ promise: this.#next_application_state_change })
	}

	get state() {
		return new ApplicationState(null, [], this.#navigation_state, this.#remote, {}, this.#cache)
	}

	async *states() {
		let next_change = this.#next_application_state_change
		while (true) {
			yield  this.state
			next_change = (await next_change).promise
		}
	}

}
