"""Tauon Music Box - Theme reading module"""

# Copyright © 2015-2022, Taiko2k captain(dot)gxj(at)gmail.com

#     This file is part of Tauon Music Box.
#
#     Tauon Music Box is free software: you can redistribute it and/or modify
#     it under the terms of the GNU General Public License as published by
#     the Free Software Foundation, either version 3 of the License, or
#     (at your option) any later version.
#
#     Tauon Music Box is distributed in the hope that it will be useful,
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#     GNU Lesser General Public License for more details.
#
#     You should have received a copy of the GNU General Public License
#     along with Tauon Music Box.  If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations

import base64
import io
import json
import logging
import os
from typing import TYPE_CHECKING, Protocol

import sdl3
from PIL import Image

from tauon.t_modules.t_extra import ColourRGBA, rgb_add_hls, test_lumi

if TYPE_CHECKING:
	from pathlib import Path

	from tauon.t_modules.t_draw import TDraw
	from tauon.t_modules.t_main import ColoursClass, Directories, PlayerCtl, Tauon
	from tauon.t_modules.t_prefs import Prefs


class GetThemesFn(Protocol):
	def __call__(self, *, dirs: Directories, deco: bool) -> list[str] | dict[str, str]: ...


THEME_SERIALISABLE_COLOURS: tuple[tuple[str, str], ...] = (
	("window frame", "window_frame"),
	("gallery highlight", "gallery_highlight"),
	("index playing", "index_playing"),
	("time playing", "time_text"),
	("artist playing", "artist_playing"),
	("track album", "album_text"),
	("album playing", "album_playing"),
	("top panel", "top_panel_background"),
	("corner button off", "corner_button"),
	("corner button on", "corner_button_active"),
	("menu button normal", "status_text_normal"),
	("menu button hover", "status_text_over"),
	("queue panel", "queue_background"),
	("side panel", "side_panel_background"),
	("lyrics panel", "lyrics_panel_background"),
	("gallery background", "gallery_background"),
	("tracklist panel", "playlist_panel_background"),
	("track line", "title_text"),
	("track missing", "playlist_text_missing"),
	("playing highlight", "row_playing_highlight"),
	("track time", "bar_time"),
	("fav line", "star_line"),
	("folder title", "folder_title"),
	("folder line", "folder_line"),
	("buttons off", "media_buttons_off"),
	("buttons over", "media_buttons_over"),
	("buttons active", "media_buttons_active"),
	("playing time", "time_playing"),
	("track index", "index_text"),
	("track playing", "title_playing"),
	("select highlight", "row_select_highlight"),
	("track artist", "artist_text"),
	("tab active text", "tab_text_active"),
	("tab text", "tab_text"),
	("tab background", "tab_background"),
	("tab over", "tab_highlight"),
	("tab active background", "tab_background_active"),
	("title info", "side_bar_line1"),
	("extra info", "side_bar_line2"),
	("bottom title", "bar_title_text"),
	("scroll bar", "scroll_colour"),
	("seek bar", "seek_bar_fill"),
	("seek bg", "seek_bar_background"),
	("volume bar", "volume_bar_fill"),
	("volume bg", "volume_bar_background"),
	("mode off", "mode_button_off"),
	("mode over", "mode_button_over"),
	("mode on", "mode_button_active"),
	("art border", "art_box"),
	("tb line", "tb_line"),
	("music vis", "vis_colour"),
	("menu background", "menu_background"),
	("menu text", "menu_text"),
	("menu disable", "menu_text_disabled"),
	("menu icons", "menu_icons"),
	("menu highlight", "menu_highlight_background"),
	("menu border", "menu_tab"),
	("lyrics text", "lyrics"),
	("active lyric", "active_lyric"),
	("bottom panel", "bottom_panel_colour"),
	("mini bg", "mini_mode_background"),
	("mini border", "mini_mode_border"),
	("mini text 1", "mini_mode_text_1"),
	("mini text 2", "mini_mode_text_2"),
	("playlist background", "playlist_box_background"),
	("box background", "box_background"),
	("box border", "box_border"),
	("box text border", "box_text_border"),
	("box text label", "box_text_label"),
	("box title text", "box_title_text"),
	("box text normal", "box_text"),
	("box sub text", "box_sub_text"),
	("box input text", "box_input_text"),
	("box button text highlight", "box_button_text_highlight"),
	("box button text normal", "box_button_text"),
	("box button background normal", "box_button_background"),
	("box button background highlight", "box_button_background_highlight"),
	("box button border", "box_check_border"),
	("window buttons background", "window_buttons_bg"),
	("window buttons on", "window_buttons_bg_over"),
	("window buttons icon off", "window_button_icon_off"),
	("window buttons icon over", "window_buttons_icon_over"),
	("window button x on", "window_button_x_on"),
	("window button x off", "window_button_x_off"),
	("column bar background", "column_bar_background"),
	("artist bio background", "artist_bio_background"),
	("artist bio text", "artist_bio_text"),
	("visualizer bg", "vis_bg"),
	("link text", "link_text"),
	("star line playing", "star_line_playing"),
	("message box bg", "message_box_bg"),
	("message box text", "message_box_text"),
	("queue drag indicator", "queue_drag_indicator_colour"),
	("pulse colour", "pulse_colour"),
	("queue card background", "queue_card_background"),
	("column grip", "column_grip"),
	("column bar text", "column_bar_text"),
	("box thumb background", "box_thumb_background"),
)


def get_colour_from_line(cline: str) -> ColourRGBA:
	colour: list[int] = [-1, -1, -1, -1]
	colour_str: list[str] = ["", "", "", ""]
	mode = 0

	is_hex = False
	if "," not in cline[:8]:
		if cline[:6].isalnum():
			is_hex = True
		if cline[0] == "#" and cline[1:7].isalnum() and cline[7].isspace() and cline[8].isspace():
			is_hex = True
		if cline[0] == "#" and cline[1:9].isalnum() and cline[9].isspace() and cline[10].isspace():
			is_hex = True

	if is_hex:
		# hex mode
		cline = cline.removeprefix("0x")
		cline = cline.removeprefix("#")
		ll = cline
		a = 255
		try:
			r = int(ll[0] + ll[1], 16)
			g = int(ll[2] + ll[3], 16)
			b = int(ll[4] + ll[5], 16)
			if ll[6].isalnum() and ll[7].isalnum():
				a = int(ll[6] + ll[7], 16)
		except ValueError:
			logging.error(f"Error parsing theme line {cline}, we will ignore it")
			return ColourRGBA(255, 255, 255, 255)
		except Exception:
			logging.exception(f"Unknown error parsing theme line {cline}")
			return ColourRGBA(255, 255, 255, 255)
		return ColourRGBA(r, g, b, a)
	# rgb mode
	for i in cline:
		if i.isdigit():
			colour_str[mode] += i
		elif i == "," and mode < 3:
			mode += 1
		# Stop parsing after RGB/RGBA values so we don't absorb digits in labels such as "mini text 2"
		elif (i.isspace() and mode >= 2 and colour_str[mode]) or (mode >= 2 and colour_str[mode]):
			break

	# Convert str list to int list
	for b in range(len(colour_str)):
		if colour_str[b] == "":
			colour[b] = 255
		else:
			colour[b] = int(colour_str[b])

	if (
		(colour[0] < 0 or colour[0] > 255) or
		(colour[1] < 0 or colour[1] > 255) or
		(colour[2] < 0 or colour[2] > 255) or
		(colour[3] < 0 or colour[3] > 255)
	):
		logging.error(f"Got nonsense colour value parsed from {cline}, we will ignore it")
		return ColourRGBA(255, 255, 255, 255)

	return ColourRGBA(colour[0], colour[1], colour[2], colour[3])


def save_theme(colours: ColoursClass, path: Path) -> None:
	lines = ["# Tauon user theme\n"]

	if colours.deco:
		lines.append(f"deco={colours.deco}\n")
	if colours.lm:
		lines.append("light-mode\n")

	for key, attr in THEME_SERIALISABLE_COLOURS:
		colour = getattr(colours, attr, None)
		if colour is None:
			continue
		lines.append(f"{colour.r},{colour.g},{colour.b},{colour.a}    {key}\n")

	for key, value in sorted(colours.column_colours.items()):
		lines.append(f"{value.r},{value.g},{value.b},{value.a}    column-{key.lower().replace(' ', '-')}\n")
	for key, value in sorted(colours.column_colours_playing.items()):
		lines.append(f"{value.r},{value.g},{value.b},{value.a}    column+{key.lower().replace(' ', '-')}\n")

	path.parent.mkdir(parents=True, exist_ok=True)
	path.write_text("".join(lines), encoding="utf-8")


def load_theme(colours: ColoursClass, path: Path) -> None:
	with path.open(encoding="utf-8") as f:
		content = f.readlines()

		for p in content:
			p = p.strip()
			if p.startswith("#"):
				continue
			if "# " in p:
				p = p.split("# ")[0]
			if not p:
				continue
			if p[0] == "#" and ("-" in p[:7] or " " in p[:7] or "\t" in p[:7]):
				continue
			if p.startswith("deco="):
				colours.deco = p.split("=", 1)[1].strip()
			elif "light-mode" in p:
				colours.light_mode()
			elif "window frame" in p:
				colours.window_frame = get_colour_from_line(p)
			elif "gallery highlight" in p:
				colours.gallery_highlight = get_colour_from_line(p)
			elif "index playing" in p:
				colours.index_playing = get_colour_from_line(p)
			elif "time playing" in p:
				colours.time_text = get_colour_from_line(p)
			elif "artist playing" in p:
				colours.artist_playing = get_colour_from_line(p)
			elif "album line" in p or "track album" in p:  # Bad name
				colours.album_text = get_colour_from_line(p)
			elif "album playing" in p:
				colours.album_playing = get_colour_from_line(p)
			elif "top panel" in p or "player background" in p:
				colours.top_panel_background = get_colour_from_line(p)

				if test_lumi(colours.bottom_panel_colour) < 0.2:
					colours.corner_icon = ColourRGBA(0, 0, 0, 60)
				elif test_lumi(colours.bottom_panel_colour) < 0.8:
					colours.corner_icon = ColourRGBA(40, 40, 40, 255)
				else:
					colours.corner_icon = ColourRGBA(255, 255, 255, 30)

				if not colours.lm:
					colours.corner_button = rgb_add_hls(colours.top_panel_background, 0, 0.18, 0)
			elif "status text" in p:
				status_text_temp = get_colour_from_line(p)
				colours.status_text_over = status_text_temp
				colours.status_text_normal = rgb_add_hls(status_text_temp, 0, 0.30, -0.15)

			elif "corner button off" in p:
				colours.corner_button = get_colour_from_line(p)
			elif "corner button on" in p:
				colours.corner_button_active = get_colour_from_line(p)
			elif "menu button normal" in p:
				colours.status_text_normal = get_colour_from_line(p)
				status_text_color_defined = 1
			elif "menu button hover" in p:
				colours.status_text_over = get_colour_from_line(p)
				status_text_color_defined = 1
			elif "queue panel" in p:
				colours.queue_background = get_colour_from_line(p)
			elif "side panel" in p:
				colours.side_panel_background = get_colour_from_line(p)
				colours.playlist_box_background = colours.side_panel_background
			elif "lyrics panel" in p:
				colours.lyrics_panel_background = get_colour_from_line(p)
				lyrics_panel_color_defined = 1
			elif "gallery background" in p:
				colours.gallery_background = get_colour_from_line(p)
			elif "playlist panel" in p or "tracklist panel" in p:  # bad name
				colours.playlist_panel_background = get_colour_from_line(p)
			elif "track line" in p:
				colours.title_text = get_colour_from_line(p)
			elif "track missing" in p:
				colours.playlist_text_missing = get_colour_from_line(p)
			elif "playing highlight" in p:
				colours.row_playing_highlight = get_colour_from_line(p)
			elif "track time" in p:
				colours.bar_time = get_colour_from_line(p)
			elif "fav line" in p:
				colours.star_line = get_colour_from_line(p)
			elif "folder title" in p:
				colours.folder_title = get_colour_from_line(p)
			elif "folder line" in p:
				colours.folder_line = get_colour_from_line(p)
			elif "buttons off" in p:
				colours.media_buttons_off = get_colour_from_line(p)
			elif "buttons over" in p:
				colours.media_buttons_over = get_colour_from_line(p)
			elif "buttons active" in p:
				colours.media_buttons_active = get_colour_from_line(p)
			elif "playing time" in p:
				colours.time_playing = get_colour_from_line(p)
			elif "track index" in p:
				colours.index_text = get_colour_from_line(p)
			elif "track playing" in p:
				colours.title_playing = get_colour_from_line(p)
			elif "select highlight" in p:
				colours.row_select_highlight = get_colour_from_line(p)
			elif "track artist" in p:
				colours.artist_text = get_colour_from_line(p)
			elif "tab active line" in p or "tab active text" in p:  # bad name
				colours.tab_text_active = get_colour_from_line(p)
			elif "tab line" in p or "tab text" in p:  # bad name
				colours.tab_text = get_colour_from_line(p)
			elif "tab background" in p:
				colours.tab_background = get_colour_from_line(p)
			elif "tab over" in p:
				colours.tab_highlight = get_colour_from_line(p)
			elif "tab active background" in p:
				colours.tab_background_active = get_colour_from_line(p)
			elif "title info" in p:
				colours.side_bar_line1 = get_colour_from_line(p)
			elif "extra info" in p:
				colours.side_bar_line2 = get_colour_from_line(p)
			elif "bottom title" in p:
				colours.bar_title_text = get_colour_from_line(p)
			elif "scroll bar" in p:
				colours.scroll_colour = get_colour_from_line(p)
			elif "seek bar" in p:
				colours.seek_bar_fill = get_colour_from_line(p)
			elif "seek bg" in p:
				colours.seek_bar_background = get_colour_from_line(p)
			elif "volume bar" in p:
				colours.volume_bar_fill = get_colour_from_line(p)
			elif "volume bg" in p:
				colours.volume_bar_background = get_colour_from_line(p)
			elif "mode off" in p:
				colours.mode_button_off = get_colour_from_line(p)
			elif "mode over" in p:
				colours.mode_button_over = get_colour_from_line(p)
			elif "mode on" in p:
				colours.mode_button_active = get_colour_from_line(p)
			elif "art border" in p:
				colours.art_box = get_colour_from_line(p)
			elif "tb line" in p:
				colours.tb_line = get_colour_from_line(p)
			elif "music vis" in p:
				colours.vis_colour = get_colour_from_line(p)
			elif "menu background" in p or "menu bg" in p:
				colours.menu_background = get_colour_from_line(p)
			elif "menu text" in p:
				colours.menu_text = get_colour_from_line(p)
			elif "menu disable" in p:
				colours.menu_text_disabled = get_colour_from_line(p)
			elif "menu icons" in p:
				colours.menu_icons = get_colour_from_line(p)
			elif "menu highlight" in p:
				colours.menu_highlight_background = get_colour_from_line(p)
			elif "menu border" in p:
				colours.menu_tab = get_colour_from_line(p)
			elif "lyrics showcase" in p or "lyrics text" in p:
				colours.lyrics = get_colour_from_line(p)
			elif "active lyric" in p:
				colours.active_lyric = get_colour_from_line(p)
			elif "bottom panel" in p:
				colours.bottom_panel_colour = get_colour_from_line(p)
				# colours.menu_background = colours.bottom_panel_colour
			elif "mini bg" in p:
				colours.mini_mode_background = get_colour_from_line(p)
			elif "mini border" in p:
				colours.mini_mode_border = get_colour_from_line(p)
			elif "mini text 1" in p:
				colours.mini_mode_text_1 = get_colour_from_line(p)
			elif "mini text 2" in p:
				colours.mini_mode_text_2 = get_colour_from_line(p)
			elif "column-" in p:
				key = p[p.find("column-") + 7 :].replace("-", " ").lower().title().rstrip()
				value = get_colour_from_line(p)
				colours.column_colours[key] = value
			elif "column+" in p:
				key = p[p.find("column+") + 7 :].replace("-", " ").lower().title().rstrip()
				value = get_colour_from_line(p)
				colours.column_colours_playing[key] = value
			elif "playlist box bg" in p or "playlist background" in p:  # bad name
				colours.playlist_box_background = get_colour_from_line(p)

			elif "box background" in p:
				colours.box_background = get_colour_from_line(p)
			elif "box border" in p:
				colours.box_border = get_colour_from_line(p)
			elif "box text border" in p:
				colours.box_text_border = get_colour_from_line(p)
			elif "box text label" in p:
				colours.box_text_label = get_colour_from_line(p)

			elif "box title text" in p:
				colours.box_title_text = get_colour_from_line(p)
			elif "box text normal" in p:
				colours.box_text = get_colour_from_line(p)
			elif "box sub text" in p:
				colours.box_sub_text = get_colour_from_line(p)
			elif "box input text" in p:
				colours.box_input_text = get_colour_from_line(p)

			elif "box button text highlight" in p:
				colours.box_button_text_highlight = get_colour_from_line(p)
			elif "box button text normal" in p:
				colours.box_button_text = get_colour_from_line(p)
			elif "box button background normal" in p:
				colours.box_button_background = get_colour_from_line(p)
			elif "box button background highlight" in p:
				colours.box_button_background_highlight = get_colour_from_line(p)
			elif "box button border" in p:
				colours.box_check_border = get_colour_from_line(p)

			elif "window buttons background" in p:
				colours.window_buttons_bg = get_colour_from_line(p)
			elif "window buttons on" in p:
				colours.window_buttons_bg_over = get_colour_from_line(p)
			elif "window buttons icon off" in p:
				colours.window_button_icon_off = get_colour_from_line(p)
				colours.window_button_x_off = colours.window_button_icon_off
			elif "window buttons icon over" in p:
				colours.window_buttons_icon_over = get_colour_from_line(p)
				colours.window_button_x_on = colours.window_buttons_icon_over
			elif "window button x on" in p:
				colours.window_button_x_on = get_colour_from_line(p)
			elif "window button x off" in p:
				colours.window_button_x_off = get_colour_from_line(p)
			elif "column bar background" in p:
				colours.column_bar_background = get_colour_from_line(p)

			elif "artist bio background" in p:
				colours.artist_bio_background = get_colour_from_line(p)
			elif "artist bio text" in p:
				colours.artist_bio_text = get_colour_from_line(p)

			elif "visualizer bg" in p:
				colours.vis_bg = get_colour_from_line(p)
			elif "link text" in p:
				colours.link_text = get_colour_from_line(p)
			elif "star line playing" in p:
				colours.star_line_playing = get_colour_from_line(p)
			elif "message box bg" in p:
				colours.message_box_bg = get_colour_from_line(p)
			elif "message box text" in p:
				colours.message_box_text = get_colour_from_line(p)
			elif "queue drag indicator" in p:
				colours.queue_drag_indicator_colour = get_colour_from_line(p)
			elif "pulse colour" in p:
				colours.pulse_colour = get_colour_from_line(p)
			elif "queue card background" in p:
				colours.queue_card_background = get_colour_from_line(p)
			elif "column grip" in p:
				colours.column_grip = get_colour_from_line(p)
			elif "column bar text" in p:
				colours.column_bar_text = get_colour_from_line(p)
			elif "box thumb background" in p:
				colours.box_thumb_background = get_colour_from_line(p)
			# elif "panel button off" in p:
			# 	colours.corner_button = get_colour_from_line(p)
			# elif "panel button on" in p:
			# 	colours.corner_button_active = get_colour_from_line(p)
	colours.post_config()
	if colours.lm:
		colours.light_mode()


class Drawable:
	def __init__(self) -> None:
		self.location = 1
		self.x = 0
		self.y = 0
		self.w = 100
		self.h = 100
		self.rect = None
		self.texture = None


class Deco:
	def __init__(self, tauon: Tauon) -> None:
		self.tauon: Tauon = tauon
		self.renderer = None
		self.pctl: PlayerCtl = tauon.pctl
		self.prefs: Prefs = tauon.prefs
		self.drawables: list[Drawable] = []
		self.get_themes: GetThemesFn

	def unload(self) -> None:
		for item in self.drawables:
			sdl3.SDL_DestroyTexture(item.texture)
		self.drawables.clear()

	def load(self, name: str) -> None:
		self.unload()
		if not name:
			return

		decos = self.get_themes(deco=True, dirs=self.tauon.dirs)

		if name not in decos:
			logging.error("Missing deco file")
			return

		target = decos[name]
		with open(target) as f:
			j = json.load(f)

		if "images" not in j:
			return
		images = j["images"]
		if not images:
			return
		item = images[0]
		if item.get("location") != 1:
			return
		x = int(item.get("x-margin", 0))
		y = int(item.get("y-margin", 0))
		opacity = int(item.get("opacity", 100))
		logical_h = int(item.get("logical-height", 400))
		file = item.get("file")
		if file:
			path = os.path.join(os.path.dirname(target), file)
			im = Image.open(path)
		else:
			s = item.get("image-data")
			if not s:
				return
			b = io.BytesIO(base64.b64decode(s))
			im = Image.open(b)

		w, h = im.size
		scale = self.tauon.gui.scale
		if not abs(h - (logical_h * scale)) < 10:
			new_h = round(logical_h * scale)
			ratio = w / h
			new_w = round(new_h * ratio)
			im = im.resize((new_w, new_h), Image.Resampling.LANCZOS)
			w, h = new_w, new_h

		g = io.BytesIO()
		g.seek(0)
		im.save(g, "PNG")
		g.seek(0)
		s_image = self.tauon.ddt.load_image(g)
		texture = sdl3.SDL_CreateTextureFromSurface(self.renderer, s_image)
		sdl3.SDL_SetTextureAlphaMod(texture, opacity)
		sdl3.SDL_DestroySurface(s_image)
		sdl_rect = sdl3.SDL_FRect(0, 0, w, h)

		drawable = Drawable()
		drawable.x = x
		drawable.y = y
		drawable.w = w
		drawable.h = h
		drawable.rect = sdl_rect
		drawable.texture = texture
		self.drawables.append(drawable)

	def draw(self, ddt: TDraw, x: int, y: int, pretty_text: bool = False) -> None:
		if self.drawables:
			d = self.drawables[0]
			d.rect.x = round(x - int(d.w + round(d.x * self.tauon.gui.scale)))
			d.rect.y = round(y - int(d.h + round(d.y * self.tauon.gui.scale)))
			sdl3.SDL_RenderTexture(self.renderer, d.texture, None, d.rect)

			if pretty_text:
				ddt.pretty_rect = (d.rect.x, d.rect.y, d.rect.w, d.rect.h)
				ddt.alpha_bg = True
