"""
Callback Registry for managing interactable elements and their callbacks.

This module provides a registry system for tracking interactive elements (links, buttons, forms)
and managing their callbacks. Supports multiple binding strategies:
- HTML id attributes for HTML-generated content
- Auto-generated ids for programmatic construction
- Type-based batch operations
"""

from typing import Dict, List, Optional, Callable
from pyWebLayout.core.base import Interactable


class CallbackRegistry:
    """
    Registry for managing interactable callbacks with multiple binding strategies.

    Supports:
    - Direct references by object id
    - HTML id attributes (from parsed HTML)
    - Type-based queries (all buttons, all links, etc.)
    - Auto-generated ids for programmatic construction

    This enables flexible callback binding for both HTML-generated content
    and manually constructed UIs.
    """

    def __init__(self):
        """Initialize an empty callback registry."""
        self._by_reference: Dict[int, Interactable] = {}  # id(obj) -> obj
        self._by_id: Dict[str, Interactable] = {}          # HTML id or auto id -> obj
        self._by_type: Dict[str, List[Interactable]] = {}  # type name -> [objs]
        self._auto_counter: int = 0

    def register(self, obj: Interactable, html_id: Optional[str] = None) -> str:
        """
        Register an interactable object with optional HTML id.

        The object is always registered by reference (using Python's id()).
        If an html_id is provided, it's also registered by that id.
        If no html_id is provided, an auto-generated id is created.

        Args:
            obj: The interactable object to register
            html_id: Optional HTML id attribute value (e.g., from <button id="save-btn">)

        Returns:
            The id used for registration (either html_id or auto-generated)

        Example:
            >>> button = ButtonText(...)
            >>> registry.register(button, html_id="save-btn")
            'save-btn'
            >>> registry.register(other_button)  # No html_id
            'auto_button_0'
        """
        # Always register by Python object id for direct lookups
        obj_id = id(obj)
        self._by_reference[obj_id] = obj

        # Determine type name and register by type
        type_name = self._get_type_name(obj)
        if type_name not in self._by_type:
            self._by_type[type_name] = []
        self._by_type[type_name].append(obj)

        # Register by HTML id or generate auto id
        if html_id:
            # Use provided HTML id
            self._by_id[html_id] = obj
            return html_id
        else:
            # Generate automatic id
            auto_id = f"auto_{type_name}_{self._auto_counter}"
            self._auto_counter += 1
            self._by_id[auto_id] = obj
            return auto_id

    def get_by_id(self, identifier: str) -> Optional[Interactable]:
        """
        Get an interactable by its id (HTML id or auto-generated id).

        Args:
            identifier: The id to lookup (e.g., "save-btn" or "auto_button_0")

        Returns:
            The interactable object, or None if not found

        Example:
            >>> button = registry.get_by_id("save-btn")
            >>> if button:
            ...     button._callback = my_save_function
        """
        return self._by_id.get(identifier)

    def get_by_type(self, type_name: str) -> List[Interactable]:
        """
        Get all interactables of a specific type.

        Args:
            type_name: The type name (e.g., "link", "button", "form_field")

        Returns:
            List of interactable objects of that type (may be empty)

        Example:
            >>> all_buttons = registry.get_by_type("button")
            >>> for button in all_buttons:
            ...     print(button.text)
        """
        return self._by_type.get(type_name, []).copy()

    def get_all_ids(self) -> List[str]:
        """
        Get all registered ids (both HTML ids and auto-generated ids).

        Returns:
            List of all ids in the registry

        Example:
            >>> ids = registry.get_all_ids()
            >>> print(ids)
            ['save-btn', 'cancel-btn', 'auto_link_0', 'auto_button_1']
        """
        return list(self._by_id.keys())

    def get_all_types(self) -> List[str]:
        """
        Get all registered type names.

        Returns:
            List of type names that have registered objects

        Example:
            >>> types = registry.get_all_types()
            >>> print(types)
            ['link', 'button', 'form_field']
        """
        return list(self._by_type.keys())

    def set_callback(self, identifier: str, callback: Callable) -> bool:
        """
        Set the callback for an interactable by its id.

        Args:
            identifier: The id of the interactable
            callback: The callback function to set

        Returns:
            True if the interactable was found and callback set, False otherwise

        Example:
            >>> def on_save(point):
            ...     print("Save clicked!")
            >>> registry.set_callback("save-btn", on_save)
            True
        """
        obj = self.get_by_id(identifier)
        if obj:
            obj._callback = callback
            return True
        return False

    def set_callbacks_by_type(self, type_name: str, callback: Callable) -> int:
        """
        Set the callback for all interactables of a specific type.

        Useful for batch operations like setting a default click sound
        for all buttons, or a default link handler for all links.

        Args:
            type_name: The type name (e.g., "button", "link")
            callback: The callback function to set

        Returns:
            Number of objects that had their callback set

        Example:
            >>> def play_click_sound(point):
            ...     audio.play("click.wav")
            >>> count = registry.set_callbacks_by_type("button", play_click_sound)
            >>> print(f"Set callback for {count} buttons")
        """
        objects = self.get_by_type(type_name)
        for obj in objects:
            obj._callback = callback
        return len(objects)

    def unregister(self, identifier: str) -> bool:
        """
        Unregister an interactable by its id.

        Args:
            identifier: The id of the interactable to unregister

        Returns:
            True if the interactable was found and unregistered, False otherwise
        """
        obj = self._by_id.pop(identifier, None)
        if obj:
            # Remove from reference map
            self._by_reference.pop(id(obj), None)

            # Remove from type map
            type_name = self._get_type_name(obj)
            if type_name in self._by_type:
                try:
                    self._by_type[type_name].remove(obj)
                except ValueError:
                    pass

            return True
        return False

    def clear(self):
        """Clear all registered interactables."""
        self._by_reference.clear()
        self._by_id.clear()
        self._by_type.clear()
        self._auto_counter = 0

    def count(self) -> int:
        """
        Get the total number of registered interactables.

        Returns:
            Total count of registered objects
        """
        return len(self._by_id)

    def count_by_type(self, type_name: str) -> int:
        """
        Get the count of interactables of a specific type.

        Args:
            type_name: The type name to count

        Returns:
            Number of objects of that type
        """
        return len(self._by_type.get(type_name, []))

    def _get_type_name(self, obj: Interactable) -> str:
        """
        Get a normalized type name for an interactable object.

        Args:
            obj: The interactable object

        Returns:
            Type name string (e.g., "link", "button", "form_field")
        """
        # Import here to avoid circular imports
        from pyWebLayout.concrete.functional import LinkText, ButtonText, FormFieldText

        if isinstance(obj, LinkText):
            return "link"
        elif isinstance(obj, ButtonText):
            return "button"
        elif isinstance(obj, FormFieldText):
            return "form_field"
        else:
            # Fallback to class name
            return obj.__class__.__name__.lower()

    def __len__(self) -> int:
        """Support len() to get count of registered interactables."""
        return self.count()

    def __contains__(self, identifier: str) -> bool:
        """Support 'in' operator to check if an id is registered."""
        return identifier in self._by_id

    def __repr__(self) -> str:
        """String representation showing registry statistics."""
        type_counts = {t: len(objs) for t, objs in self._by_type.items()}
        return f"CallbackRegistry(total={self.count()}, types={type_counts})"
