import json
import uuid
from pathlib import Path
from IPython.display import HTML, display

from klotho.utils.playback.tonejs.cdn import (
    cdn_scripts, INSTRUMENTS_JS_PATH, PLAYER_JS_PATH,
)


def _convert_numpy_types(obj):
    """
    Recursively convert NumPy scalar types to native Python types.

    Parameters
    ----------
    obj : object
        Value or nested structure (dict, list) potentially containing
        NumPy numeric scalars.

    Returns
    -------
    object
        The same structure with NumPy scalars replaced by ``int``
        or ``float``.
    """
    try:
        import numpy as np
        if isinstance(obj, dict):
            return {k: _convert_numpy_types(v) for k, v in obj.items()}
        elif isinstance(obj, list):
            return [_convert_numpy_types(v) for v in obj]
        elif isinstance(obj, (np.floating, np.float64, np.float32)):
            return float(obj)
        elif isinstance(obj, (np.integer, np.int64, np.int32)):
            return int(obj)
        else:
            return obj
    except ImportError:
        return obj


class ToneEngine:
    """
    Jupyter widget that renders Tone.js-based audio playback controls.

    Generates an HTML/JS snippet with play/stop and loop buttons,
    wiring the provided event list through Tone.js instruments.

    Parameters
    ----------
    events : list of dict
        Audio event payload (typically from :func:`convert_to_events`).
    custom_js_path : str or Path, optional
        Path to a custom JavaScript file loaded before the player.
    custom_js : str, optional
        Inline JavaScript source embedded before the player.
    """

    def __init__(self, events, custom_js_path=None, custom_js=None):
        self.events = _convert_numpy_types(events)
        self.custom_js_path = Path(custom_js_path) if custom_js_path else None
        self.custom_js = custom_js
        self.widget_id = f"klotho_{uuid.uuid4().hex[:8]}"

    def _load_instruments_js(self):
        """Return the bundled instruments JavaScript source."""
        return INSTRUMENTS_JS_PATH.read_text()

    def _load_player_js(self):
        """Return the bundled player JavaScript source."""
        return PLAYER_JS_PATH.read_text()

    def _load_user_custom_js(self):
        """
        Return user-supplied custom JavaScript, if any.

        Returns
        -------
        str
            Custom JS source, or an empty string when none is provided.
        """
        if self.custom_js_path and self.custom_js_path.exists():
            return self.custom_js_path.read_text()
        if self.custom_js:
            return self.custom_js
        return ""

    def _generate_html(self):
        """
        Build the full HTML string for the playback widget.

        Returns
        -------
        str
            HTML containing inline styles, CDN scripts, instrument
            definitions, and the player logic.
        """
        payload_json = json.dumps(self.events)
        user_custom_js = self._load_user_custom_js()
        instruments_js = self._load_instruments_js()
        player_js = self._load_player_js()
        scripts = "\n".join([s for s in (user_custom_js, instruments_js, player_js) if s])

        tone_script = cdn_scripts(include_tone=True)

        html = f'''
<div id="{self.widget_id}" style="
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 10px;
    background: #1a1a2e;
    border-radius: 6px;
    user-select: none;
">
    <button id="{self.widget_id}_toggle" style="
        width: 32px;
        height: 32px;
        border: none;
        border-radius: 4px;
        background: #16213e;
        cursor: pointer;
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 0;
    ">
        <span id="{self.widget_id}_icon" style="
            width: 0;
            height: 0;
            border-top: 7px solid transparent;
            border-bottom: 7px solid transparent;
            border-left: 12px solid #4ade80;
            margin-left: 3px;
        "></span>
    </button>
    <button id="{self.widget_id}_loop" style="
        width: 28px;
        height: 28px;
        border: none;
        border-radius: 4px;
        background: #16213e;
        cursor: pointer;
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 0;
        opacity: 0.5;
    ">
        <svg id="{self.widget_id}_loop_svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#a0a0a0" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
            <path d="M17 2l4 4-4 4"></path>
            <path d="M3 11v-1a4 4 0 0 1 4-4h14"></path>
            <path d="M7 22l-4-4 4-4"></path>
            <path d="M21 13v1a4 4 0 0 1-4 4H3"></path>
        </svg>
    </button>
</div>

{tone_script}
<script>
{scripts}
</script>
<script>
(() => {{
    const toggleBtn = document.getElementById("{self.widget_id}_toggle");
    const iconEl = document.getElementById("{self.widget_id}_icon");
    const loopBtn = document.getElementById("{self.widget_id}_loop");
    const loopSvg = document.getElementById("{self.widget_id}_loop_svg");
    const payload = {payload_json};
    const events = payload.events || [];
    const instruments = globalThis.KLOTHO_BUILD_INSTRUMENTS(payload.instruments || {{}});
    const player = KlothoPlayer.create();

    let looping = false;

    function setPlayIcon() {{
        iconEl.style.width = "0";
        iconEl.style.height = "0";
        iconEl.style.borderTop = "7px solid transparent";
        iconEl.style.borderBottom = "7px solid transparent";
        iconEl.style.borderLeft = "12px solid #4ade80";
        iconEl.style.borderRight = "none";
        iconEl.style.marginLeft = "3px";
        iconEl.style.background = "none";
    }}

    function setStopIcon() {{
        iconEl.style.width = "12px";
        iconEl.style.height = "12px";
        iconEl.style.border = "none";
        iconEl.style.borderRadius = "2px";
        iconEl.style.marginLeft = "0";
        iconEl.style.background = "#ef4444";
    }}

    loopBtn.onclick = () => {{
        looping = !looping;
        loopBtn.style.opacity = looping ? "1" : "0.5";
        loopSvg.setAttribute("stroke", looping ? "#4ade80" : "#a0a0a0");
    }};

    toggleBtn.onclick = async () => {{
        if (player.isPlaying()) {{
            player.stop();
            setPlayIcon();
        }} else {{
            setStopIcon();
            await player.play(events, instruments, {{
                loop: looping,
                onFinish: () => setPlayIcon()
            }});
        }}
    }};
}})();
</script>
'''
        return html

    def display(self):
        """
        Render the playback widget in the active Jupyter notebook.

        Returns
        -------
        IPython.display.DisplayHandle
            The display handle for the rendered HTML widget.
        """
        html = self._generate_html()
        return display(HTML(html))

    def _repr_html_(self):
        """Return the widget HTML for automatic notebook rendering."""
        return self._generate_html()
