"""Runtime helpers for generated browser bundles."""
import asyncio
import io
import json
import sys
import js

browser_base_exception = BaseException

if sys.implementation.name == 'micropython':
    from jsffi import create_proxy, JsProxy
    import os

    # Browser state only: never read or serialize the build/server environment.
    if not hasattr(os, 'environ'):
        os.environ = {}

    def _browser_getenv(key, default=None):
        if not isinstance(key, str):
            raise TypeError('environment variable name must be a string')
        return os.environ.get(key, default)

    if not hasattr(os, 'getenv'):
        os.getenv = _browser_getenv

    def create_once_callable(callback):
        if not callable(callback):
            raise TypeError('create_once_callable requires a callable')
        return js.pytinctureCreateOnceCallable(create_proxy(callback))
else:
    from pyodide.ffi import create_proxy, create_once_callable, JsProxy


def run_js(*args):
    """Match pyodide.code.run_js; browser CSP still governs evaluation."""
    if len(args) != 1:
        raise TypeError("run_js requires one positional argument")
    code = args[0]
    if not isinstance(code, str):
        raise TypeError('run_js requires a string')
    result = js.pytinctureRunJavascript(code, False)
    if not result.ok:
        raise RuntimeError(str(result.error))
    return result.value


def widget_run_js(*args):
    if len(args) != 1:
        raise TypeError("run_js requires one positional argument")
    code = args[0]
    if not isinstance(code, str):
        raise TypeError('run_js requires a string')
    result = js.pytinctureRunJavascript(code, True)
    if not result.ok:
        raise RuntimeError(str(result.error))
    return result.value


def browser_monotonic():
    return js.performance.now() / 1000.0


def _expand_list(result, values):
    result.extend(values)
    return result


def _expand_set(result, values):
    if result is None:
        result = set()
    result.update(values)
    return result


def _display_tuple(values):
    return tuple(values)

def merge_dicts(*parts):
    result = {}
    for part in parts:
        result.update(part)
    return result


def to_python(value):
    if value is None or isinstance(value, (str, int, float, bool, list, dict)):
        return value
    return json.loads(js.JSON.stringify(value))


def can_to_python(value):
    return isinstance(value, JsProxy) or hasattr(value, 'to_py')


def to_js(value, **kwargs):
    if kwargs:
        raise TypeError('browser to_js supports JSON-compatible data without options')
    return js.JSON.parse(json.dumps(value))


def initialize_layout(cls):
    """Replace dhxpyt's metaclass, including nested and inherited layouts."""
    if not getattr(cls, '_pytincture_layout', False):
        return cls
    original = cls.__init__

    def initialize(self, *args, **kwargs):
        depth = getattr(self, '_pytincture_init_depth', 0)
        self._pytincture_init_depth = depth + 1
        try:
            original(self, *args, **kwargs)
        finally:
            self._pytincture_init_depth = depth
        if depth == 0:
            self.load_ui()

    cls.__init__ = initialize
    return cls


def format_exception(error):
    if hasattr(sys, 'print_exception'):
        output = io.StringIO()
        sys.print_exception(error, output)
        return output.getvalue()
    import traceback
    return ''.join(traceback.format_exception(type(error), error, error.__traceback__))


def spawn(coroutine):
    return asyncio.create_task(coroutine)


class _BrowserEventLoop:
    """Only task scheduling and running-state inspection are portable."""
    def is_running(self):
        return True

    def create_task(self, coroutine):
        return spawn(coroutine)


def browser_event_loop():
    return _BrowserEventLoop()


async def finish_startup():
    # Background timers, polling and event listeners must not hold startup open.
    # Apps that require data before readiness can await it in async main().
    await asyncio.sleep(0)


def uuid4():
    return js.crypto.randomUUID()


def string_title(value):
    if hasattr(value, 'title'):
        return value.title()
    if not isinstance(value, str):
        raise AttributeError('Object has no title method')
    result = []
    previous_cased = False
    for char in value:
        result.append(char.lower() if previous_cased else char.upper())
        previous_cased = char.lower() != char.upper()
    return ''.join(result)


def fstring_convert(value, conversion):
    if conversion == 115:  # !s
        return str(value)
    text = repr(value)  # !r or !a
    if conversion != 97:
        return text
    parts = []
    for character in text:
        point = ord(character)
        if point < 128:
            parts.append(character)
        elif point < 256:
            parts.append('\\x%02x' % point)
        elif point < 65536:
            parts.append('\\u%04x' % point)
        else:
            parts.append('\\U%08x' % point)
    return ''.join(parts)


def fstring_format(value, spec):
    # Special methods are resolved on the type, as with CPython FORMAT_VALUE.
    formatter = getattr(type(value), '__format__', None)
    if formatter is not None:
        result = formatter(value, spec)
    elif not spec:
        result = str(value)
    elif isinstance(value, (str, int, float, bool)):
        # MicroPython has native built-in formatting but no format() builtin.
        if isinstance(value, bool):
            value = int(value)
        result = ('{0:' + spec + '}').format(value)
    else:
        raise TypeError('unsupported format string passed to ' + type(value).__name__ + '.__format__')
    if not isinstance(result, str):
        raise TypeError('__format__ must return a str')
    return result
