#!/usr/bin/env python3

import json
import os
import re
import shlex
import shutil
import struct
import subprocess
import sys

COMMAND_TO_METHOD = {
    "branch-from": "branch_from",
    "bash-completions": "bash_completions",
}
METHOD_TO_COMMAND = {method: command for command, method in COMMAND_TO_METHOD.items()}

WAIT_FOR_PROCESS = "wait_for_process"

class DevHelper:
    SHADER_EXTENSIONS = (".vert", ".frag", ".comp", ".geom", ".tesc", ".tese")

    def __init__(self):
        self.last_exit_status = 0

    # Core logic
    def call(self, *args, **kwargs):
        if not args:
            return

        command, rest = args[0], args[1:]
        method_name = COMMAND_TO_METHOD.get(command, command)
        method = getattr(self, method_name, None)
        if callable(method) and not method_name.startswith("_") and method_name != "call":
            return method(*rest, **kwargs)

    # Primary command wrappers
    def build(self, *args, **kwargs):
        command = f"rm -rf build && cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug"
        self._exec_in_root(command, replace=True)

    ##
    # Checks out a branch named after a Gitea issue URL.
    # If the issue is from this project's repo, the branch name will be:
    #   issue-<issue_number>/<issue_title>
    # If the issue is from another repo (presumably upstream), the branch name will be:
    #   <issue_owner>-issue-<issue_number>/<issue_title>
    def branch_from(self, *args, **kwargs):
        issue_url = args[0] if args else None
        if issue_url is None:
            raise ValueError("Must provide a Gitea issue URL.")

        base_url, issue_repo, issue_number = self._parse_issue_url(issue_url)
        issue_title = self._fetch_issue_title(base_url, issue_repo, issue_number)
        branch_name = self._format_branch_name(issue_repo, issue_number, issue_title)

        exists = subprocess.run(
            ["git", "-C", self._project_root(), "rev-parse", "--verify", "--quiet", f"refs/heads/{branch_name}"],
            stdout=subprocess.DEVNULL,
        ).returncode == 0
        flag = "" if exists else "-b "
        self._exec_in_root(f"git checkout {flag}{shlex.quote(branch_name)}", replace=True)

    def renderdoc(self, *args, **kwargs):
        qrenderdoc = self._find_qrenderdoc()
        self._exec_in_root(qrenderdoc, replace=True)

    def shaders(self, *args, **kwargs):
        shaders_dir = os.path.join(self._project_root(), "engine", "src", "shaders")
        if not os.path.isdir(shaders_dir):
            raise ValueError(f"Shaders directory not found: {shaders_dir}")

        glslc = self._find_glslc()

        shader_paths = sorted(
            os.path.join(root, filename)
            for root, _dirs, files in os.walk(shaders_dir)
            for filename in files
            if filename.endswith(self.SHADER_EXTENSIONS)
        )

        if not shader_paths:
            print(f"No shaders found in {shaders_dir}")
            return

        for shader_path in shader_paths:
            self._generate_shader_header(glslc, shader_path)

    # Custom helpers

    def ownit(self, *args, **kwargs):
        file_or_directory = args[0] if args else None
        if file_or_directory is None:
            raise ValueError("Must provide a file or directory path.")

        if sys.platform.startswith("linux"):
            print(f"Take ownership of the file or directory? {file_or_directory}")
            command = f"sudo chown -R {self._user_id()}:{self._group_id()} {file_or_directory}"
            self._exec(command, replace=True)
        else:
            raise NotImplementedError(f"Not implemented for platform {sys.platform}")

    def bash_completions(self, *args, **kwargs):
        completions = sorted(
            METHOD_TO_COMMAND.get(name, name)
            for name in vars(DevHelper)
            if not name.startswith("_") and name != "call" and callable(getattr(DevHelper, name))
        )
        print(" ".join(completions))

    # Private helpers
    def _parse_issue_url(self, url):
        match = re.match(r"^(https?://[^/]+)/([^/]+)/([^/]+)/issues/(\d+)", url)
        if match is None:
            raise ValueError(f"Not a Gitea issue URL: {url}")
        return match[1], f"{match[2]}/{match[3]}", match[4]

    def _fetch_issue_title(self, base_url, repo, issue_number):
        if shutil.which("tea") is None:
            raise RuntimeError("tea not found on PATH (install gitea tea and run `tea login add`)")

        endpoint = f"{base_url}/api/v1/repos/{repo}/issues/{issue_number}"
        print(f"Running: tea api {endpoint}")
        result = subprocess.run(["tea", "api", endpoint], stdout=subprocess.PIPE, text=True)
        if result.returncode != 0:
            raise RuntimeError(f"failed to fetch issue title from {endpoint}")
        return json.loads(result.stdout)["title"]

    def _format_branch_name(self, issue_repo, issue_number, issue_title):
        formatted_title = re.sub(r"-+", "-", re.sub(r"[^a-z0-9\-]", "", re.sub(r"\s+", "-", issue_title.lower().strip())))
        if issue_repo == self._origin_repo():
            return f"issue-{issue_number}/{formatted_title}"
        issue_owner = issue_repo.split("/")[0]
        return f"{issue_owner}-issue-{issue_number}/{formatted_title}"

    def _origin_repo(self):
        result = subprocess.run(
            ["git", "-C", self._project_root(), "remote", "get-url", "origin"],
            stdout=subprocess.PIPE,
            text=True,
        )
        match = re.search(r"[:/]([^/:]+/[^/]+?)(?:\.git)?$", result.stdout.strip())
        return match[1] if match else None

    def _find_glslc(self):
        glslc = shutil.which("glslc")
        if glslc is None:
            raise RuntimeError(
                "glslc not found on PATH (install the Vulkan SDK)"
            )
        return glslc

    def _find_qrenderdoc(self):
        qrenderdoc = shutil.which("qrenderdoc")
        if qrenderdoc is None:
            raise RuntimeError(
                "qrenderdoc not found on PATH (install RenderDoc)"
            )
        return qrenderdoc

    def _generate_shader_header(self, glslc, shader_path):
        relative_path = os.path.relpath(shader_path, self._project_root())
        print(f"Compiling shader: {relative_path}")

        result = subprocess.run(
            [glslc, shader_path, "-o", "-"],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )
        if result.returncode != 0:
            raise RuntimeError(
                f"failed to compile {shader_path}: {result.stderr.decode()}"
            )

        spirv = result.stdout
        if len(spirv) % 4 != 0:
            raise RuntimeError(
                f"invalid SPIR-V output for {shader_path} (size not a multiple of 4)"
            )

        word_count = len(spirv) // 4
        words = struct.unpack(f"<{word_count}I", spirv)

        filename = os.path.basename(shader_path)
        variable_name = f"{filename.replace('.', '_')}_spv"
        header_path = shader_path + ".spv.h"

        body_lines = [
            "    " + ", ".join(f"0x{word:08x}" for word in words[i:i + 4]) + ","
            for i in range(0, len(words), 4)
        ]

        header = "\n".join([
            "#pragma once",
            "",
            "#include <array>",
            "#include <cstdint>",
            "",
            "namespace sophia::shaders {",
            "",
            f"inline constexpr std::array<uint32_t, {word_count}> {variable_name} = {{",
            *body_lines,
            "};",
            "",
            "}  // namespace sophia::shaders",
            "",
        ])

        with open(header_path, "w") as f:
            f.write(header)

        print(f"Wrote {os.path.relpath(header_path, self._project_root())} "
              f"({word_count * 4} bytes)")

    def _wait_for_process_with_logging(self, command):
        process = self._exec(command, capture=True)
        for line in process.stdout:
            print(line, end="")
        process.wait()
        self.last_exit_status = process.returncode

    def _exec_in_root(self, command, **kwargs):
        return self._exec(f"cd {self._project_root()} && {command}", **kwargs)

    def _exec(self, command, *, replace=False, capture=False):
        print(f"Running: {command}")
        if replace:
            os.execv("/bin/sh", ["/bin/sh", "-c", command])
        if capture:
            return subprocess.Popen(
                ["/bin/sh", "-c", f"{command} 2>&1"],
                stdout=subprocess.PIPE,
                text=True,
            )
        return subprocess.run(["/bin/sh", "-c", command])

    def _project_root(self):
        return os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))

    def _take_over_needed(self, file_or_directory):
        result = subprocess.run(
            ["/bin/sh", "-c", f"find {file_or_directory} -not -user {self._user_id()} -print -quit | grep -q ."],
        )
        return result.returncode == 0

    def _user_id(self):
        if not sys.platform.startswith("linux"):
            raise NotImplementedError(f"Not implemented for platform {sys.platform}")
        return subprocess.run(["id", "-u"], stdout=subprocess.PIPE, text=True).stdout.strip()

    def _group_id(self):
        if not sys.platform.startswith("linux"):
            raise NotImplementedError(f"Not implemented for platform {sys.platform}")
        return subprocess.run(["id", "-g"], stdout=subprocess.PIPE, text=True).stdout.strip()


if __name__ == "__main__":
    DevHelper().call(*sys.argv[1:])
