# CrsGame.py
# High-level curses wrapper with automatic delta rendering

import curses
import asyncio

# -------------------------------
# Core Objects
# -------------------------------

class Vector:
    def __init__(self, x=0.0, y=0.0):
        self.X = x
        self.Y = y

class Color:
    def __init__(self, name="white"):
        colors = {
            "black": curses.COLOR_BLACK,
            "red": curses.COLOR_RED,
            "green": curses.COLOR_GREEN,
            "yellow": curses.COLOR_YELLOW,
            "blue": curses.COLOR_BLUE,
            "magenta": curses.COLOR_MAGENTA,
            "cyan": curses.COLOR_CYAN,
            "white": curses.COLOR_WHITE,
        }
        self.Color = colors.get(name, curses.COLOR_WHITE)

class Line:
    def __init__(self, start: Vector, end: Vector, char="#", color="white"):
        self.Start = start
        self.End = end
        self.Char = char
        self.Color = color

    def GetCells(self):
        """Return all integer cells this line occupies (Bresenham)"""
        x0, y0 = round(self.Start.X), round(self.Start.Y)
        x1, y1 = round(self.End.X), round(self.End.Y)
        cells = []

        dx = abs(x1 - x0)
        dy = abs(y1 - y0)
        sx = 1 if x0 < x1 else -1
        sy = 1 if y0 < y1 else -1
        err = dx - dy

        while True:
            cells.append((y0, x0))
            if x0 == x1 and y0 == y1: break
            e2 = 2 * err
            if e2 > -dy:
                err -= dy
                x0 += sx
            if e2 < dx:
                err += dx
                y0 += sy
        return cells

class Box:
    def __init__(self, pos: Vector, size: Vector, fill=True, char="#", color="white"):
        self.Position = pos
        self.Size = size
        self.Fill = fill
        self.Char = char
        self.Color = color

    def GetCells(self):
        """Return all cells this box currently occupies"""
        cells = []
        x0, y0 = int(self.Position.X), int(self.Position.Y)
        x1, y1 = int(self.Position.X + self.Size.X), int(self.Position.Y + self.Size.Y)
        for y in range(y0, y1):
            for x in range(x0, x1):
                cells.append((y, x))
        return cells

class Quad:
    def __init__(self, top_left, top_right, bottom_left, bottom_right, fill=True, char="#", color="white"):
        self.TopLeft = top_left
        self.TopRight = top_right
        self.BottomLeft = bottom_left
        self.BottomRight = bottom_right
        self.Fill = fill
        self.Char = char
        self.Color = color

    def GetCells(self):
        vertices = [
            (self.TopLeft.X, self.TopLeft.Y),
            (self.TopRight.X, self.TopRight.Y),
            (self.BottomRight.X, self.BottomRight.Y),
            (self.BottomLeft.X, self.BottomLeft.Y),
        ]

        edges = [(vertices[i], vertices[(i + 1) % 4]) for i in range(4)]
        cells = set()

        # Draw edges
        for (x0, y0), (x1, y1) in edges:
            x0, y0, x1, y1 = round(x0), round(y0), round(x1), round(y1)
            dx = abs(x1 - x0)
            dy = abs(y1 - y0)
            sx = 1 if x0 < x1 else -1
            sy = 1 if y0 < y1 else -1
            err = dx - dy
            while True:
                cells.add((y0, x0))
                if x0 == x1 and y0 == y1:
                    break
                e2 = 2 * err
                if e2 > -dy:
                    err -= dy
                    x0 += sx
                if e2 < dx:
                    err += dx
                    y0 += sy

        if self.Fill:
            min_y = min(round(v[1]) for v in vertices)
            max_y = max(round(v[1]) for v in vertices)
            for y in range(min_y, max_y + 1):
                intersections = []
                for (x0, y0), (x1, y1) in edges:
                    if y0 == y1: 
                        continue
                    if min(y0, y1) <= y <= max(y0, y1):
                        x = x0 + (y - y0) * (x1 - x0) / (y1 - y0)
                        intersections.append(x)
                intersections.sort()
                for i in range(0, len(intersections), 2):
                    x_start = round(intersections[i])
                    if i + 1 < len(intersections):
                        x_end = round(intersections[i + 1])
                        for x in range(x_start, x_end + 1):
                            cells.add((y, x))

        return list(cells)

class Text:
    def __init__(self, pos: Vector, txt: "TextLabel", color="white"):
        self.Position = pos
        self.Text = txt
        self.Color = color

    def GetCells(self):
        cells = []

        for i in range(len(self.Text)):
            cells.append((self.Position.Y, self.Position.X + i))

        return cells

# -------------------------------
# Event System
# -------------------------------

class Event:
    def __init__(self):
        self._callbacks = []

    def Connect(self, fn):
        self._callbacks.append(fn)

    def Fire(self, *args, **kwargs):
        for fn in self._callbacks:
            res = fn(*args, **kwargs)
            if asyncio.iscoroutine(res):
                asyncio.create_task(res)

# -------------------------------
# Renderer
# -------------------------------

class Renderer:
    def __init__(self, stdscr, bg_color=Color("black"), fps=30):
        curses.curs_set(0)
        curses.start_color()
        curses.use_default_colors()
        self._screen = stdscr
        self._screen.nodelay(True)
        self.FPS = fps

        self.BGColor = bg_color
        self.Colors = {}

        h, w = stdscr.getmaxyx()
        self.ScreenSize = Vector(w, h)
        self._screen_buffer = [[(' ', -1) for _ in range(w)] for _ in range(h)]
        self.RenderStep = Event()
        self.InputSent = Event()
        self._prev_object_cells = {}  # object -> list of (y,x)

    # -------------------------------
    # Color
    # -------------------------------
    def RegisterColor(self, name, clr: Color):
        idx = 1
        used = set(self.Colors.values())
        while idx in used:
            idx += 1
        curses.init_pair(idx, clr.Color, self.BGColor.Color)
        self.Colors[name] = idx

    # -------------------------------
    # Draw helpers
    # -------------------------------
    def _draw_char(self, y, x, char, color_idx):
        if 0 <= y < int(self.ScreenSize.Y) and 0 <= x < int(self.ScreenSize.X):
            cur_char, cur_color = self._screen_buffer[y][x]
            if cur_char != char or cur_color != color_idx:
                try:
                    self._screen.addch(y, x, char, curses.color_pair(color_idx))
                    self._screen_buffer[y][x] = (char, color_idx)
                except curses.error:
                    pass

    # -------------------------------
    # Draw Objects
    # -------------------------------
    def draw_box(self, box: Box):
        # Clear previous
        for y, x in self._prev_object_cells.get(box, []):
            self._draw_char(y, x, ' ', 0)
        color_idx = self.Colors.get(box.Color, 0)
        cells = box.GetCells()
        for y, x in cells:
            self._draw_char(y, x, box.Char, color_idx)
        self._prev_object_cells[box] = cells

    def draw_line(self, line: Line):
        for y, x in self._prev_object_cells.get(line, []):
            self._draw_char(y, x, ' ', 0)
        color_idx = self.Colors.get(line.Color, 0)
        cells = line.GetCells()
        for y, x in cells:
            self._draw_char(y, x, line.Char, color_idx)
        self._prev_object_cells[line] = cells

    def draw_quad(self, quad: Quad):
        for y, x in self._prev_object_cells.get(quad, []):
            self._draw_char(y, x, ' ', 0)
        color_idx = self.Colors.get(quad.Color, 0)
        cells = quad.GetCells()
        for y, x in cells:
            self._draw_char(y, x, quad.Char, color_idx)
        self._prev_object_cells[quad] = cells

    def draw_text(self, text: Text):
        for y, x in self._prev_object_cells.get(text, []):
            self._draw_char(y, x, ' ', 0)
        color_idx = self.Colors.get(text.Color, 0)
        cells = text.GetCells()
        for y, x in cells:
            ch_idx = x - text.Position.X
            self._draw_char(y, x, text.Text[ch_idx], color_idx)
        self._prev_object_cells[text] = cells

    # -------------------------------
    # Render
    # -------------------------------
    def Render(self, objects):
        for obj in objects:
            if isinstance(obj, Box):
                self.draw_box(obj)
            elif isinstance(obj, Line):
                self.draw_line(obj)
            elif isinstance(obj, Quad):
                self.draw_quad(obj)
            elif isinstance(obj, Text):
                self.draw_text(obj)
        self._screen.refresh()

    # -------------------------------
    # Async loop
    # -------------------------------
    async def _loop(self):
        while True:
            self.RenderStep.Fire(1/self.FPS)
            try:
                key = self._screen.getch()
                if key != -1:
                    self.InputSent.Fire(key)
            except curses.error:
                pass
            await asyncio.sleep(1/self.FPS)

    def Run(self):
        asyncio.run(self._loop())