#!/usr/bin/env python3

import json
import re
import sys
from pathlib import Path
from urllib.error import HTTPError
from urllib.request import urlopen
from xml.etree import ElementTree as ET

DEVICON_VERSION = "v2.17.0"

ICONS = {
	"c": "c",
	"cpp": "cplusplus",
	"csharp": "csharp",
	"python": "python",
	"rust": "rust",
	"javascript": "javascript",
	"typescript": "typescript",
	"bash": "bash",
	"zsh": "zsh",
	"powershell": "powershell",
	"cmake": "cmake",
	"lua": "lua",
	"java": "java",
	"kotlin": "kotlin",
	"swift": "swift",
	"go": "go",
	"dart": "dart",
	"php": "php",
	"ruby": "ruby",
	"scala": "scala",
	"haskell": "haskell",
	"elixir": "elixir",
	"erlang": "erlang",
	"clojure": "clojure",
	"r": "r",
	"perl": "perl",
	"zig": "zig",
	"nim": "nim",
	"ocaml": "ocaml",
	"fsharp": "fsharp",
	"visualbasic": "visualbasic",
	"fortran": "fortran",
	"crystal": "crystal",
	"gleam": "gleam",
	"julia": "julia",
	"objectivec": "objectivec",
	"vala": "vala",
	"groovy": "groovy",
	"racket": "racket",
	"haxe": "haxe",
	"purescript": "purescript",
	"delphi": "delphi",
	"coffeescript": "coffeescript",
	"elm": "elm",
	"awk": "awk",
	"matlab": "matlab",
	"solidity": "solidity",
	"wasm": "wasm",
	"vim": "vim",
	"sql": "sqlite",
	"json": "json",
	"yaml": "yaml",
	"xml": "xml",
	"html": "html5",
	"css": "css3",
	"sass": "sass",
	"markdown": "markdown",
	"docker": "docker",
	"latex": "latex",
	"graphql": "graphql",
}

ALIASES = {
	"cpp": "cpp",
	"cc": "cpp",
	"cxx": "cpp",
	"cs": "csharp",
	"js": "javascript",
	"jsx": "javascript",
	"ts": "typescript",
	"tsx": "typescript",
	"py": "python",
	"sh": "bash",
	"shell": "bash",
	"ps1": "powershell",
	"vb": "visualbasic",
	"objective-c": "objectivec",
	"obj-c": "objectivec",
	"groovyscript": "groovy",
	"pascal": "delphi",
	"coffee": "coffeescript",
	"mysql": "sql",
	"postgres": "sql",
	"postgresql": "sql",
	"sqlite": "sql",
	"yml": "yaml",
	"htm": "html",
	"scss": "sass",
	"md": "markdown",
	"dockerfile": "docker",
	"docker-compose": "docker",
	"tex": "latex",
	"gql": "graphql",
}


ICON_VARIANTS = (
	"plain",
	"original",
)


def fetch_svg(devicon_name: str) -> str:
	for variant in ICON_VARIANTS:
		url = (
			f"https://raw.githubusercontent.com/devicons/devicon/"
			f"{DEVICON_VERSION}/icons/{devicon_name}/"
			f"{devicon_name}-{variant}.svg"
		)

		print(f"Fetching {devicon_name} ({variant}): {url}")

		try:
			with urlopen(url) as response:
				return response.read().decode("utf-8")
		except HTTPError as error:
			if error.code != 404:
				raise

	raise RuntimeError(f"No usable SVG variant found for '{devicon_name}'.")


def element_to_path(element) -> str:
	"""Convert a basic SVG shape element to equivalent path data."""
	tag = element.tag.rsplit("}", 1)[-1]
	attrib = element.attrib

	if tag == "path":
		return attrib.get("d", "")

	if tag == "circle":
		cx = float(attrib["cx"])
		cy = float(attrib["cy"])
		r = float(attrib["r"])
		return (
			f"M {cx - r},{cy} "
			f"a {r},{r} 0 1,0 {2 * r},0 "
			f"a {r},{r} 0 1,0 {-2 * r},0 Z"
		)

	if tag == "ellipse":
		cx = float(attrib["cx"])
		cy = float(attrib["cy"])
		rx = float(attrib["rx"])
		ry = float(attrib["ry"])
		return (
			f"M {cx - rx},{cy} "
			f"a {rx},{ry} 0 1,0 {2 * rx},0 "
			f"a {rx},{ry} 0 1,0 {-2 * rx},0 Z"
		)

	if tag == "rect":
		x = float(attrib["x"])
		y = float(attrib["y"])
		width = float(attrib["width"])
		height = float(attrib["height"])
		return f"M {x},{y} h {width} v {height} h {-width} Z"

	if tag in ("polygon", "polyline"):
		points = attrib["points"].split()
		commands = [
			f"{float(points[i])},{float(points[i + 1])}"
			for i in range(0, len(points) - 1, 2)
		]
		data = "M " + " L ".join(commands)
		if tag == "polygon":
			data += " Z"
		return data

	if tag == "line":
		return (
			f"M {float(attrib['x1'])},{float(attrib['y1'])} "
			f"L {float(attrib['x2'])},{float(attrib['y2'])}"
		)

	return ""


PATH_COMMANDS = "MmLlHhVvCcSsQqTtAaZz"
_NUMBER = re.compile(r"[+-]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][+-]?\d+)?")


def _skip_separators(data: str, i: int) -> int:
	while i < len(data) and data[i] in " \t\r\n,":
		i += 1
	return i


def _read_number(data: str, i: int) -> tuple[str, int]:
	i = _skip_separators(data, i)
	match = _NUMBER.match(data, i)
	if not match:
		raise ValueError(
			f"Expected number in path data at {i}: {data[i : i + 24]!r}"
		)
	return match.group(0), match.end()


def parse_path(data: str) -> list[tuple[str, list[str]]]:
	"""Parse SVG path data into (command, parameters) tuples."""
	commands: list[tuple[str, list[str]]] = []
	i, length = 0, len(data)

	while i < length:
		i = _skip_separators(data, i)
		if i >= length:
			break

		command = data[i]
		if command not in PATH_COMMANDS:
			raise ValueError(
				f"Unexpected character {command!r} at {i} in {data!r}"
			)
		i += 1

		if command in "Zz":
			commands.append((command, []))
			continue

		implicit = command
		while i < length:
			i = _skip_separators(data, i)
			if i >= length or data[i] in PATH_COMMANDS:
				break

			if implicit in "Mm":
				x, i = _read_number(data, i)
				y, i = _read_number(data, i)
				commands.append((implicit, [x, y]))
				implicit = "L" if implicit == "M" else "l"
			elif implicit in "LlTt":
				x, i = _read_number(data, i)
				y, i = _read_number(data, i)
				commands.append((implicit, [x, y]))
			elif implicit in "Hh":
				x, i = _read_number(data, i)
				commands.append((implicit, [x]))
			elif implicit in "Vv":
				y, i = _read_number(data, i)
				commands.append((implicit, [y]))
			elif implicit in "Cc":
				p = []
				for _ in range(6):
					value, i = _read_number(data, i)
					p.append(value)
				commands.append((implicit, p))
			elif implicit in "SsQq":
				p = []
				for _ in range(4):
					value, i = _read_number(data, i)
					p.append(value)
				commands.append((implicit, p))
			elif implicit in "Aa":
				rx, i = _read_number(data, i)
				ry, i = _read_number(data, i)
				rotation, i = _read_number(data, i)
				i = _skip_separators(data, i)
				large_arc = data[i]
				i += 1
				i = _skip_separators(data, i)
				sweep = data[i]
				i += 1
				if large_arc not in "01" or sweep not in "01":
					raise ValueError(
						f"Invalid arc flags near {i - 2} in {data!r}"
					)
				x, i = _read_number(data, i)
				y, i = _read_number(data, i)
				commands.append(
					(implicit, [rx, ry, rotation, large_arc, sweep, x, y])
				)
			else:
				raise ValueError(f"Unhandled path command {implicit!r}")

	return commands


def normalize_path(data: str) -> str:
	"""Re-emit path data with canonical spacing (Qt PathSvg friendly)."""
	return " ".join(
		command if not params else command + " " + " ".join(params)
		for command, params in parse_path(data)
	)


def extract_paths(svg: str) -> str:
	root = ET.fromstring(svg)

	paths = []

	for element in root.iter():
		path = element_to_path(element)
		if not path:
			continue

		normalized = normalize_path(path)

		if parse_path(normalized) != parse_path(path):
			raise RuntimeError(
				"Path normalization round-trip mismatch:\n"
				f"  in : {path[:80]}\n  out: {normalized[:80]}"
			)

		paths.append(normalized)

	if not paths:
		raise RuntimeError("SVG contains no drawable shape elements")

	return " ".join(paths)


def qml_string(value: str) -> str:
	return json.dumps(value, ensure_ascii=False)


def qml_identifier(name: str) -> str:
	name = re.sub(r"[^A-Za-z0-9_]", "_", name)

	if name and name[0].isdigit():
		name = "_" + name

	return name


def generate(output: Path) -> None:
	icons = {}

	for name, devicon_name in ICONS.items():
		svg = fetch_svg(devicon_name)
		icons[name] = extract_paths(svg)

	output.parent.mkdir(parents=True, exist_ok=True)

	with output.open("w", encoding="utf-8") as file:
		file.write(
			"""pragma Singleton

import Quickshell
import QtQuick

Singleton {
"""
		)

		for icon, path in icons.items():
			identifier = qml_identifier(icon)

			file.write(
				f"    readonly property string {identifier}: "
				f"{qml_string(path)}\n"
			)

		file.write("\n")

		file.write(
			"""    function path(language) {
        const key = language.toLowerCase()

        switch (key) {
"""
		)

		for icon in icons:
			identifier = qml_identifier(icon)

			file.write(
				f'        case "{icon}":\n            return {identifier}\n'
			)

		for alias, icon in ALIASES.items():
			if icon not in icons:
				raise RuntimeError(
					f"Alias '{alias}' points to unavailable icon '{icon}'"
				)

			identifier = qml_identifier(icon)

			file.write(
				f'        case "{alias}":\n            return {identifier}\n'
			)

		file.write(
			"""        default:
            return ""
        }
    }

    function name(language) {
        switch (language.toLowerCase()) {
"""
		)

		names = {
			"c": "C",
			"cpp": "C++",
			"csharp": "C#",
			"python": "Python",
			"rust": "Rust",
			"javascript": "JavaScript",
			"typescript": "TypeScript",
			"qml": "QML",
			"bash": "Bash",
			"zsh": "Zsh",
			"powershell": "PowerShell",
			"cmake": "CMake",
			"lua": "Lua",
			"java": "Java",
			"kotlin": "Kotlin",
			"swift": "Swift",
			"go": "Go",
			"dart": "Dart",
			"php": "PHP",
			"ruby": "Ruby",
			"scala": "Scala",
			"haskell": "Haskell",
			"elixir": "Elixir",
			"erlang": "Erlang",
			"clojure": "Clojure",
			"r": "R",
			"perl": "Perl",
			"zig": "Zig",
			"nim": "Nim",
			"ocaml": "OCaml",
			"fsharp": "F#",
			"visualbasic": "Visual Basic",
			"fortran": "Fortran",
			"crystal": "Crystal",
			"gleam": "Gleam",
			"julia": "Julia",
			"objectivec": "Objective-C",
			"vala": "Vala",
			"groovy": "Groovy",
			"racket": "Racket",
			"haxe": "Haxe",
			"purescript": "PureScript",
			"delphi": "Delphi",
			"coffeescript": "CoffeeScript",
			"elm": "Elm",
			"awk": "AWK",
			"matlab": "MATLAB",
			"solidity": "Solidity",
			"wasm": "Wasm",
			"vim": "Vim",
			"sql": "SQL",
			"json": "JSON",
			"yaml": "YAML",
			"xml": "XML",
			"html": "HTML",
			"css": "CSS",
			"sass": "Sass",
			"markdown": "Markdown",
			"docker": "Docker",
			"latex": "LaTeX",
			"graphql": "GraphQL",
		}

		for icon in icons:
			if icon not in names:
				continue

			name = names[icon]

			file.write(f'        case "{icon}":\n            return "{name}"\n')

		for alias, icon in ALIASES.items():
			if icon not in names:
				continue

			file.write(
				f'        case "{alias}":\n            return "{names[icon]}"\n'
			)

		file.write(
			"""        default:
            return language
        }
    }
}
"""
		)

	print(f"Generated {output}")


if len(sys.argv) != 2:
	print(f"Usage: {sys.argv[0]} OUTPUT.qml", file=sys.stderr)
	sys.exit(1)

generate(Path(sys.argv[1]))
