import json
import time
import threading
import urllib.request
from http.server import HTTPServer, BaseHTTPRequestHandler
from typing import List, Tuple

from functools import partial

from pyos import Application, Activity, Segue, Length, Percentage, Fill, Min
from pyos import Service, ServiceState
from pyos import (
    NORMAL, BOLD, DIM, UNDERLINE, REVERSE,
    RED, GREEN, BLUE, YELLOW, MAGENTA, CYAN, WHITE,
    fg, bg,
)
from pyos import Keys
from pyos.CentralDispatch import CentralDispatch
from pyos.EventTypes import KeyStroke, KeyRelease, ScrollChange, TextBoxChange, TextBoxSubmit, MouseClick, MouseDrag, MouseScroll, MouseButton
from pyos.input_handlers import (
    handle_scroll_list_input,
    handle_text_box_input,
)
from pyos.ContextUtils import (
    get_items_len,
    scroll_up,
    scroll_down,
)
from pyos.printers.TopBar import TopBar
from pyos.printers.BottomBar import BottomBar
from pyos.printers.HorizontalBar import HorizontalBar
from pyos.printers.MultilineText import MultilineText
from pyos.printers.TextInput import TextInput
from pyos.printers.ScrollList import ScrollList
from pyos.printers.Table import Table
from pyos.printers.ContextMenuList import ContextMenuList
from pyos.printers.ThreadList import ThreadList
from pyos.printers.Spacer import Spacer
from pyos.printers.Accordion import Accordion


# ---------------------------------------------------------------------------
# WebServerService — a concrete Service for the gallery demo
# ---------------------------------------------------------------------------

class _CountingHandler(BaseHTTPRequestHandler):
    """Simple HTTP handler that returns JSON with request count."""

    def do_GET(self):
        self.server.request_count += 1
        body = json.dumps({
            "message": "Hello from pyos WebServerService",
            "request_count": self.server.request_count,
        }).encode()
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, format, *args):
        pass  # suppress stderr logging inside curses


class WebServerService(Service):
    """A tiny HTTP server running as a pyos Service."""

    def __init__(self, port=8765):
        super().__init__()
        self.port = port
        self._server = None
        self._thread = None
        self._last_request_count = 0

    @property
    def url(self):
        return f"http://localhost:{self.port}"

    @property
    def request_count(self):
        server = self._server
        if server:
            return server.request_count
        return self._last_request_count

    def on_start(self):
        self._server = HTTPServer(("127.0.0.1", self.port), _CountingHandler)
        self._server.request_count = 0
        self._last_request_count = 0
        self._thread = threading.Thread(
            target=self._server.serve_forever, daemon=True
        )
        self._thread.start()

    def on_stop(self):
        if self._server:
            self._server.shutdown()
            self._server.server_close()
            if self._thread:
                self._thread.join(timeout=2)
            self._last_request_count = self._server.request_count
            self._server = None
            self._thread = None


# ---------------------------------------------------------------------------
# Sample data
# ---------------------------------------------------------------------------

CONTACTS = [
    {"name": "Alice Johnson", "email": "alice@example.com", "dept": "Engineering"},
    {"name": "Bob Martinez", "email": "bob@example.com", "dept": "Design"},
    {"name": "Carol Chen", "email": "carol@example.com", "dept": "Engineering"},
    {"name": "David Kim", "email": "david@example.com", "dept": "Marketing"},
    {"name": "Elena Rossi", "email": "elena@example.com", "dept": "Engineering"},
    {"name": "Frank Hughes", "email": "frank@example.com", "dept": "Sales"},
    {"name": "Grace Okafor", "email": "grace@example.com", "dept": "Design"},
    {"name": "Henry Tanaka", "email": "henry@example.com", "dept": "Engineering"},
    {"name": "Irene Novak", "email": "irene@example.com", "dept": "Marketing"},
    {"name": "James Liu", "email": "james@example.com", "dept": "Sales"},
    {"name": "Karen Walsh", "email": "karen@example.com", "dept": "Engineering"},
    {"name": "Leo Fernandez", "email": "leo@example.com", "dept": "Design"},
    {"name": "Maya Patel", "email": "maya@example.com", "dept": "Engineering"},
    {"name": "Nathan Brooks", "email": "nathan@example.com", "dept": "Marketing"},
    {"name": "Olivia Grant", "email": "olivia@example.com", "dept": "Sales"},
    {"name": "Paul Dubois", "email": "paul@example.com", "dept": "Engineering"},
    {"name": "Quinn Harper", "email": "quinn@example.com", "dept": "Design"},
    {"name": "Rachel Stone", "email": "rachel@example.com", "dept": "Marketing"},
    {"name": "Sam Volkov", "email": "sam@example.com", "dept": "Sales"},
    {"name": "Tina Yang", "email": "tina@example.com", "dept": "Engineering"},
]

TASKS_SEED = [
    {"title": "Set up CI pipeline", "priority": "high", "done": False},
    {"title": "Write unit tests for auth module", "priority": "high", "done": False},
    {"title": "Update README with API docs", "priority": "medium", "done": False},
    {"title": "Fix date picker timezone bug", "priority": "high", "done": False},
    {"title": "Refactor database migrations", "priority": "medium", "done": False},
    {"title": "Add dark mode support", "priority": "low", "done": False},
    {"title": "Review PR #42 from Elena", "priority": "medium", "done": False},
    {"title": "Benchmark query performance", "priority": "low", "done": False},
    {"title": "Deploy staging environment", "priority": "high", "done": False},
    {"title": "Design onboarding flow mockups", "priority": "medium", "done": False},
    {"title": "Audit npm dependencies", "priority": "low", "done": False},
    {"title": "Prepare sprint retro slides", "priority": "low", "done": False},
]


# ============================================================================
# Gallery menu
# ============================================================================

class DemoMenuActivity(Activity):
    """
    The gallery lobby. Pick an exhibit with arrow keys, Enter to open.
    Press F1 anywhere to see the live log viewer.
    ESC backs out of any screen.
    """

    def __init__(self):
        super().__init__()
        self.items = [
            "Contacts Browser",
            "Data Dashboard",
            "Notepad",
            "Task Manager",
            "Comment Thread",
            "Accordion Showcase",
            "Layout Lab",
            "Constraint Layout",
            "Color Showcase",
            "Web Server",
            "Reflex Game",
            "Paint",
            "Raise Exception (demo)",
        ]

    def on_start(self):
        self.application.subscribe(KeyStroke, self, self.on_key_stroke)
        self.application.subscribe(ScrollChange, self, self.on_scroll_change)

        welcome = [
            "Welcome to the PyOS Component Gallery!",
            "Each exhibit demonstrates a different UI pattern. Explore them all",
            "to see what you can build with PyOS.",
            "",
            "  TAB focus cycling    Context menus     Accordion sections",
            "  Live search/filter   Flex layouts      Constraint layouts",
        ]

        self.display_state = {
            "top": TopBar.display_state(items={
                "title": "PyOS Gallery",
                "help": "Enter: open  |  ESC: quit  |  F1: logs"
            }),
            "welcome": MultilineText.display_state(
                lines=welcome,
                min_height=6,
                max_height=6,
            ),
            "hr": HorizontalBar.display_state(),
            "list": ScrollList.display_state(
                self.screen,
                items=self.items,
                selected_index=0,
                focused=True,
                input_handler=handle_scroll_list_input,
                min_height=6,
                flex=1
            ),
            "bottom": BottomBar.display_state(items={
                "hint": "13 exhibits to explore"
            }),
        }
        self.refresh_screen()

    def on_scroll_change(self, _):
        idx = int(self.display_state["list"].get("selected_index", 0))
        total = get_items_len(self.display_state["list"]) or 0
        self.display_state["bottom"]["items"]["selection"] = f"{idx+1}/{total}"
        self.refresh_screen()

    def on_key_stroke(self, event: KeyStroke):
        handle_scroll_list_input("list", self.display_state["list"], event, self.event_queue)

        if event.key in (Keys.ENTER, 343, 13):
            idx = int(self.display_state["list"].get("selected_index", 0))
            label = self.items[idx]
            targets = {
                "Contacts Browser": ContactsBrowser,
                "Data Dashboard": DataDashboard,
                "Notepad": NotepadDemo,
                "Task Manager": TaskManagerDemo,
                "Comment Thread": CommentThreadDemo,
                "Accordion Showcase": AccordionShowcase,
                "Layout Lab": LayoutShowcase,
                "Constraint Layout": ConstraintLayoutDemo,
                "Color Showcase": ColorShowcase,
                "Web Server": WebServerActivity,
                "Reflex Game": ReflexGame,
                "Paint": PaintDemo,
            }
            if label in targets:
                self.application.segue_to(targets[label]())
            elif label == "Raise Exception (demo)":
                raise RuntimeError("Intentional demo exception from DemoMenuActivity")
        elif event.key == Keys.ESC:
            self.application.pop_activity()

        self.refresh_screen()


# ============================================================================
# 1. Contacts Browser  --  search + scroll list with TAB focus cycling
# ============================================================================

class ContactsBrowser(Activity):
    """
    Shows TAB-cycling between a search bar and a scroll list.
    Type to filter contacts in real time.
    """

    def __init__(self):
        super().__init__()
        self.all_contacts = [f"{c['name']}  ({c['dept']})" for c in CONTACTS]

    def on_start(self):
        self.application.subscribe(KeyStroke, self, self.on_key_stroke)
        self.application.subscribe(TextBoxChange, self, self.on_search_change)
        self.application.subscribe(ScrollChange, self, self.on_scroll_change)

        self.display_state = {
            "top": TopBar.display_state(items={
                "title": "Contacts Browser",
                "help": "TAB: switch focus  |  Type to filter  |  ESC: back"
            }),
            "search": TextInput.display_state(
                label="Search",
                text="",
                focused=True,
                input_handler=handle_text_box_input,
            ),
            "hr": HorizontalBar.display_state(),
            "results": ScrollList.display_state(
                self.screen,
                items=list(self.all_contacts),
                selected_index=0,
                focused=False,
                input_handler=handle_scroll_list_input,
                min_height=6,
                flex=1,
            ),
            "bottom": BottomBar.display_state(items={
                "count": f"{len(self.all_contacts)} contacts"
            }),
        }
        self.tab_order = ["search", "results"]
        self.focus = "search"
        self.refresh_screen()

    def _filter_contacts(self, query):
        q = query.lower()
        return [c for c in self.all_contacts if q in c.lower()]

    def on_search_change(self, _):
        query = self.display_state["search"].get("text", "")
        filtered = self._filter_contacts(query)
        self.display_state["results"]["items"] = filtered
        self.display_state["results"]["selected_index"] = 0
        n = len(filtered)
        self.display_state["bottom"]["items"]["count"] = f"{n} contact{'s' if n != 1 else ''}"
        self.refresh_screen()

    def on_scroll_change(self, _):
        items = self.display_state["results"].get("items", [])
        idx = int(self.display_state["results"].get("selected_index", 0))
        if items:
            self.display_state["bottom"]["items"]["selected"] = items[idx]
        self.refresh_screen()

    def on_key_stroke(self, event: KeyStroke):
        if event.key == Keys.TAB:
            self.cycle_focus()
            self.refresh_screen()
            return

        if event.key == Keys.ESC:
            self.application.pop_activity()
            return

        self.delegate_to_focused(event)
        self.refresh_screen()


# ============================================================================
# 2. Data Dashboard  --  table + summary panel
# ============================================================================

class DataDashboard(Activity):
    """
    A table of project metrics with a summary section above.
    Shows multi-section layout with flex + max_height.
    """

    def on_start(self):
        self.application.subscribe(KeyStroke, self, self.on_key_stroke)
        self.application.subscribe(ScrollChange, self, self.on_scroll_change)

        headers = ["Project", "Status", "Tasks", "Bugs", "Coverage"]
        rows = [
            ["auth-service", "Active", "34", "2", "87%"],
            ["web-frontend", "Active", "58", "5", "72%"],
            ["mobile-app", "Active", "41", "8", "65%"],
            ["data-pipeline", "Maintenance", "12", "0", "91%"],
            ["api-gateway", "Active", "27", "3", "78%"],
            ["ml-recommender", "Beta", "19", "1", "83%"],
            ["admin-panel", "Active", "22", "4", "69%"],
            ["notification-svc", "Active", "15", "0", "95%"],
            ["search-engine", "Beta", "31", "6", "71%"],
            ["analytics-dash", "Planning", "8", "0", "0%"],
            ["billing-system", "Active", "25", "1", "88%"],
            ["cdn-manager", "Maintenance", "9", "0", "93%"],
        ]

        total_tasks = sum(int(r[2]) for r in rows)
        total_bugs = sum(int(r[3]) for r in rows)
        active = sum(1 for r in rows if r[1] == "Active")

        summary = [
            f"  Projects: {len(rows)}  |  Active: {active}  |  Tasks: {total_tasks}  |  Open bugs: {total_bugs}",
        ]

        self.display_state = {
            "top": TopBar.display_state(items={
                "title": "Data Dashboard",
                "help": "arrow keys: navigate  |  ESC: back"
            }),
            "summary": MultilineText.display_state(
                lines=summary,
                min_height=1,
                max_height=1,
            ),
            "hr": HorizontalBar.display_state(),
            "table": Table.display_state(
                self.screen,
                headers=headers,
                rows=rows,
                column_justify=["left", "left", "right", "right", "right"],
                column_min_widths=[15, 12, 6, 5, 8],
                padding=1,
                show_header_delimiter=True,
                min_height=8,
                flex=1,
            ),
            "bottom": BottomBar.display_state(items={
                "hint": "Navigate with arrow keys"
            }),
        }
        self.display_state["table"]["focused"] = True
        self.display_state["table"]["selected_index"] = 0
        self.refresh_screen()

    def on_scroll_change(self, _):
        self._update_status()
        self.refresh_screen()

    def _update_status(self):
        table = self.display_state["table"]
        rows = table.get("rows", [])
        sel = int(table.get("selected_index", 0))
        n = len(rows)
        if rows and 0 <= sel < n:
            row = rows[sel]
            self.display_state["bottom"]["items"]["row"] = f"{sel+1}/{n}"
            self.display_state["bottom"]["items"]["project"] = row[0]
        else:
            self.display_state["bottom"]["items"]["row"] = f"0/{n}"

    def on_key_stroke(self, event: KeyStroke):
        table_ctx = self.display_state["table"]
        n = len(table_ctx.get("rows", []))
        sel = int(table_ctx.get("selected_index", 0))

        if event.key == Keys.UP:
            sel = max(0, sel - 1)
        elif event.key == Keys.DOWN:
            sel = min(max(0, n - 1), sel + 1)
        elif event.key == Keys.ESC:
            self.application.pop_activity()
            return

        table_ctx["selected_index"] = sel
        self._update_status()
        self.refresh_screen()


# ============================================================================
# 3. Notepad  --  text input, live stats, submission log
# ============================================================================

class NotepadDemo(Activity):
    """
    Enhanced text input with live word/char count and a submission log.
    Shows TextInput + MultilineText + dynamic state updates.
    """

    def __init__(self):
        super().__init__()
        self.submissions = []

    def on_start(self):
        self.application.subscribe(KeyStroke, self, self.on_key_stroke)
        self.application.subscribe(TextBoxChange, self, self.on_change)
        self.application.subscribe(TextBoxSubmit, self, self.on_submit)

        self.display_state = {
            "top": TopBar.display_state(items={
                "title": "Notepad",
                "help": "Type text  |  Enter: submit  |  ESC: back"
            }),
            "input": TextInput.display_state(
                label="Note",
                text="",
                focused=True,
                input_handler=handle_text_box_input,
            ),
            "stats": MultilineText.display_state(
                lines=["  Chars: 0  |  Words: 0"],
                min_height=1,
                max_height=1,
            ),
            "hr": HorizontalBar.display_state(),
            "log": MultilineText.display_state(
                lines=["(Submitted notes appear here)"],
                min_height=3,
                flex=1,
            ),
            "bottom": BottomBar.display_state(items={
                "submissions": "0 notes submitted"
            }),
        }
        self.refresh_screen()

    def _update_stats(self):
        text = self.display_state["input"].get("text", "")
        chars = len(text)
        words = len(text.split()) if text.strip() else 0
        self.display_state["stats"]["lines"] = [
            f"  Chars: {chars}  |  Words: {words}"
        ]

    def on_change(self, _):
        self._update_stats()
        self.refresh_screen()

    def on_submit(self, _):
        text = self.display_state["input"].get("text", "").strip()
        if text:
            n = len(self.submissions) + 1
            self.submissions.append(text)
            self.display_state["input"]["text"] = ""
            self.display_state["input"]["cursor_index"] = 0
            self._update_stats()

            log_lines = []
            for i, note in enumerate(reversed(self.submissions), 1):
                log_lines.append(f"  [{len(self.submissions) - i + 1}] {note}")
            self.display_state["log"]["lines"] = log_lines

            count = len(self.submissions)
            self.display_state["bottom"]["items"]["submissions"] = (
                f"{count} note{'s' if count != 1 else ''} submitted"
            )
        self.refresh_screen()

    def on_key_stroke(self, event: KeyStroke):
        handle_text_box_input("input", self.display_state["input"], event, self.event_queue)
        if event.key == Keys.ESC:
            self.application.pop_activity()
        self.refresh_screen()


# ============================================================================
# 4. Task Manager  --  context menus + state mutations
# ============================================================================

class TaskManagerDemo(Activity):
    """
    A task list with context menus for Complete, Delete, and View.
    Shows ContextMenuList with real state mutations and counters.
    """

    def __init__(self):
        super().__init__()
        self.tasks = [dict(t) for t in TASKS_SEED]

    def on_start(self):
        self.application.subscribe(KeyStroke, self, self.on_key_stroke)

        self.display_state = {
            "top": TopBar.display_state(items={
                "title": "Task Manager",
                "help": "Space: menu  |  left/right: pick  |  Enter: run  |  ESC: back"
            }),
            "list": ContextMenuList.display_state(
                self.screen,
                items=list(self.tasks),
                item_renderer=self._render_task,
                menu={"selected_index": 0, "items": [
                    {"text": "Complete"},
                    {"text": "Delete"},
                    {"text": "View"},
                ]},
                selected_index=0,
                focused=True,
                min_height=8,
                flex=1,
            ),
            "bottom": BottomBar.display_state(items={}),
        }
        self._update_counts()
        self.refresh_screen()

    @staticmethod
    def _render_task(task, screen) -> List[str]:
        check = "[x]" if task["done"] else "[ ]"
        prio = task["priority"].upper()
        return [f"{check} [{prio}] {task['title']}"]

    def _update_counts(self):
        total = len(self.tasks)
        done = sum(1 for t in self.tasks if t["done"])
        pending = total - done
        self.display_state["bottom"]["items"]["status"] = (
            f"Total: {total}  |  Done: {done}  |  Pending: {pending}"
        )

    def _current_line_item(self):
        ctx = self.display_state["list"]
        line_ids = ctx.get("line_ids", [])
        idx = int(ctx.get("selected_index", 0))
        return line_ids[idx] if 0 <= idx < len(line_ids) else None

    def _run_action(self, choice: str, item):
        if item is None:
            return
        lst = self.display_state["list"]
        items = lst.get("items", [])
        label = (choice or "").strip().lower()

        if label == "complete":
            item["done"] = not item["done"]
            status = "completed" if item["done"] else "reopened"
            self.display_state["bottom"]["items"]["action"] = f"{status}: {item['title']}"
            lst["selected_item"] = None
            self._update_counts()
            self.refresh_screen()
            return

        if label == "delete":
            if item in items:
                pos = items.index(item)
                items.pop(pos)
                self.tasks = [t for t in self.tasks if t is not item]
                lst["selected_item"] = None
                new_len = len(items)
                if new_len == 0:
                    lst["selected_index"] = 0
                else:
                    lst["selected_index"] = min(pos, new_len - 1)
                self.display_state["bottom"]["items"]["action"] = f"Deleted: {item['title']}"
                self._update_counts()
                self.refresh_screen()
            return

        if label == "view":
            self.application.segue_to(TaskDetailActivity(item))
            lst["selected_item"] = None
            return

        lst["selected_item"] = None
        self.refresh_screen()

    def on_key_stroke(self, event: KeyStroke):
        lst = self.display_state["list"]
        opened = lst.get("selected_item") is not None

        if event.key == ord(' '):
            if opened:
                lst["selected_item"] = None
            else:
                lst["selected_item"] = self._current_line_item()
            self.refresh_screen()
            return

        if event.key in (Keys.ENTER, 343, 13):
            if opened:
                menu = lst.get("menu", {})
                m_idx = int(menu.get("selected_index", 0))
                m_items = list(menu.get("items", []))
                choice = (m_items[m_idx] if 0 <= m_idx < len(m_items) else {}).get("text", "?")
                item = lst.get("selected_item")
                self._run_action(choice, item)
            return

        if event.key == Keys.ESC:
            if opened:
                lst["selected_item"] = None
            else:
                self.application.pop_activity()
            self.refresh_screen()
            return

        handle_scroll_list_input("list", lst, event, self.event_queue)
        self.refresh_screen()


class TaskDetailActivity(Activity):
    """Detail view for a single task."""

    def __init__(self, task: dict):
        super().__init__()
        self.task = task

    def on_start(self):
        self.application.subscribe(KeyStroke, self, self.on_key_stroke)

        t = self.task
        status = "Done" if t["done"] else "Pending"
        lines = [
            f"  Title:    {t['title']}",
            f"  Priority: {t['priority'].upper()}",
            f"  Status:   {status}",
        ]

        self.display_state = {
            "top": TopBar.display_state(items={
                "title": "Task Detail",
                "help": "ESC: back"
            }),
            "hr": HorizontalBar.display_state(),
            "body": MultilineText.display_state(
                lines=lines,
                min_height=3,
                flex=1,
            ),
            "bottom": BottomBar.display_state(items={
                "status": status,
            }),
        }
        self.refresh_screen()

    def on_key_stroke(self, event: KeyStroke):
        if event.key == Keys.ESC:
            self.application.pop_activity()
        self.refresh_screen()


# ============================================================================
# 5. Comment Thread  --  hierarchical tree with multi-line items
# ============================================================================

class Comment:
    def __init__(self, author: str, text: str, children: List["Comment"] = None):
        self.author = author
        self.text = text
        self.children = list(children or [])

def _walk_comments(root: Comment, depth=0) -> List[Tuple[Comment, int]]:
    out = [(root, depth)]
    for c in root.children:
        out.extend(_walk_comments(c, depth + 1))
    return out


class CommentThreadDemo(Activity):
    """
    A comment thread with nested replies rendered via ThreadList.
    Multi-line items show author + text.
    """

    def on_start(self):
        self.application.subscribe(KeyStroke, self, self.on_key_stroke)

        thread = Comment("alice", "Has anyone tried the new layout engine?", [
            Comment("bob", "Yes! The flex constraints are great.", [
                Comment("carol", "Agreed, min/max height is very useful."),
                Comment("alice", "What about the Percentage constraint?"),
            ]),
            Comment("dave", "I prefer the classic dict-based layout.", [
                Comment("elena", "Both work well for different cases."),
            ]),
            Comment("frank", "The accordion component is my favorite."),
        ])
        items = _walk_comments(thread)

        self.display_state = {
            "top": TopBar.display_state(items={
                "title": "Comment Thread",
                "help": "arrow keys: navigate  |  ESC: back"
            }),
            "thread": ThreadList.display_state(
                self.screen,
                items=items,
                item_renderer=self._render_comment,
                selected_index=0,
                focused=True,
                min_height=8,
                flex=1,
            ),
            "bottom": BottomBar.display_state(items={}),
        }
        self._update_status()
        self.refresh_screen()

    @staticmethod
    def _render_comment(comment: Comment, screen) -> List[str]:
        return [
            f"@{comment.author}:",
            f"  {comment.text}",
        ]

    def _update_status(self):
        tree = self.display_state["thread"]
        items = tree.get("items", [])
        sel = int(tree.get("selected_index", 0))
        n = len(items)
        # Map line index back to the comment item
        line_ids = tree.get("line_ids", [])
        if line_ids and 0 <= sel < len(line_ids):
            comment = line_ids[sel]
            self.display_state["bottom"]["items"]["author"] = f"@{comment.author}"
        self.display_state["bottom"]["items"]["position"] = f"{sel+1}/{n}"

    def on_key_stroke(self, event: KeyStroke):
        tree = self.display_state["thread"]
        n = len(tree.get("lines", tree.get("items", [])))
        sel = int(tree.get("selected_index", 0))

        if event.key == Keys.UP:
            sel = max(0, sel - 1)
        elif event.key == Keys.DOWN:
            sel = min(max(0, n - 1), sel + 1)
        elif event.key == Keys.ESC:
            self.application.pop_activity()
            return

        tree["selected_index"] = sel
        self._update_status()
        self.refresh_screen()


# ============================================================================
# 6. Accordion Showcase  --  collapsible sections
# ============================================================================

class AccordionShowcase(Activity):
    """
    Demonstrates the Accordion component with multiple collapsible sections.
    TAB to cycle focus, Enter/Space to toggle expand/collapse.
    """

    def on_start(self):
        self.application.subscribe(KeyStroke, self, self.on_key_stroke)

        # Section 1: Project info
        info_lines = [
            "  PyOS is a Python TUI framework built on blessed.",
            "  It provides Activity-based navigation, flex layout,",
            "  and composable UI components called printers.",
        ]

        # Section 2: Features list
        features_items = [
            "Activity stack navigation (push/pop/replace)",
            "Flex layout engine with min/max constraints",
            "Ratatui-style constraint layout (Length, %, Fill, Min)",
            "Event bus with subscribe/dispatch",
            "Serial + concurrent dispatch queues",
            "Tab focus cycling and input delegation",
            "Accordion, Table, ScrollList, TextInput, and more",
        ]

        # Section 3: Keyboard shortcuts
        shortcuts_lines = [
            "  ESC       Back / collapse menu",
            "  TAB       Cycle focus between sections",
            "  Enter     Toggle section / confirm action",
            "  Space     Open context menu / toggle",
            "  F1        Open log viewer",
            "  arrows    Navigate lists and tables",
        ]

        self.display_state = {
            "top": TopBar.display_state(items={
                "title": "Accordion Showcase",
                "help": "TAB: cycle  |  Enter: toggle  |  \u2190/\u2192: collapse/expand  |  ESC: back"
            }),
            "about": Accordion.display_state(
                title="About PyOS",
                child=MultilineText.display_state(lines=info_lines, flex=1, min_height=1),
                collapsed=False,
                flex=1,
                max_height=6,
            ),
            "features": Accordion.display_state(
                title="Features",
                child=ScrollList.display_state(
                    self.screen,
                    items=features_items,
                    selected_index=0,
                    focused=False,
                    input_handler=handle_scroll_list_input,
                    min_height=3,
                    flex=1,
                ),
                collapsed=False,
                flex=2,
                max_height=12,
            ),
            "shortcuts": Accordion.display_state(
                title="Keyboard Shortcuts",
                child=MultilineText.display_state(lines=shortcuts_lines, flex=1, min_height=1),
                collapsed=True,
                flex=1,
                max_height=10,
            ),
            "empty": Accordion.display_state(
                title="Empty Section",
                child=MultilineText.display_state(lines=[], flex=1, min_height=0),
                disabled=True,
            ),
            "bottom": BottomBar.display_state(items={
                "hint": "TAB to cycle, Enter/Space to toggle, \u2190/\u2192 to collapse/expand"
            }),
        }
        self.tab_order = ["about", "features", "shortcuts"]
        self._set_focus("about")
        self.refresh_screen()

    def on_key_stroke(self, event: KeyStroke):
        if event.key == Keys.TAB:
            self.cycle_focus()
            self._update_status()
            self.refresh_screen()
            return

        if event.key in (Keys.ENTER, ord(' ')):
            if self.focus in self.display_state:
                ctx = self.display_state[self.focus]
                if ctx.get("disabled"):
                    pass
                else:
                    new_collapsed = not ctx.get("collapsed", False)
                    Accordion.set_state(ctx, collapsed=new_collapsed,
                                        max_height=ctx["layout"].get("max_height"))
            self.refresh_screen()
            return

        # LEFT collapses, RIGHT expands the focused accordion
        if event.key == Keys.LEFT:
            if self.focus in self.display_state:
                ctx = self.display_state[self.focus]
                if not ctx.get("disabled") and not ctx.get("collapsed", False):
                    Accordion.set_state(ctx, collapsed=True,
                                        max_height=ctx["layout"].get("max_height"))
            self.refresh_screen()
            return

        if event.key == Keys.RIGHT:
            if self.focus in self.display_state:
                ctx = self.display_state[self.focus]
                if not ctx.get("disabled") and ctx.get("collapsed", False):
                    Accordion.set_state(ctx, collapsed=False,
                                        max_height=ctx["layout"].get("max_height"))
            self.refresh_screen()
            return

        if event.key == Keys.ESC:
            self.application.pop_activity()
            return

        # Delegate up/down keys to focused accordion's child if it's a scroll list
        if self.focus in self.display_state:
            ctx = self.display_state[self.focus]
            child = ctx.get("child", {})
            handler = child.get("input_handler")
            if handler and not ctx.get("collapsed", False):
                handler(self.focus + ".child", child, event, self.event_queue)

        self.refresh_screen()

    def _update_status(self):
        focused_name = self.focus or ""
        self.display_state["bottom"]["items"]["focused"] = focused_name


# ============================================================================
# 7. Layout Lab  --  interactive layout presets
# ============================================================================

class LayoutShowcase(Activity):
    """
    Layout Lab: pick a preset with 1-7 or select and press Enter.
    """

    def on_start(self):
        self.application.subscribe(KeyStroke, self, self.on_key_stroke)

        presets = [
            "1 Fixed bars only",
            "2 50/50 flex split",
            "3 70/30 flex split",
            "4 Min clamp (A=5, B=1)",
            "5 Max clamp (A<=3, B<=3)",
            "6 Collapse B",
            "7 Restore defaults",
        ]

        self.display_state = {
            "top": TopBar.display_state(items={
                "title": "Layout Lab",
                "help": "1-7 apply  |  Enter apply  |  arrows select  |  ESC back"
            }),
            "controls": ScrollList.display_state(
                self.screen,
                items=presets,
                selected_index=0,
                focused=True,
                input_handler=handle_scroll_list_input,
                min_height=4,
                flex=0,
                max_height=4
            ),
            "hr1": HorizontalBar.display_state(),
            "panel_a": MultilineText.display_state(lines=self._panel_fill("A"), min_height=3, flex=2),
            "panel_b": MultilineText.display_state(lines=self._panel_fill("B"), min_height=3, flex=1),
            "hr2": HorizontalBar.display_state(),
            "explain": MultilineText.display_state(lines=self._explain("7 Restore defaults"), min_height=2, flex=0, max_height=3),
            "bottom": BottomBar.display_state(items={"status": "A flex=2 m=3 | B flex=1 m=3"}),
        }
        self._update_inspector()
        self.refresh_screen()

    def _panel_fill(self, name):
        line = (name * 60)[:60]
        return [f"Panel {name}"] + [line for _ in range(80)]

    def _explain(self, label):
        m = {
            "1 Fixed bars only": [
                "Top/Bottom consume all fixed space.",
                "Panels are height=0 so the middle collapses."
            ],
            "2 50/50 flex split": [
                "Panels A and B share remaining space equally.",
                "Min heights are small to keep the 1:1 obvious."
            ],
            "3 70/30 flex split": [
                "A gets more with flex=7 vs B's flex=3.",
                "Relative flex controls leftover space."
            ],
            "4 Min clamp (A=5, B=1)": [
                "A never shrinks below 5 lines.",
                "B can shrink to 1 line if space is tight.",
                "Chrome reduced so clamp is unmistakable."
            ],
            "5 Max clamp (A<=3, B<=3)": [
                "Both panels cap at 3 lines even with extra space.",
                "Extra space stays unused."
            ],
            "6 Collapse B": [
                "B has height=0 and disappears.",
                "A expands to fill the area."
            ],
            "7 Restore defaults": [
                "A flex=2 min=3, B flex=1 min=3.",
                "A gets twice the leftover height of B."
            ],
        }
        return m.get(label, m["7 Restore defaults"])

    def _apply(self, label):
        a = self.display_state["panel_a"]
        b = self.display_state["panel_b"]
        controls = self.display_state["controls"]
        explain = self.display_state["explain"]
        hr2 = self.display_state["hr2"]

        controls["layout"] = {"min_height": 4, "max_height": 4}
        explain["layout"] = {"min_height": 2, "max_height": 3}
        hr2["layout"] = {"height": 1}

        if label.startswith("1"):
            a["layout"] = {"height": 0}
            b["layout"] = {"height": 0}
            self.display_state["bottom"]["items"]["status"] = "Fixed bars only"
        elif label.startswith("2"):
            a["layout"] = {"flex": 1, "min_height": 1}
            b["layout"] = {"flex": 1, "min_height": 1}
            self.display_state["bottom"]["items"]["status"] = "A flex=1 m=1 | B flex=1 m=1"
        elif label.startswith("3"):
            a["layout"] = {"flex": 7, "min_height": 1}
            b["layout"] = {"flex": 3, "min_height": 1}
            self.display_state["bottom"]["items"]["status"] = "A flex=7 m=1 | B flex=3 m=1"
        elif label.startswith("4"):
            a["layout"] = {"flex": 1, "min_height": 5}
            b["layout"] = {"flex": 1, "min_height": 1}
            controls["layout"] = {"height": 1}
            explain["layout"] = {"min_height": 1, "max_height": 2}
            hr2["layout"] = {"height": 0}
            self.display_state["bottom"]["items"]["status"] = "Clamp: A m=5 | B m=1"
        elif label.startswith("5"):
            a["layout"] = {"flex": 1, "min_height": 1, "max_height": 3}
            b["layout"] = {"flex": 1, "min_height": 1, "max_height": 3}
            self.display_state["bottom"]["items"]["status"] = "Cap: A M=3 | B M=3"
        elif label.startswith("6"):
            a["layout"] = {"flex": 1, "min_height": 1}
            b["layout"] = {"height": 0}
            self.display_state["bottom"]["items"]["status"] = "Collapse B"
        else:
            a["layout"] = {"flex": 2, "min_height": 3}
            b["layout"] = {"flex": 1, "min_height": 3}
            self.display_state["bottom"]["items"]["status"] = "Defaults: A flex=2 m=3 | B flex=1 m=3"

        self.display_state["explain"]["lines"] = self._explain(label)
        self._update_inspector()
        self.refresh_screen()

    def _update_inspector(self):
        alloc = self._measure_allocations()
        a_h = int(alloc.get("panel_a", 0))
        b_h = int(alloc.get("panel_b", 0))
        self.display_state["bottom"]["items"]["heights"] = f"A={a_h} lines | B={b_h} lines"

    def _measure_allocations(self):
        from pyos.Activity import solve_layout
        num_rows, _ = self.application.curses_screen.getmaxyx()

        measured = {}
        for key, ctx in self.display_state.items():
            layout = dict(ctx.get("layout", {}))
            is_fixed = "height" in layout
            flex_value = float(layout.get("flex", 0) or 0)
            is_flex = flex_value > 0
            if not is_fixed and not is_flex:
                min_h = int(layout.get("min_height", 0) or 0)
                max_h_raw = layout.get("max_height", None)
                try:
                    printers = ctx["line_generator"](ctx, 1_000_000)
                    natural = len(list(printers))
                except Exception:
                    natural = min_h
                if max_h_raw is None:
                    natural_h = max(min_h, natural)
                else:
                    mx = int(max_h_raw)
                    if mx < 0:
                        mx = 0
                    natural_h = max(min_h, min(mx, natural))
                ml = layout.copy()
                ml["height"] = int(max(0, natural_h))
                ml.pop("flex", None)
                measured[key] = {"layout": ml}
            else:
                measured[key] = {"layout": layout}

        heights = solve_layout(measured, num_rows)
        return heights

    def on_key_stroke(self, event: KeyStroke):
        ctrl = self.display_state["controls"]
        handle_scroll_list_input("controls", ctrl, event, self.event_queue)

        if event.key in (ord("1"), ord("2"), ord("3"), ord("4"), ord("5"), ord("6"), ord("7")):
            idx = int(chr(event.key)) - 1
            items = list(ctrl.get("items", []))
            if 0 <= idx < len(items):
                self._apply(items[idx])
            return

        if event.key in (Keys.ENTER, 343, 13):
            items = list(ctrl.get("items", []))
            idx = int(ctrl.get("selected_index", 0))
            if 0 <= idx < len(items):
                self._apply(items[idx])
            return

        if event.key == Keys.ESC:
            self.application.pop_activity()
            return

        self._update_inspector()
        self.refresh_screen()


# ============================================================================
# 8. Constraint Layout  --  Ratatui-style constraint engine
# ============================================================================

class ConstraintLayoutDemo(Activity):
    """
    Demonstrates the Ratatui-style constraint layout engine.

    Each region's "layout" is a typed Constraint object rather than a plain dict:

      TopBar       Length(2)       -- always exactly 2 lines
      HorizontalBar Length(1)     -- always exactly 1 line
      info panel   Percentage(25) -- 25% of total screen height
      main panel   Fill(2)        -- 2/3 of remaining space
      side panel   Fill(1)        -- 1/3 of remaining space
      guarded      Min(3)         -- at least 3 lines, expands proportionally
      BottomBar    Length(2)       -- always exactly 2 lines
    """

    def on_start(self):
        self.application.subscribe(KeyStroke, self, self.on_key_stroke)

        info_lines = [
            "Constraint layout engine (Ratatui-style)",
            "Resize the terminal to see how each constraint behaves.",
            "  Length(n)      -- exact n lines, highest priority.",
            "  Percentage(p)  -- p% of total screen height.",
            "  Fill(w)        -- proportional share of remaining space.",
            "  Min(n)         -- at least n lines, grows like Fill(1).",
        ]
        main_lines = ["Fill(2) -- takes 2/3 of remaining space."] + [
            f"  row {i:03d}" for i in range(1, 60)
        ]
        side_lines = ["Fill(1) -- takes 1/3 of remaining space."] + [
            f"  row {i:03d}" for i in range(1, 60)
        ]
        min_lines = [
            "Min(3) -- guaranteed at least 3 lines.",
            "  Grows proportionally if space allows.",
            "  Useful for status areas that must never collapse.",
        ]

        top = TopBar.display_state(items={"title": "Constraint Layout", "help": "ESC back"})
        top["layout"] = Length(2)

        hr = HorizontalBar.display_state()
        hr["layout"] = Length(1)

        info = MultilineText.display_state(lines=info_lines)
        info["layout"] = Percentage(25)

        main = MultilineText.display_state(lines=main_lines)
        main["layout"] = Fill(2)

        side = MultilineText.display_state(lines=side_lines)
        side["layout"] = Fill(1)

        guarded = MultilineText.display_state(lines=min_lines)
        guarded["layout"] = Min(3)

        bot = BottomBar.display_state(items={"note": "Length | Percentage | Fill | Min"})
        bot["layout"] = Length(2)

        self.display_state = {
            "top": top,
            "hr": hr,
            "info": info,
            "main": main,
            "side": side,
            "guarded": guarded,
            "bottom": bot,
        }
        self.refresh_screen()

    def on_key_stroke(self, event: KeyStroke):
        if event.key == Keys.ESC:
            self.application.pop_activity()
        self.refresh_screen()


# ============================================================================
# 9. Color Showcase  --  named colors, fg/bg helpers, decorated combos
# ============================================================================

def _print_styled_line(text, attr, screen, y):
    screen.addstr(y, 0, text, attr)


def _make_color_lines(context, remaining_height):
    printers = []
    for text, attr in context["styled_lines"]:
        printers.append(partial(_print_styled_line, text, attr))
    return printers


class ColorShowcase(Activity):
    """Demonstrates the pyos color system: named colors, fg/bg helpers, combinations."""

    SWATCHES = [
        ("Named Colors", BOLD),
        ("  Red", RED),
        ("  Green", GREEN),
        ("  Blue", BLUE),
        ("  Yellow", YELLOW),
        ("  Magenta", MAGENTA),
        ("  Cyan", CYAN),
        ("  White", WHITE),
        ("", NORMAL),
        ("Decorated Colors", BOLD),
        ("  Bold Red", BOLD | RED),
        ("  Dim Green", DIM | GREEN),
        ("  Underline Blue", UNDERLINE | BLUE),
        ("  Reverse Yellow", REVERSE | YELLOW),
        ("", NORMAL),
        ("Background Colors", BOLD),
        ("  On Red", bg("red")),
        ("  On Blue", bg("blue")),
        ("", NORMAL),
        ("Combined Styles", BOLD),
        ("  White on Red", WHITE | bg("red")),
        ("  Bold Cyan on Blue", BOLD | CYAN | bg("blue")),
    ]

    def on_start(self):
        self.application.subscribe(KeyStroke, self, self.on_key_stroke)

        self.display_state = {
            "top": TopBar.display_state(items={
                "title": "Color Showcase",
                "help": "ESC: back",
            }),
            "swatches": {
                "styled_lines": list(self.SWATCHES),
                "layout": {"flex": 1, "min_height": 10},
                "line_generator": _make_color_lines,
            },
            "bottom": BottomBar.display_state(items={
                "hint": "8 named colors + fg/bg helpers",
            }),
        }
        self.refresh_screen()

    def on_key_stroke(self, event: KeyStroke):
        if event.key == Keys.ESC:
            self.application.pop_activity()
        self.refresh_screen()


# ============================================================================
# 10. Web Server  --  Service lifecycle demo
# ============================================================================

class WebServerActivity(Activity):
    """
    Interactive control panel for the WebServerService.
    Start, stop, restart the server and send test requests.
    """

    ACTIONS = ["Start Server", "Stop Server", "Restart Server", "Send Test Request"]

    def __init__(self):
        super().__init__()
        self.server = None
        self.log_lines = ["(actions will appear here)"]

    def _status_lines(self):
        state = self.server.state.value.upper()
        lines = [f"  State:    {state}"]
        if self.server.is_running:
            lines.append(f"  URL:      {self.server.url}")
            lines.append(f"  Port:     {self.server.port}")
        lines.append(f"  Requests: {self.server.request_count}")
        return lines

    def _log(self, msg):
        self.log_lines.append(f"  {msg}")
        if len(self.log_lines) > 50:
            self.log_lines = self.log_lines[-50:]

    def _update_display(self):
        self.display_state["status"]["lines"] = self._status_lines()
        self.display_state["bottom"]["items"]["state"] = self.server.state.value.upper()
        self.display_state["log"]["lines"] = list(self.log_lines)
        self.refresh_screen()

    def on_start(self):
        self.server = self.application.service("webserver")
        self.application.subscribe(KeyStroke, self, self.on_key_stroke)
        self.application.subscribe(ScrollChange, self, self.on_scroll_change)

        self.display_state = {
            "top": TopBar.display_state(items={
                "title": "Web Server",
                "help": "Enter: action  |  ESC: back"
            }),
            "status": MultilineText.display_state(
                lines=self._status_lines(),
                min_height=4,
                max_height=5,
            ),
            "hr1": HorizontalBar.display_state(),
            "actions": ScrollList.display_state(
                self.screen,
                items=list(self.ACTIONS),
                selected_index=0,
                focused=True,
                input_handler=handle_scroll_list_input,
                min_height=4,
                max_height=6,
            ),
            "hr2": HorizontalBar.display_state(),
            "log": MultilineText.display_state(
                lines=list(self.log_lines),
                min_height=3,
                flex=1,
            ),
            "bottom": BottomBar.display_state(items={
                "state": self.server.state.value.upper(),
            }),
        }
        self.refresh_screen()

    def on_scroll_change(self, _):
        self.refresh_screen()

    def _start_server(self):
        if self.server.state in (ServiceState.IDLE, ServiceState.STOPPED):
            self._log("Starting server...")
            self._update_display()
            self.application.start_service("webserver", on_started=self._on_service_changed)
        else:
            self._log(f"Cannot start (state={self.server.state.value})")
            self._update_display()

    def _stop_server(self):
        if self.server.state == ServiceState.RUNNING:
            self._log("Stopping server...")
            self._update_display()
            self.application.stop_service("webserver", on_stopped=self._on_service_changed)
        else:
            self._log(f"Cannot stop (state={self.server.state.value})")
            self._update_display()

    def _restart_server(self):
        if self.server.state in (ServiceState.RUNNING, ServiceState.IDLE, ServiceState.STOPPED):
            self._log("Restarting server...")
            self._update_display()
            self.application.restart_service("webserver", on_started=self._on_service_changed)
        else:
            self._log(f"Cannot restart (state={self.server.state.value})")
            self._update_display()

    def _on_service_changed(self):
        self._log(f"Server now {self.server.state.value}")
        self._update_display()

    def _send_request(self):
        if not self.server.is_running:
            self._log("Server not running — start it first")
            self._update_display()
            return
        self._log("Sending request...")
        self._update_display()

        url = self.server.url
        def _fetch():
            try:
                resp = urllib.request.urlopen(url, timeout=5)
                data = resp.read().decode()
                self.main_thread.submit_async(self._on_fetch_result, data)
            except Exception as e:
                self.main_thread.submit_async(self._on_fetch_result, None, str(e))

        CentralDispatch.future(_fetch)

    def _on_fetch_result(self, data, error=None):
        self._log(f"Error: {error}" if error else f"Response: {data}")
        self._update_display()

    def on_key_stroke(self, event: KeyStroke):
        handle_scroll_list_input(
            "actions", self.display_state["actions"], event, self.event_queue
        )

        if event.key in (Keys.ENTER, 343, 13):
            idx = int(self.display_state["actions"].get("selected_index", 0))
            action = self.ACTIONS[idx]
            dispatch = {
                "Start Server": self._start_server,
                "Stop Server": self._stop_server,
                "Restart Server": self._restart_server,
                "Send Test Request": self._send_request,
            }
            dispatch[action]()
            return

        if event.key == Keys.ESC:
            self.application.pop_activity()
            return

        self.refresh_screen()


# ============================================================================
# 11. Reflex Game  --  KeyRelease timing game
# ============================================================================

class ReflexGame(Activity):
    """
    Hold SPACE to start a timer, release it as close to the target as you can.
    Demonstrates KeyRelease: press starts the clock, release stops it.
    """

    TARGET_TIME = 3.0  # seconds
    MAX_ROUNDS = 5

    def __init__(self):
        super().__init__()
        self._round = 0
        self._scores = []       # list of (target, actual, error) per round
        self._press_time = None  # monotonic timestamp of last SPACE press
        self._state = "idle"     # idle -> holding -> result -> (idle|done)
        self._timer = None
        self._elapsed = 0.0

    def on_start(self):
        self.application.subscribe(KeyStroke, self, self.on_key_stroke)
        self.application.subscribe(KeyRelease, self, self.on_key_release)
        self._build_display()
        self.refresh_screen()

    def on_stop(self):
        if self._timer:
            self._timer.cancel()
            self._timer = None

    def _build_display(self):
        self.display_state = {
            "top": TopBar.display_state(items={
                "title": "Reflex Game",
                "help": "ESC: back",
            }),
            "content": {
                "lines": self._content_lines(),
                "layout": {"flex": 1, "min_height": 10},
                "line_generator": _make_reflex_lines,
            },
            "bottom": BottomBar.display_state(items={
                "hint": self._hint_text(),
            }),
        }

    def _hint_text(self):
        if self._state == "done":
            return "Game over!  ENTER: play again  |  ESC: back"
        if self._state == "result":
            return "Hold SPACE: next round  |  ESC: back"
        if self._state == "holding":
            return "Release SPACE as close to the target as you can!"
        return "Hold SPACE to start the timer"

    def _content_lines(self):
        lines = []
        lines.append(("", NORMAL))
        if self._state == "done":
            lines.append(("  GAME OVER", BOLD))
            lines.append(("", NORMAL))
            for i, (target, actual, error) in enumerate(self._scores):
                marker = "***" if error < 0.1 else "**" if error < 0.25 else "*" if error < 0.5 else ""
                lines.append((
                    f"  Round {i+1}:  target {target:.1f}s  |  you: {actual:.3f}s  |  off by {error:.3f}s  {marker}",
                    GREEN if error < 0.1 else YELLOW if error < 0.25 else RED if error > 0.5 else NORMAL,
                ))
            lines.append(("", NORMAL))
            avg = sum(e for _, _, e in self._scores) / len(self._scores) if self._scores else 0
            lines.append((f"  Average error: {avg:.3f}s", BOLD))
            if avg < 0.1:
                lines.append(("  Rating: INCREDIBLE reflexes!", BOLD | GREEN))
            elif avg < 0.25:
                lines.append(("  Rating: Great job!", GREEN))
            elif avg < 0.5:
                lines.append(("  Rating: Not bad!", YELLOW))
            else:
                lines.append(("  Rating: Keep practicing!", RED))
        elif self._state == "result":
            target, actual, error = self._scores[-1]
            lines.append((f"  Round {self._round}/{self.MAX_ROUNDS}", BOLD))
            lines.append(("", NORMAL))
            lines.append((f"  Target:   {target:.1f}s", NORMAL))
            lines.append((f"  You held: {actual:.3f}s", BOLD))
            lines.append(("", NORMAL))
            if error < 0.1:
                lines.append((f"  Off by {error:.3f}s  --  PERFECT!", BOLD | GREEN))
            elif error < 0.25:
                lines.append((f"  Off by {error:.3f}s  --  Great!", GREEN))
            elif error < 0.5:
                lines.append((f"  Off by {error:.3f}s  --  Decent", YELLOW))
            else:
                lines.append((f"  Off by {error:.3f}s  --  Oops!", RED))
        elif self._state == "holding":
            lines.append((f"  Round {self._round}/{self.MAX_ROUNDS}", BOLD))
            lines.append(("", NORMAL))
            lines.append((f"  Target: hold for exactly {self.TARGET_TIME:.1f} seconds", NORMAL))
            lines.append(("", NORMAL))
            lines.append(("  >>>>  HOLDING  <<<<", BOLD | YELLOW))
            lines.append(("", NORMAL))
            lines.append((f"  Elapsed: {self._elapsed:.1f}s ...", BOLD))
        else:
            # idle
            lines.append(("  Reflex Tester", BOLD))
            lines.append(("", NORMAL))
            lines.append((f"  Hold SPACE for exactly {self.TARGET_TIME:.1f} seconds, then release.", NORMAL))
            lines.append(("  The closer you are to the target, the better your score!", NORMAL))
            lines.append(("", NORMAL))
            if self._round == 0:
                lines.append((f"  {self.MAX_ROUNDS} rounds.  Press and hold SPACE to begin!", NORMAL))
            else:
                lines.append((f"  Round {self._round + 1}/{self.MAX_ROUNDS} coming up.  Hold SPACE!", NORMAL))
            lines.append(("", NORMAL))
            if self._scores:
                lines.append(("  Scores so far:", DIM))
                for i, (t, a, e) in enumerate(self._scores):
                    lines.append((f"    Round {i+1}: off by {e:.3f}s", DIM))
        return lines

    def _update_display(self):
        self.display_state["content"]["lines"] = self._content_lines()
        self.display_state["bottom"]["items"]["hint"] = self._hint_text()
        self.refresh_screen()

    def _tick(self):
        """Called periodically while holding to update the elapsed timer."""
        if self._state != "holding":
            return
        self._elapsed = time.monotonic() - self._press_time
        self.main_thread.submit_async(self._update_display)
        self._timer = CentralDispatch.timer(0.1, self._tick)
        self._timer.start()

    def on_key_stroke(self, event: KeyStroke):
        if event.key == Keys.ESC:
            if self._timer:
                self._timer.cancel()
                self._timer = None
            self.application.pop_activity()
            return

        if event.key == ord(" "):
            if self._state in ("idle", "result"):
                # Start the next round (from idle or after seeing a result)
                self._round += 1
                self._press_time = time.monotonic()
                self._elapsed = 0.0
                self._state = "holding"
                self._update_display()
                self._timer = CentralDispatch.timer(0.1, self._tick)
                self._timer.start()

        if event.key in (Keys.ENTER, 343, 13):
            if self._state == "done":
                # Reset for a new game
                self._round = 0
                self._scores = []
                self._state = "idle"
                self._update_display()

    def on_key_release(self, event: KeyRelease):
        if event.key == ord(" ") and self._state == "holding":
            if self._timer:
                self._timer.cancel()
                self._timer = None
            elapsed = time.monotonic() - self._press_time
            error = abs(elapsed - self.TARGET_TIME)
            self._scores.append((self.TARGET_TIME, elapsed, error))
            self._press_time = None
            if self._round >= self.MAX_ROUNDS:
                self._state = "done"
            else:
                self._state = "result"
            self._update_display()


def _make_reflex_lines(context, remaining_height):
    printers = []
    for text, attr in context["lines"]:
        printers.append(partial(_print_styled_line, text, attr))
    return printers


# ============================================================================
# 12. Paint  --  click/drag to draw, scroll to cycle colors
# ============================================================================

_PAINT_PALETTE = [
    ("black",        "black"),
    ("white",        "white"),
    ("red",          "red"),
    ("green",        "green"),
    ("blue",         "blue"),
    ("yellow",       "yellow"),
    ("magenta",      "magenta"),
    ("cyan",         "cyan"),
    ("bright_red",   "orange"),
    ("bright_green", "bright green"),
]


def _make_palette_line(context, remaining_height):
    selected = context.get("selected", 0)
    palette = context["palette"]

    def _print_palette_row(label_row, screen, y):
        x = 1
        for i, (color_name, _label) in enumerate(palette):
            style = bg(color_name)
            if label_row:
                text = f">{i}<" if i == selected else f" {i} "
                screen.addstr(y, x, text, BOLD | style if i == selected else style)
            else:
                screen.addstr(y, x, "    ", style)
            x += 4

    return [
        partial(_print_palette_row, False),
        partial(_print_palette_row, True),
        partial(_print_palette_row, False),
    ]


def _make_canvas_lines(context, remaining_height):
    canvas = context["canvas"]
    palette = context["palette"]

    def _make_row_printer(row_data):
        def _print_row(screen, y):
            _, num_cols = screen.getmaxyx()
            for col, color_idx in enumerate(row_data):
                if col >= num_cols:
                    break
                if color_idx:
                    screen.addstr(y, col, " ", bg(palette[color_idx][0]))
        return _print_row

    printers = []
    for row in canvas[:remaining_height]:
        printers.append(_make_row_printer(row))
    return printers


class PaintDemo(Activity):
    """
    A terminal pixel-painting canvas.
    Click/drag to paint, scroll to cycle colors, number keys to pick a color.
    """

    def __init__(self):
        super().__init__()
        self._color_index = 1  # start on white
        self._canvas = []      # populated in on_start once screen size is known

    def on_start(self):
        self.application.subscribe(KeyStroke, self, self.on_key_stroke)
        self.application.subscribe(MouseClick, self, self.on_mouse_click)
        self.application.subscribe(MouseDrag, self, self.on_mouse_drag)
        self.application.subscribe(MouseScroll, self, self.on_mouse_scroll)

        num_rows, num_cols = self.screen.getmaxyx()
        canvas_height = max(1, num_rows - 5)  # top bar + palette (3) + bottom bar
        self._canvas = [[0] * num_cols for _ in range(canvas_height)]

        self._build_display()
        self.refresh_screen()

    def _build_display(self):
        self.display_state = {
            "top": TopBar.display_state(items={
                "title": "Paint",
                "help": "Click: draw  Scroll: color  c: clear  ESC: back",
            }),
            "palette": {
                "palette": _PAINT_PALETTE,
                "selected": self._color_index,
                "layout": {"height": 3},
                "line_generator": _make_palette_line,
            },
            "canvas": {
                "canvas": self._canvas,
                "palette": _PAINT_PALETTE,
                "layout": {"flex": 1, "min_height": 1},
                "line_generator": _make_canvas_lines,
            },
            "bottom": BottomBar.display_state(items={
                "color": f"Color: {_PAINT_PALETTE[self._color_index][1]}",
            }),
        }

    def _paint(self, x, y, color_index):
        canvas_row = y - 4  # offset for top bar + palette (3 rows)
        if canvas_row < 0 or canvas_row >= len(self._canvas):
            return
        if x < 0 or x >= len(self._canvas[0]):
            return
        self._canvas[canvas_row][x] = color_index
        self.refresh_screen()

    def _select_color(self, index):
        self._color_index = index
        self.display_state["palette"]["selected"] = index
        self.display_state["bottom"]["items"]["color"] = (
            f"Color: {_PAINT_PALETTE[index][1]}"
        )
        self.refresh_screen()

    def _palette_hit(self, x, y):
        """Return palette index if (x, y) is on the palette strip, else None."""
        if not (1 <= y <= 3):
            return None
        # Each swatch is 4 chars wide starting at x=1
        idx = (x - 1) // 4
        if 0 <= idx < len(_PAINT_PALETTE) and x >= 1:
            return idx
        return None

    def on_mouse_click(self, event: MouseClick):
        if event.button == MouseButton.LEFT:
            hit = self._palette_hit(event.x, event.y)
            if hit is not None:
                self._select_color(hit)
                return
            self._paint(event.x, event.y, self._color_index)
        elif event.button == MouseButton.RIGHT:
            self._paint(event.x, event.y, 0)

    def on_mouse_drag(self, event: MouseDrag):
        if event.button == MouseButton.LEFT:
            self._paint(event.x, event.y, self._color_index)
        elif event.button == MouseButton.RIGHT:
            self._paint(event.x, event.y, 0)

    def on_mouse_scroll(self, event: MouseScroll):
        n = len(_PAINT_PALETTE)
        if event.direction == "up":
            self._select_color((self._color_index + 1) % n)
        else:
            self._select_color((self._color_index - 1) % n)

    def on_key_stroke(self, event: KeyStroke):
        key = event.key
        if key == Keys.ESC:
            self.application.pop_activity()
            return
        if key in (ord("c"), ord("C")):
            for row in self._canvas:
                for i in range(len(row)):
                    row[i] = 0
            self.refresh_screen()
            return
        if isinstance(key, int) and ord("0") <= key <= ord("9"):
            idx = key - ord("0")
            if idx < len(_PAINT_PALETTE):
                self._select_color(idx)


# ============================================================================
# DemoApplication — registers services at startup
# ============================================================================

class DemoApplication(Application):
    def on_start(self):
        self.register_service("webserver", WebServerService())


# ============================================================================
# Entry point
# ============================================================================

def _main():
    app = DemoApplication(enable_kitty=True, enable_mouse=True)
    app.start(DemoMenuActivity())

if __name__ == "__main__":
    _main()
