import webview
import os
import threading
import re
import importlib
import json
import traceback
import sys
def escape_js_string(string:str)->str:
    """Escape string for JavaScript"""
    return string.replace("\\", "\\\\").replace("\"", "\\\"").replace("\'","\\'").replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t").replace("\f", "\\f").replace("\v", "\\v")
class PositronContext:
    def __init__(self, window):
        self.window = window
        self.globals = {}
        self.locals = {}
        self.exposed_functions = {}
        self.event_handlers = {}
    def execute(self, code):
        try:
            self.locals.update({
                "window": self.window,
                "document":Document(self.window),
                "exposed": ExposedFunctions(self.exposed_functions),
                "import_module": importlib.import_module,
            })
            exec(code, self.globals, self.locals)
            self.globals.update(self.locals) #...
            return True, None
        except Exception as e:
            error_info = traceback.format_exc()
            return False, error_info
    def register_event_handler(self, element_id, event_type, callback):
        
        key = f"{element_id}_{event_type}"
        callback_globals = dict(self.globals)  
        callback_locals = dict(self.locals)    
        callback_func = callback
        exposed_functions = self.exposed_functions
        
  
        def wrapped_callback():
            try:
                exec_globals = dict(callback_globals)
                exec_locals = dict(callback_locals)
                important_stuff={
                    "window": self.window,
                    "document": Document(self.window),
                    "exposed": ExposedFunctions(exposed_functions),
                    "callback_func": callback_func
                }
                exec_globals.update(important_stuff)
                exec_locals.update(important_stuff)
                exec_code = "def _executor():\n    return callback_func()"
                exec(exec_code, exec_globals, exec_locals)
                result = exec_locals["_executor"]()
                return result
            except Exception as e:
                print(f"Error in event handler: {e}")
                traceback.print_exc()
                return str(e)
        self.event_handlers[key] = wrapped_callback
        return key
class Element:
    def __init__(self, window, js_path):
        self.window = window
        self.js_path = js_path

    @property
    def innerText(self) -> str:
        """Get inner text"""
        return self.window.evaluate_js(f'{self.js_path}.innerText')
    @innerText.setter
    def innerText(self, value):
        """Set inner text"""
        self.window.evaluate_js(f'{self.js_path}.innerText = {json.dumps(value)}')
    
    @property
    def innerHTML(self) -> str:
        """Get inner HTML"""
        return self.window.evaluate_js(f'{self.js_path}.innerHTML')
    @innerHTML.setter
    def innerHTML(self, value):
        """Set inner HTML - Warning: this can lead to XSS vulnerabilities if not sanitized properly. Use with caution."""
        self.window.evaluate_js(f'{self.js_path}.innerHTML = {json.dumps(value)}')
    
    def setAttribute(self, attr_name, value):
        """Set attribute"""
        self.window.evaluate_js(f'{self.js_path}.setAttribute("{attr_name}", {json.dumps(value)})')
    
    @property
    def value(self):
        """Get value of form element."""
        return self.window.evaluate_js(f'{self.js_path}.value')
    @value.setter
    def value(self, value):
        """Set value of form element."""
        self.window.evaluate_js(f'{self.js_path}.value = {json.dumps(value)}')
    
    @property
    def style(self):
        """Get style object"""
        return Style(self.window, f'{self.js_path}.style')
    
    def appendChild(self, child):
        """Append child"""
        if isinstance(child, Element):
            self.window.evaluate_js(f'{self.js_path}.appendChild({child.js_path})')
        else:
            raise TypeError("appendChild expects an Element")
    def removeChild(self, child):
        """Remove child"""
        if isinstance(child, Element):
            self.window.evaluate_js(f'{self.js_path}.removeChild({child.js_path})')
        else:
            raise TypeError("removeChild expects an Element")
    
    def replaceChild(self, new_child, old_child):
        """Replace child"""
        if isinstance(new_child, Element) and isinstance(old_child, Element):
            self.window.evaluate_js(f'{self.js_path}.replaceChild({new_child.js_path}, {old_child.js_path})')
        else:
            raise TypeError("replaceChild expects Element objects")
    
    def addEventListener(self, event_type, callback)-> bool:
        """Add event listener. Returns success. Example:
        >>> element.addEventListener("click",callback_function)
        -> True (if successful)"""
        
        element_id = self.window.evaluate_js(f"""
            (function() {{
                var el = {self.js_path};
                return el ? (el.id || 'anonymous_' + Math.random().toString(36).substr(2, 9)) : null;
            }})()
        """)
        
        if not element_id:
            print(f"WARNING: Could not get ID for element: {self.js_path}")
            return False
        
        # Get the PyContext from the window
        context = None
        if hasattr(self.window, '_py_context'):
            context = self.window._py_context
        
        if not context:
            # If no context found, create a temporary one just for this handler
            context = PositronContext(self.window)
            self.window._py_context = context
            
        # Register the event handler with the context
        handler_key = context.register_event_handler(element_id, event_type, callback)
        
        # Create global event handler if not already created
        if not hasattr(self.window, 'handle_py_event'):
            def handle_py_event(element_id, event_type):
                key = f"{element_id}_{event_type}"
                if hasattr(self.window, '_py_context') and key in self.window._py_context.event_handlers:
                    try:
                        return self.window._py_context.event_handlers[key]()
                    except Exception as e:
                        print(f"[ERROR] handling event: {e}")
                        traceback.print_exc()
                        return str(e)
                print(f"WARNING: No handler found for {key}")
                return False
                
            self.window.handle_py_event = handle_py_event
            self.window.expose(handle_py_event)
        
        # Add the event listener in JavaScript
        js_code = f"""
        (function() {{
            var element = {self.js_path};
            if (!element) return false;
            
            element.addEventListener("{event_type}", function(event) {{
                console.log("Event triggered: {event_type} on {element_id}");
                window.pywebview.api.handle_py_event("{element_id}", "{event_type}");
            }});
            return true;
        }})();
        """
        
        success = self.window.evaluate_js(js_code)
        
        return success
class Style:
    def __init__(self, window, js_path):
        self.window = window
        self.js_path = js_path
    
    def __setattr__(self, name, value):
        if name in ['window', 'js_path']:
            super().__setattr__(name, value)
        else:
            self.window.evaluate_js(f'{self.js_path}.{name} = {json.dumps(value)}')
    def __getattr__(self, name):
        return self.window.evaluate_js(f'{self.js_path}.{name}')
    
class ElementList:
    def __init__(self, window, js_path):
        self.window = window
        self.js_path = js_path
        self.length = self.window.evaluate_js(f'{self.js_path}.length') or 0
    def __getitem__(self, index):
        if 0 <= index < self.length:
            return Element(self.window, f'{self.js_path}[{index}]')
        raise IndexError("ElementList index out of range")
    def __len__(self):
        return self.length
    def __iter__(self):
        for i in range(self.length):
            yield self[i]

class Document:
    def __init__(self, window):
        self.window = window
        #self.readyState
    def getElementById(self, element_id) -> Element:
        """Get element by ID"""
        return Element(self.window, f'document.getElementById("{element_id}")')
    def getElementsByClassName(self, class_name) -> ElementList:
        """Get elements by class name"""
        return ElementList(self.window, f'document.getElementsByClassName("{class_name}")')
    def querySelector(self, selector) -> Element:
        """Query selector - Selects a single element from the DOM matching a CSS query selector."""
        return Element(self.window, f'document.querySelector("{selector}")')
    def querySelectorAll(self, selector) -> ElementList:
        """Query selector all - Selects all elements from the DOM matching a CSS query selector."""
        return ElementList(self.window, f'document.querySelectorAll("{selector}")')
    def createElement(self, tag_name) -> Element:
        """Create element"""
        return Element(self.window, f'document.createElement("{tag_name}")')
    def alert(self, message) -> None:
        """Show alert pop-up."""
        self.window.evaluate_js(f'alert("{escape_js_string(message)}")')
    def confirm(self, message) -> bool:
        """Show confirm dialog with "Yes" and "No" buttons, returns True or False."""
        return self.window.evaluate_js(f'confirm("{escape_js_string(message)}")')
    def prompt(self, message:str, default_value=None) -> str:
        """Show prompt dialog with an input field, returns the input value or None if cancelled."""
        if default_value:
            return self.window.evaluate_js(f'prompt("{(escape_js_string(message))}", "{escape_js_string(default_value)}")')
        return self.window.evaluate_js(f'prompt("{escape_js_string(message)}")')
    @property
    def body(self) -> Element:
        """Get body element"""
        return Element(self.window, 'document.body')
    @property
    def html(self) -> Element:
        """Get html element"""
        return Element(self.window, 'document.html')
    @property
    def forms(self) -> ElementList:
        """Get all the forms in a document"""
        return ElementList(self.window, 'document.forms')

class ExposedFunctions:
    def __init__(self, functions_dict):
        for name, func in functions_dict.items():
            setattr(self, name, func)

def run_python_code_in_html(html_content, context):
    try:
        src_pattern = re.compile(r'<py(?:\s*|\s.*\s)src=\"(.*?)\"\s?.*>.*</py>', re.DOTALL)
        for match in src_pattern.finditer(html_content):
            path = match.group(1)
            if not os.path.isabs(path):
                path = os.path.abspath(path)
            try:
                with open(path, 'r', encoding='utf-8') as f:
                    src_code = f.read()
            except Exception as e:
                print(f"[ERROR] loading Python src '{path}': {e}")
            try:
                success, error = context.execute(src_code)
                if success == False:
                    print(f"[ERROR] in <py src> tag code execution: {error}")

            except Exception as e:
                print(f"[ERROR] in <py src> tag execution function: {e}\n[NOTE] This error happened within the function for running the python code, not the code itself.\nIt may be a problem with PyPositron.")
        
        no_src_pattern=re.compile(r"<py(?:|\s.+)>(.*?)</py>",re.DOTALL)
        matches = list(no_src_pattern.finditer(html_content))
        for match in matches:
            code = match.group(1)
            success, error = context.execute(code)
            if success==False:
                print(f"[ERROR] in <py> tag execution: {error}")
        return html_content
    except Exception as e:
        print(f"[ERROR] in processsing <py> tags: {e}")
        print(traceback.format_exc())
        return html_content

def openUI(html_path, functions=None, width=900, height=700, title="Python UI"):
    if not os.path.isabs(html_path):
        html_path = os.path.abspath(html_path)
    if not os.path.exists(html_path):
        raise FileNotFoundError(f"HTML file not found: {html_path}")
    print(f"Loading HTML from: {html_path}")
    with open(html_path, 'r', encoding='utf-8') as f:
        html_content = f.read()
    window = webview.create_window(
        title=title,
        url=html_path,
        width=width,
        height=height
    )
    context = PositronContext(window)
    window._py_context = context
    if functions:
        for func in functions:
            context.exposed_functions[func.__name__] = func
            window.expose(func)
    def process_html():
        try:
            sys.stdout.flush()
            run_python_code_in_html(html_content, context)
            if functions:
                function_dict = {func.__name__: func for func in functions}
                js_code = """
                if (!window.python) {
                    window.python = {};
                }
                """
                for func_name in function_dict:
                    js_code += f"""
                    window.python.{func_name} = function() {{
                        return window.pywebview.api.{func_name}.apply(null, arguments);
                    }};
                    """
                window.evaluate_js(js_code)
            def debug_event_handlers():
                if hasattr(window, '_py_context'):
                    handlers = list(window._py_context.event_handlers.keys())
                    return f"Registered handlers: {handlers}"
                return "No event handlers found"
            window.expose(debug_event_handlers)
        except Exception as e:
            print(f"[ERROR] in process_html thread: {e}")
            print(traceback.format_exc())
    

    thread = threading.Thread(target=process_html, daemon=True)
    thread.start()
    return window
def start():
    """Start the webview event loop."""
    webview.start()
