"""Script to update the wallpaper"""
from abc import ABC, abstractmethod
from collections.abc import Iterator
from json import dumps, loads
from math import ceil, floor, isnan, nan
from os import environ
from pathlib import Path
from random import sample
from re import Pattern, compile, match
from subprocess import run
from typing import cast, override, Self

from .evening import on_steam, sunup_amount


# Python sets aren't insertion-ordered, but dict keys are
type OrderedSet[T] = dict[T, None]


class Background(ABC):
	_colour: Pattern[str] = compile(r"^[a-fA-F0-9]{6}$")

	@abstractmethod
	def blue_light(self) -> float: ...

	@abstractmethod
	@override
	def __eq__(self, value: object, /) -> bool: ...

	@abstractmethod
	@override
	def __hash__(self) -> int: ...

	@abstractmethod
	@override
	def __str__(self) -> str: ...

	@classmethod
	def parse(cls, arg: str) -> "Background":
		if match(cls._colour, arg):
			return Colour(int(arg, 16))
		else:
			return Wallpaper(Path(arg))


class Wallpaper(Background):
	_cache: Path = Path(environ["XDG_CACHE_HOME"]) / "wallpaper_blue_light.json"
	_wallpapers: Path = Path("~/Pictures/wallpapers/").expanduser()
	_ignore_dirs: tuple[str, ...] = ("ignore", ".stfolder")

	def __init__(self, image: Path) -> None:
		self._path: Path = image
		self._blue_light: float = nan

	def _calculate_blue_light(self) -> float:
		from PIL import Image
		data = cast(list[int], Image.open(self._path).getdata(2))
		return sum(data) / len(data)

	@classmethod
	def _read_cache(cls) -> dict[str, float]:
		if cls._cache.exists():
			if (text := cls._cache.read_text().strip()):
				if isinstance((obj := loads(text)), dict):
					return cast(dict[str, float], obj)
		return {}

	@classmethod
	def _write_cache(cls, cache: dict[str, float]) -> None:
		stringified = dumps(cache)
		if len(stringified) != cls._cache.write_text(stringified):
			raise Exception("Did not fully write blue light cache to file")

	@classmethod
	def _files(cls) -> Iterator[Path]:
		for (root, _, files) in cls._wallpapers.walk():
			if root.name not in cls._ignore_dirs:
				for file in files:
					yield root / file

	@classmethod
	def available(cls) -> OrderedSet[Background]:
		cache = cls._read_cache()
		modified = False
		wallpapers = {str(path): Wallpaper(path) for path in cls._files()}
		for wallpaper in wallpapers.values():
			cached = cache.get(str(wallpaper._path))
			if cached is None or isnan(cached):
				print("New file:", wallpaper._path)
				try:
					cached = wallpaper._calculate_blue_light()
				except Exception as e:
					print(e)
					continue
				modified = True
				cache[str(wallpaper._path)] = cached
			wallpaper._blue_light = cached
		if modified:
			cache = {path: score for (path, score) in sorted(cache.items(), key=lambda pair: pair[1])}
			cls._write_cache(cache)
		return {wallpapers[path]: None for path in cache.keys()}

	@override
	def blue_light(self) -> float:
		return self._blue_light

	@override
	def __eq__(self, value: object, /) -> bool:
		if isinstance(value, Wallpaper):
			return self._path == value._path
		return False

	@override
	def __hash__(self) -> int:
		return hash(self._path)

	@override
	def __str__(self) -> str:
		return str(self._path)

	@override
	def __repr__(self) -> str:
		return f"Wallpaper({repr(self._path)})"


class Colour(Background):
	def __init__(self, value: int) -> None:
		self._value: int = value

	@override
	def blue_light(self) -> float:
		return self._value & 0xFF

	@override
	def __eq__(self, value: object, /) -> bool:
		if isinstance(value, Colour):
			return self._value == value._value
		return False

	@override
	def __hash__(self) -> int:
		return hash(self._value)

	@override
	def __str__(self) -> str:
		return hex(self._value)

	@override
	def __repr__(self) -> str:
		return f"Colour({hex(self._value)})"


def _convert_bytes(output: bytes) -> str:
	return output.decode("utf-8").strip()


def _swww_cmd(*args: str) -> str:
	output = run(["swww", *args], capture_output=True)
	if output.stderr:
		raise Exception(f"swww returned: '{_convert_bytes(output.stderr)}'")
	return _convert_bytes(output.stdout)


class Monitor:
	_query: Pattern[str] = compile(r"^(?:: )?([^:]+): (\d+)x(\d+), scale: ([\d.]+), currently displaying: (?:image|color): (.+)$")
	_transition_args: list[str] = [
		"--transition-type=wipe",
		"--transition-angle=30",
		"--transition-step=45",
		"--transition-fps=120",
	]

	def __init__(self, line: str) -> None:
		if not (values := match(self._query, line)):
			raise Exception(f"Failed to extract information from: '{line}'")
		self._name: str = values[1]
		self._resolution: tuple[int, int] = (int(values[2]), int(values[3]))
		self._scale: float = float(values[4])
		self._background: Background = Background.parse(values[5])

	def is_colour(self) -> bool:
		return isinstance(self._background, Colour)

	def set_wallpaper(self, background: Background) -> None:
		self._background = background
		_ = _swww_cmd("img", *self._transition_args, f"--outputs={self._name}", str(self._background))

	def get_background(self) -> Background:
		return self._background

	@classmethod
	def current(cls) -> list[Self]:
		return [Monitor(line) for line in _swww_cmd("query").splitlines()]

	@override
	def __eq__(self, value: object, /) -> bool:
		if isinstance(value, Monitor):
			return self._background == value._background
		return False

	@override
	def __repr__(self) -> str:
		return f"Monitor('{self._name}: {self._resolution[0]}x{self._resolution[1]}, scale: {self._scale}, currently displaying: {"color" if self.is_colour() else "image"}: {self._background}')"

	@override
	def __str__(self) -> str:
		return f"{self._name}: {self._background}"


def select_wallpapers(available: list[Background]) -> list[Background]:
	radius = len(available) / 3
	centre = sunup_amount() * len(available)
	return available[max(0, floor(centre - radius)): min(len(available) + 1, ceil(centre + radius))]


def main(check_steam: bool = False) -> None:
	if check_steam and on_steam():
		return

	monitors = Monitor.current()
	wallpapers = Wallpaper.available()

	for monitor in monitors:
		wallpapers.pop(monitor.get_background(), None)

	for monitor, wallpaper in zip(monitors, sample(select_wallpapers(list(wallpapers.keys())), len(monitors))):
		monitor.set_wallpaper(wallpaper)
