#!/usr/bin/env python3

from __future__ import annotations

import tkinter as tk
from pathlib import Path
from tkinter import filedialog, messagebox


# ============================================================
# CONFIGURATION
#
# Doit correspondre à asm_to_schematic.py :
#   - colonnes : STORE <adresse 0-255> <registre> -> une colonne
#     par adresse mémoire
#   - lignes   : dépendent de la position du bit dans l'octet
#     écrit en registre (bit 0 = ligne du bas, bit 7 = ligne
#     du haut). Max 8 lignes puisqu'un registre fait 1 octet.
#   - LOAD n'écrit que sur 4 bits (0-15) : au-delà, il faut
#     construire la valeur avec des ADD.
# ============================================================

GRID_COLUMNS = 16
GRID_LINES = 8

CELL_SIZE = 30

# Valeur maximale qu'un LOAD peut écrire directement (4 bits).
MAX_LOAD_VALUE = 15

# Registres utilisés pour le stockage final (dessin de pixels),
# avec petit cache pour éviter de refaire le même calcul.
# On garde les autres registres libres pour le calcul et les
# comparaisons.
STORAGE_REGISTERS = ["R0", "R1"]

# Registres de calcul pour bâtir une valeur > 15 par additions
# successives : ADD lit toujours ADD_OPERAND_A et
# ADD_OPERAND_B, et écrit le résultat dans ADD_RESULT.
ADD_OPERAND_A = "R7"
ADD_OPERAND_B = "R6"
ADD_RESULT = "R5"

# Nombre max d'instructions autorisées par asm_to_schematic.py
MAX_LINES = 64

# Couleurs façon lampe redstone
COLOR_OFF = "#3b2f2f"
COLOR_ON = "#ffd83d"
COLOR_GRID_LINE = "#1a1414"


# ============================================================
# CONSTRUCTION D'UNE VALEUR (LOAD direct ou chaîne d'ADD)
# ============================================================

def _split_into_load_chunks(value: int) -> list[int]:
    """
    Découpe `value` en morceaux de MAX_LOAD_VALUE (15) maximum,
    chacun chargeable directement par un LOAD.
    """

    chunks: list[int] = []
    remaining = value

    while remaining > 0:

        chunk = min(remaining, MAX_LOAD_VALUE)
        chunks.append(chunk)
        remaining -= chunk

    return chunks


def _build_value_lines(
    value: int,
) -> tuple[list[str], str]:
    """
    Retourne les instructions nécessaires pour obtenir `value`
    dans un registre, ainsi que le nom de ce registre.

    - value <= 15 : rien à construire, le futur LOAD direct
      s'en charge ailleurs (cette fonction n'est pas appelée).
    - value > 15  : LOAD R7 <a>, LOAD R6 <b>, ADD (-> R5), puis
      un ADD supplémentaire par morceau restant en repassant
      le résultat courant par MOV.
    """

    chunks = _split_into_load_chunks(value)

    lines = [
        f"LOAD {ADD_OPERAND_A} {chunks[0]}",
        f"LOAD {ADD_OPERAND_B} {chunks[1]}",
        "ADD",
    ]

    for chunk in chunks[2:]:

        lines.append(
            f"MOV {ADD_OPERAND_A} {ADD_RESULT}"
        )
        lines.append(
            f"LOAD {ADD_OPERAND_B} {chunk}"
        )
        lines.append("ADD")

    return lines, ADD_RESULT


# ============================================================
# GÉNÉRATION DE L'ASM
# ============================================================

def generate_asm(lit_pixels: set[tuple[int, int]]) -> str:
    """
    lit_pixels : ensemble de tuples (colonne, ligne_interne)
    avec colonne 0 à GRID_COLUMNS-1 et ligne_interne 0 à
    GRID_LINES-1 (0 = pixel du bas).

    Pour chaque colonne contenant au moins un pixel allumé, on
    calcule l'octet correspondant (bit N = ligne N allumée),
    on l'obtient dans un registre (LOAD direct si <= 15, sinon
    chaîne d'ADD via R7/R6 -> R5), puis on l'écrit à l'adresse
    de cette colonne via STORE.

    R0 et R1 servent de petit cache : si l'octet à écrire est
    déjà présent dans l'un des deux, on réutilise directement
    ce registre (LOAD ou chaîne d'ADD sautés).
    """

    lines: list[str] = []

    # registre de stockage (R0/R1) -> valeur actuellement dedans
    cache: dict[str, int] = {}
    next_register_index = 0

    for column in range(GRID_COLUMNS):

        byte_value = 0

        for line_internal in range(GRID_LINES):

            if (column, line_internal) in lit_pixels:
                byte_value |= 1 << line_internal

        if byte_value == 0:
            continue

        # La valeur est-elle déjà dans R0 ou R1 ?
        register = next(
            (
                reg
                for reg, cached_value in cache.items()
                if cached_value == byte_value
            ),
            None,
        )

        if register is None:

            register = STORAGE_REGISTERS[
                next_register_index
            ]
            next_register_index = (
                next_register_index + 1
            ) % len(STORAGE_REGISTERS)

            if byte_value <= MAX_LOAD_VALUE:

                lines.append(
                    f"LOAD {register} {byte_value}"
                )

            else:

                build_lines, result_register = (
                    _build_value_lines(byte_value)
                )
                lines.extend(build_lines)

                # On rapatrie le résultat dans R0/R1 pour
                # pouvoir le réutiliser sans tout reconstruire.
                lines.append(
                    f"MOV {register} {result_register}"
                )

            cache[register] = byte_value

        lines.append(
            f"STORE {column} {register}"
        )

    lines.append("HALT")

    return "\n".join(lines) + "\n"


# ============================================================
# INTERFACE
# ============================================================

class PixelToAsmApp:

    def __init__(self, root: tk.Tk):

        self.root = root
        self.root.title(
            "Dessin -> ASM"
        )
        self.root.resizable(False, False)

        self.lit_pixels: set[tuple[int, int]] = set()
        self.cell_ids: dict[
            tuple[int, int], int
        ] = {}

        self._build_layout()

    # ========================================================
    # CONSTRUCTION DE L'INTERFACE
    # ========================================================

    def _build_layout(self) -> None:

        container = tk.Frame(
            self.root,
            bg="#1a1414",
        )
        container.pack(
            fill="both",
            expand=True,
        )

        # ----------------------------------------------------
        # Grille de pixels
        # ----------------------------------------------------

        canvas_width = GRID_COLUMNS * CELL_SIZE
        canvas_height = GRID_LINES * CELL_SIZE

        self.canvas = tk.Canvas(
            container,
            width=canvas_width,
            height=canvas_height,
            bg=COLOR_GRID_LINE,
            highlightthickness=0,
        )
        self.canvas.grid(
            row=0,
            column=0,
            columnspan=4,
            padx=10,
            pady=10,
        )

        self.canvas.bind(
            "<Button-1>",
            self._on_click,
        )

        self._draw_grid()

        # ----------------------------------------------------
        # Compteur de pixels
        # ----------------------------------------------------

        self.counter_label = tk.Label(
            container,
            text=self._counter_text(),
            bg="#1a1414",
            fg="white",
            font=("Consolas", 10),
        )
        self.counter_label.grid(
            row=1,
            column=0,
            columnspan=4,
            sticky="w",
            padx=10,
        )

        # ----------------------------------------------------
        # Boutons
        # ----------------------------------------------------

        button_row = tk.Frame(
            container,
            bg="#1a1414",
        )
        button_row.grid(
            row=2,
            column=0,
            columnspan=4,
            pady=(5, 0),
            padx=10,
            sticky="w",
        )

        tk.Button(
            button_row,
            text="Générer ASM",
            command=self._on_generate,
        ).pack(side="left", padx=(0, 5))

        tk.Button(
            button_row,
            text="Effacer tout",
            command=self._on_clear,
        ).pack(side="left", padx=(0, 5))

        tk.Button(
            button_row,
            text="Sauvegarder .asm",
            command=self._on_save,
        ).pack(side="left")

        # ----------------------------------------------------
        # Zone de résultat
        # ----------------------------------------------------

        self.output_text = tk.Text(
            container,
            width=40,
            height=20,
            bg="#0f0c0c",
            fg="#7CFC00",
            insertbackground="white",
            font=("Consolas", 10),
        )
        self.output_text.grid(
            row=3,
            column=0,
            columnspan=4,
            padx=10,
            pady=10,
        )

    # ========================================================
    # GRILLE
    # ========================================================

    def _draw_grid(self) -> None:

        # Sur le canvas tkinter, Y augmente vers le BAS, alors
        # que ligne_interne = 0 doit être affichée tout en BAS
        # (convention utilisée par asm_to_schematic.py). On
        # inverse donc la position verticale à l'affichage.

        for line_internal in range(GRID_LINES):

            row_on_screen = (
                GRID_LINES - 1 - line_internal
            )

            for column in range(GRID_COLUMNS):

                x0 = column * CELL_SIZE
                y0 = row_on_screen * CELL_SIZE
                x1 = x0 + CELL_SIZE
                y1 = y0 + CELL_SIZE

                cell_id = self.canvas.create_rectangle(
                    x0, y0, x1, y1,
                    fill=COLOR_OFF,
                    outline=COLOR_GRID_LINE,
                    width=2,
                )

                self.cell_ids[
                    (column, line_internal)
                ] = cell_id

    def _on_click(self, event: tk.Event) -> None:

        column = event.x // CELL_SIZE
        row_on_screen = event.y // CELL_SIZE

        if not (0 <= column < GRID_COLUMNS):
            return

        if not (0 <= row_on_screen < GRID_LINES):
            return

        # Même inversion qu'à l'affichage : la rangée tout en
        # bas de l'écran correspond à ligne_interne = 0.
        line_internal = (
            GRID_LINES - 1 - row_on_screen
        )

        key = (column, line_internal)

        if key in self.lit_pixels:

            self.lit_pixels.remove(key)
            self.canvas.itemconfig(
                self.cell_ids[key],
                fill=COLOR_OFF,
            )

        else:

            self.lit_pixels.add(key)
            self.canvas.itemconfig(
                self.cell_ids[key],
                fill=COLOR_ON,
            )

        self.counter_label.config(
            text=self._counter_text()
        )

    def _used_columns(self) -> set[int]:

        return {
            column
            for column, _ in self.lit_pixels
        }

    def _counter_text(self) -> str:

        total_instructions = self._instruction_count()

        return (
            f"Pixels allumés : {len(self.lit_pixels)}   "
            f"Colonnes utilisées : {len(self._used_columns())}   "
            f"Instructions : {total_instructions}/{MAX_LINES}"
        )

    def _instruction_count(self) -> int:

        asm_code = generate_asm(self.lit_pixels)

        return len(
            [
                line
                for line in asm_code.splitlines()
                if line.strip()
            ]
        )

    # ========================================================
    # ACTIONS
    # ========================================================

    def _on_clear(self) -> None:

        self.lit_pixels.clear()

        for cell_id in self.cell_ids.values():
            self.canvas.itemconfig(
                cell_id,
                fill=COLOR_OFF,
            )

        self.counter_label.config(
            text=self._counter_text()
        )

        self.output_text.delete("1.0", "end")

    def _on_generate(self) -> None:

        total_instructions = self._instruction_count()

        if total_instructions > MAX_LINES:

            messagebox.showwarning(
                "Trop d'instructions",
                (
                    f"Le dessin nécessite "
                    f"{total_instructions} instructions, "
                    f"mais asm_to_schematic.py accepte "
                    f"{MAX_LINES} maximum.\n\n"
                    "Réduis le nombre de pixels allumés."
                ),
            )

        asm_code = generate_asm(self.lit_pixels)

        self.output_text.delete("1.0", "end")
        self.output_text.insert("1.0", asm_code)

    def _on_save(self) -> None:

        asm_code = self.output_text.get(
            "1.0", "end"
        ).strip()

        if not asm_code:
            messagebox.showinfo(
                "Rien à sauvegarder",
                "Clique d'abord sur \"Générer ASM\".",
            )
            return

        file_path = filedialog.asksaveasfilename(
            defaultextension=".asm",
            filetypes=[("Fichier ASM", "*.asm")],
            initialfile="dessin.asm",
        )

        if not file_path:
            return

        Path(file_path).write_text(
            asm_code + "\n",
            encoding="utf-8",
        )

        messagebox.showinfo(
            "Sauvegardé",
            f"Fichier écrit : {file_path}",
        )


# ============================================================
# MAIN
# ============================================================

def main() -> int:

    root = tk.Tk()
    PixelToAsmApp(root)
    root.mainloop()

    return 0


if __name__ == "__main__":
    raise SystemExit(main())