/* eslint-disable react-refresh/only-export-components */
import * as React from "react"

type Theme = "dark" | "light" | "system"
type ResolvedTheme = "dark" | "light"

type ThemeProviderProps = {
	children: React.ReactNode
	defaultTheme?: Theme
	storageKey?: string
	disableTransitionOnChange?: boolean
}

type ThemeProviderState = {
	theme: Theme
	setTheme: (theme: Theme) => void
}

const COLOR_SCHEME_QUERY = "(prefers-color-scheme: dark)"
const THEME_VALUES: Theme[] = ["dark", "light", "system"]

const ThemeProviderContext = React.createContext<ThemeProviderState | undefined>(undefined)

function isTheme(value: string | null): value is Theme {
	if (value === null) {
		return false
	}

	return THEME_VALUES.includes(value as Theme)
}

function getSystemTheme(): ResolvedTheme {
	if (window.matchMedia(COLOR_SCHEME_QUERY).matches) {
		return "dark"
	}

	return "light"
}

function disableTransitionsTemporarily() {
	const style = document.createElement("style")
	style.appendChild(
		document.createTextNode(
			"*,*::before,*::after{-webkit-transition:none!important;transition:none!important}",
		),
	)
	document.head.appendChild(style)

	return () => {
		window.getComputedStyle(document.body)
		requestAnimationFrame(() => {
			requestAnimationFrame(() => {
				style.remove()
			})
		})
	}
}

function isEditableTarget(target: EventTarget | null) {
	if (!(target instanceof HTMLElement)) {
		return false
	}

	if (target.isContentEditable) {
		return true
	}

	const editableParent = target.closest("input, textarea, select, [contenteditable='true']")
	if (editableParent) {
		return true
	}

	return false
}

export function ThemeProvider({
	children,
	defaultTheme = "system",
	storageKey = "theme",
	disableTransitionOnChange = true,
	...props
}: ThemeProviderProps) {
	const [theme, setThemeState] = React.useState<Theme>(() => {
		const storedTheme = localStorage.getItem(storageKey)
		if (isTheme(storedTheme)) {
			return storedTheme
		}

		return defaultTheme
	})

	const setTheme = React.useCallback(
		(nextTheme: Theme) => {
			localStorage.setItem(storageKey, nextTheme)
			setThemeState(nextTheme)
		},
		[storageKey],
	)

	const applyTheme = React.useCallback(
		(nextTheme: Theme) => {
			const root = document.documentElement
			const resolvedTheme = nextTheme === "system" ? getSystemTheme() : nextTheme
			const restoreTransitions = disableTransitionOnChange ? disableTransitionsTemporarily() : null

			root.classList.remove("light", "dark")
			root.classList.add(resolvedTheme)

			if (restoreTransitions) {
				restoreTransitions()
			}
		},
		[disableTransitionOnChange],
	)

	React.useEffect(() => {
		applyTheme(theme)

		if (theme !== "system") {
			return undefined
		}

		const mediaQuery = window.matchMedia(COLOR_SCHEME_QUERY)
		const handleChange = () => {
			applyTheme("system")
		}

		mediaQuery.addEventListener("change", handleChange)

		return () => {
			mediaQuery.removeEventListener("change", handleChange)
		}
	}, [theme, applyTheme])

	React.useEffect(() => {
		const handleKeyDown = (event: KeyboardEvent) => {
			if (event.repeat) {
				return
			}

			if (event.metaKey || event.ctrlKey || event.altKey) {
				return
			}

			if (isEditableTarget(event.target)) {
				return
			}

			if (event.key.toLowerCase() !== "d") {
				return
			}

			setThemeState((currentTheme) => {
				const nextTheme =
					currentTheme === "dark"
						? "light"
						: currentTheme === "light"
							? "dark"
							: getSystemTheme() === "dark"
								? "light"
								: "dark"

				localStorage.setItem(storageKey, nextTheme)
				return nextTheme
			})
		}

		window.addEventListener("keydown", handleKeyDown)

		return () => {
			window.removeEventListener("keydown", handleKeyDown)
		}
	}, [storageKey])

	React.useEffect(() => {
		const handleStorageChange = (event: StorageEvent) => {
			if (event.storageArea !== localStorage) {
				return
			}

			if (event.key !== storageKey) {
				return
			}

			if (isTheme(event.newValue)) {
				setThemeState(event.newValue)
				return
			}

			setThemeState(defaultTheme)
		}

		window.addEventListener("storage", handleStorageChange)

		return () => {
			window.removeEventListener("storage", handleStorageChange)
		}
	}, [defaultTheme, storageKey])

	const value = React.useMemo(
		() => ({
			theme,
			setTheme,
		}),
		[theme, setTheme],
	)

	return (
		<ThemeProviderContext.Provider {...props} value={value}>
			{children}
		</ThemeProviderContext.Provider>
	)
}

export const useTheme = () => {
	const context = React.useContext(ThemeProviderContext)

	if (context === undefined) {
		throw new Error("useTheme must be used within a ThemeProvider")
	}

	return context
}
