"""Play SoDuckYou in a window, refereed by your own code.

Save this file in the same folder as your soduckyou.py and levels.json, then
run it. You don't need to read or change anything here: every rule the game
checks comes from the four functions you write in soduckyou.py.

Click a square once to mark it with a dot (no duck can go there), again to
put a duck down, and a third time to clear it. The panel on the right shows
what your functions return after every click, so a part that isn't done
yet, or crashes, says so there instead of stopping the game.

Author: CS149 Faculty
Version: 11/02/2026
"""

import json
import tkinter as tk

import soduckyou

CELL = 80
EMPTY, DOT, DUCK = 0, 1, 2
COLORS = ["#f7c8a0", "#b9dcf2", "#c9e4b4", "#e6c6ec", "#f4e39a", "#f3b7b7", "#b8e6dc", "#d8d0c0"]
BAD = "#d6453d"
GOOD = "#2f8f5b"
INK = "#2b2a27"
MUTED = "#6b675e"

# Everything the game needs to remember: the level, the marks, and the widgets.
game = {"grid": [], "marks": [], "solved": False}


def call(function, *args):
    """Call one of your functions without letting it stop the game.

    Args:
        function: The function from soduckyou.py to call.
        *args: The arguments to pass it.

    Returns:
        A tuple (result, error): the return value and None if the call
        worked, or None and a short message if it crashed or returned None.
    """
    try:
        result = function(*args)
    except Exception as error:  # any crash in soduckyou.py
        return None, f"{function.__name__}() crashed: {type(error).__name__}: {error}"
    if result is None:
        return None, f"{function.__name__}() returned None (not done yet?)"
    return result, None


def find_levels():
    """Load every level, with Part D if it works and a stand-in if it doesn't.

    Returns:
        A tuple (levels, note): a dictionary that maps each level's name to
        its grid, and a line saying where the levels came from.
    """
    levels = {}
    error = None
    for size in [4, 5, 6]:
        found, error = call(soduckyou.load_levels, "levels.json", size)
        if error is not None:
            break
        levels.update(found)
    if error is None and levels:
        return levels, "Levels loaded by your load_levels()."
    with open("levels.json") as file:
        data = json.load(file)
    levels = {level["name"]: level["grid"] for level in data if not level.get("draft", False)}
    reason = error or "load_levels() found no levels"
    return levels, f"Part D: {reason}\nUntil it works, a stand-in loads the levels."


def ducks():
    """Find every duck on the pond.

    Returns:
        A list of (row, column) tuples, in reading order.
    """
    size = len(game["grid"])
    return [(r, c) for r in range(size) for c in range(size) if game["marks"][r][c] == DUCK]


def referee():
    """Ask your functions about the pond.

    Returns:
        A tuple (problems, notes, pairs): the problems to list, a message
        for each part that isn't working, and the pairs of touching ducks.
    """
    grid = game["grid"]
    placed = ducks()
    problems = []
    notes = []
    pairs, error = call(soduckyou.touching, placed)
    if error:
        notes.append("Part C: " + error)
        pairs = []
    for pair in pairs:
        problems.append(f"Ducks at {pair[0]} and {pair[1]} are touching")
    breaks, error = call(soduckyou.rule_breaks, grid, placed)
    if error:
        notes.append("Part B: " + error)
        breaks = []
    for line in breaks:
        # Until the pond is full, an empty row, column, or patch is just unfinished.
        if len(placed) == len(grid) or not line.endswith(" 0 ducks"):
            problems.append(line)
    game["solved"] = len(placed) == len(grid) and problems == [] and notes == []
    if placed and problems == [] and not game["solved"]:
        problems.append("So far, so good.")
    return problems, notes, pairs


def draw_duck(x, y):
    """Draw a yellow duck centered at (x, y).

    Args:
        x: The x-coordinate of the center of the square.
        y: The y-coordinate of the center of the square.
    """
    canvas = game["canvas"]
    canvas.create_oval(x - 24, y - 4, x + 20, y + 24, fill="#ffd23f", outline="#c99a00", width=2)
    canvas.create_oval(x - 2, y - 26, x + 22, y - 2, fill="#ffd23f", outline="#c99a00", width=2)
    canvas.create_polygon(x + 20, y - 16, x + 32, y - 12, x + 20, y - 8, fill="#f28c28")
    canvas.create_oval(x + 10, y - 19, x + 15, y - 14, fill=INK, outline="")
    canvas.create_arc(x - 18, y + 2, x + 8, y + 18, start=180, extent=150, style=tk.ARC,
                      outline="#c99a00", width=2)


def draw_square(row, col, close):
    """Draw one square of the pond, with its dot or duck.

    Args:
        row: The square's row.
        col: The square's column.
        close: The list of ducks that touch another duck.
    """
    canvas = game["canvas"]
    grid = game["grid"]
    letters = sorted(set("".join(grid)))
    x, y = col * CELL, row * CELL
    color = COLORS[letters.index(grid[row][col]) % len(COLORS)]
    canvas.create_rectangle(x, y, x + CELL, y + CELL, fill=color, outline="#bbb")
    canvas.create_text(x + 6, y + 4, text=grid[row][col], anchor="nw", fill="#777")
    if game["marks"][row][col] == DOT:
        canvas.create_oval(x + 34, y + 34, x + 46, y + 46, fill="#8a8170", outline="")
    elif game["marks"][row][col] == DUCK:
        draw_duck(x + CELL // 2, y + CELL // 2 + 4)
        if (row, col) in close:
            canvas.create_rectangle(x + 3, y + 3, x + CELL - 3, y + CELL - 3, outline=BAD, width=4)


def draw_borders():
    """Draw a thick line wherever two patches meet, and around the pond."""
    canvas = game["canvas"]
    grid = game["grid"]
    size = len(grid)
    for row in range(size):
        for col in range(size):
            x, y = col * CELL, row * CELL
            if col < size - 1 and grid[row][col] != grid[row][col + 1]:
                canvas.create_line(x + CELL, y, x + CELL, y + CELL, width=4, fill=INK)
            if row < size - 1 and grid[row][col] != grid[row + 1][col]:
                canvas.create_line(x, y + CELL, x + CELL, y + CELL, width=4, fill=INK)
    canvas.create_rectangle(2, 2, size * CELL - 2, size * CELL - 2, width=4, outline=INK)


def show_panel(problems, notes):
    """Fill in the panel beside the pond.

    Args:
        problems: The problems your functions found.
        notes: A message for each part that isn't working.
    """
    shown = ["- " + line for line in problems[:6]]
    if len(problems) > 6:
        shown.append(f"- and {len(problems) - 6} more")
    if game["solved"]:
        game["status"].config(text="You solved it! Every duck has its own space.", fg=GOOD)
    elif notes or shown:
        game["status"].config(text="\n".join(notes + shown), fg=BAD)
    else:
        game["status"].config(text="Click a square to begin.", fg=INK)

    counts, error = call(soduckyou.count_patches, game["grid"], ducks())
    if error:
        game["patches"].config(text="Part A: " + error, fg=BAD)
    else:
        text = ", ".join([f"{letter}: {counts[letter]}" for letter in sorted(counts)])
        game["patches"].config(text="Ducks per patch (from your count_patches):\n" + text,
                               fg=MUTED)


def draw():
    """Redraw the pond and the panel beside it."""
    problems, notes, pairs = referee()
    close = [duck for pair in pairs for duck in pair]
    size = len(game["grid"])
    game["canvas"].delete("all")
    game["canvas"].config(width=size * CELL, height=size * CELL)
    for row in range(size):
        for col in range(size):
            draw_square(row, col, close)
    draw_borders()
    show_panel(problems, notes)


def click(event):
    """Cycle the clicked square: empty, then dot, then duck, then empty.

    Args:
        event: The mouse click, which says where on the pond it landed.
    """
    row, col = event.y // CELL, event.x // CELL
    size = len(game["grid"])
    if game["solved"] or row >= size or col >= size:
        return
    game["marks"][row][col] = (game["marks"][row][col] + 1) % 3
    draw()


def start(name):
    """Start the named level with an empty pond.

    Args:
        name: The level's name.
    """
    game["grid"] = game["levels"][name]
    size = len(game["grid"])
    game["marks"] = [[EMPTY] * size for _ in range(size)]
    draw()


def build_window(note):
    """Make the window: the pond on the left and the panel on the right.

    Args:
        note: Where the levels came from, shown at the bottom of the panel.

    Returns:
        The window.
    """
    root = tk.Tk()
    root.title("SoDuckYou")
    game["canvas"] = tk.Canvas(root, highlightthickness=0, bg="white")
    game["canvas"].grid(row=0, column=0, padx=16, pady=16)
    game["canvas"].bind("<Button-1>", click)
    side = tk.Frame(root)
    side.grid(row=0, column=1, sticky="n", padx=(0, 16), pady=16)

    names = list(game["levels"])
    game["choice"] = tk.StringVar(value=names[0])
    tk.Label(side, text="Level", font=("TkDefaultFont", 12, "bold")).pack(anchor="w")
    tk.OptionMenu(side, game["choice"], *names, command=start).pack(anchor="w", fill="x")
    clear = tk.Button(side, text="Clear", command=lambda: start(game["choice"].get()))
    clear.pack(anchor="w", pady=(6, 12))
    for key, gap in [("status", 0), ("patches", 12), ("note", 12)]:
        game[key] = tk.Label(side, justify="left", anchor="w", wraplength=320, fg=MUTED)
        game[key].pack(anchor="w", fill="x", pady=(gap, 0))
    game["status"].config(font=("TkDefaultFont", 13))
    game["note"].config(text=note)
    return root


def main():
    """Open the window and run the game."""
    game["levels"], note = find_levels()
    root = build_window(note)
    start(game["choice"].get())
    root.mainloop()


if __name__ == "__main__":
    main()
