import json
import logging
import os
import pathlib
import sys
import webbrowser
from threading import Semaphore, Thread, main_thread
from typing import Any
from uuid import uuid1

from webview import FileDialog, _state, settings, windows
from webview.dom import _dnd_state
from webview.menu import Menu, MenuAction, MenuSeparator
from webview.models import Request, Response
from webview.screen import Screen
from webview.util import (
    DEFAULT_HTML,
    create_cookie,
    inject_pywebview,
    js_bridge_call,
    parse_file_type,
)
from webview.window import FixPoint, Window

logger = logging.getLogger('pywebview')
os.environ['EGL_LOG_LEVEL'] = 'fatal'

import gi  # noqa: E402

gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')

try:
    gi.require_version('WebKit2', '4.1')
    gi.require_version('Soup', '3.0')
except ValueError:
    logger.debug('WebKit2 4.1 not found. Using 4.0.')
    gi.require_version('WebKit2', '4.0')
    gi.require_version('Soup', '2.4')

from gi.repository import Gdk, Gio  # noqa: E402
from gi.repository import GLib as glib  # noqa: E402
from gi.repository import Gtk as gtk  # noqa: E402
from gi.repository import WebKit2 as webkit  # noqa: E402

renderer = 'gtkwebkit2'
webkit_ver = webkit.get_major_version(), webkit.get_minor_version(), webkit.get_micro_version()

_app = None
_app_actions = {}  # action_label: function

cert = None


class BrowserView:
    instances = {}

    class JSBridge:
        def __init__(self, window: Window) -> None:
            self.window = window
            self.uid = uuid1().hex[:8]

        def call(self, func_name: str, param: Any, value_id: str):
            if param == 'undefined':
                param = None
            return js_bridge_call(self.window, func_name, param, value_id)

    def __init__(self, window: Window) -> None:
        # Note: _app won't be None because BrowserView() is called after _app is made in `create_window`
        global _app

        BrowserView.instances[window.uid] = self
        self.uid = window.uid
        self.pywebview_window = window

        self.is_fullscreen = False
        self.js_results = {}

        self.window = gtk.ApplicationWindow(title=window.title, application=_app)
        self.pywebview_window.native = self.window

        self.shown = window.events.shown
        self.loaded = window.events.loaded

        self.localization = window.localization

        self._last_width = window.initial_width
        self._last_height = window.initial_height

        if window.screen:
            self.screen = window.screen.frame
        else:
            display = Gdk.Display.get_default()
            monitor = Gdk.Display.get_monitor(display, 0)
            self.screen = Gdk.Monitor.get_geometry(monitor)

        if window.resizable:
            self.window.set_size_request(window.min_size[0], window.min_size[1])
            self.window.resize(window.initial_width, window.initial_height)
        else:
            self.window.set_size_request(window.initial_width, window.initial_height)

        if window.maximized:
            self.window.maximize()
        elif window.minimized:
            self.window.iconify()

        if window.initial_x is not None and window.initial_y is not None:
            self.move(window.initial_x, window.initial_y)
        else:
            window_width, window_height = self.window.get_size()
            x = (self.screen.width - window_width) // 2
            y = (self.screen.height - window_height) // 2
            self.move(x, y)

        self.window.set_resizable(window.resizable)
        self.window.set_accept_focus(window.focus)

        # Set window background color
        style_provider = gtk.CssProvider()
        style_provider.load_from_data(
            f'GtkWindow {{ background-color: {window.background_color}; }}'.encode()
        )
        gtk.StyleContext.add_provider_for_screen(
            Gdk.Screen.get_default(), style_provider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
        )

        # Also set background color directly on ScrolledWindow
        self.background_rgba = self._hex_to_rgba(window.background_color)

        if window.menu:
            logger.warning('Window specific menu is not supported on GTK')

        scrolled_window = gtk.ScrolledWindow()
        scrolled_window.set_policy(gtk.PolicyType.NEVER, gtk.PolicyType.NEVER)

        # Set scrolled window background color using CSS
        scrolled_style_provider = gtk.CssProvider()
        scrolled_style_provider.load_from_data(
            f'* {{ background-color: {window.background_color}; }}'.encode()
        )
        scrolled_window.get_style_context().add_provider(
            scrolled_style_provider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
        )

        self.window.add(scrolled_window)

        self.window.connect('delete-event', self.close_window)

        self.window.connect('window-state-event', self.on_window_state_change)
        self.window.connect('size-allocate', self.on_window_resize)
        self.window.connect('configure-event', self.on_window_configure)

        self.js_bridge = BrowserView.JSBridge(window)

        if _state['private_mode']:
            # Create ephemeral context for private mode
            web_context = webkit.WebContext.new_ephemeral()
        elif _state['storage_path']:
            storage_path = _state['storage_path']

            # Create storage directory if it doesn't exist
            if not os.path.exists(storage_path):
                os.makedirs(storage_path)

            # Create website data manager with storage directories
            self.website_data_manager = webkit.WebsiteDataManager(
                base_data_directory=storage_path,
                base_cache_directory=os.path.join(storage_path, 'cache'),
                local_storage_directory=os.path.join(storage_path, 'localstorage'),
                indexeddb_directory=os.path.join(storage_path, 'indexeddb'),
                websql_directory=os.path.join(storage_path, 'websql'),
                hsts_cache_directory=os.path.join(storage_path, 'hsts'),
                itp_directory=os.path.join(storage_path, 'itp'),
                service_worker_registrations_directory=os.path.join(storage_path, 'serviceworkers'),
                dom_cache_directory=os.path.join(storage_path, 'domcache'),
            )
            web_context = webkit.WebContext.new_with_website_data_manager(self.website_data_manager)
        else:
            web_context = webkit.WebContext.get_default()

        self.cookie_manager = web_context.get_cookie_manager()

        if not _state['private_mode']:
            script_name = pathlib.Path(sys.argv[0]).name
            if script_name in ['python', 'python3', '-c', '']:
                script_name = 'pywebview'
            storage_path = _state['storage_path'] or os.path.expanduser(f'~/.cache/{script_name}/')

            self.cookie_manager.set_persistent_storage(
                os.path.join(storage_path, 'cookies'), webkit.CookiePersistentStorage.TEXT
            )

        if cert:
            web_context.allow_tls_certificate_for_host(cert, '127.0.0.1')

        self.manager = webkit.UserContentManager()
        self.manager.register_script_message_handler('jsBridge')
        self.manager.connect('script-message-received', self.on_js_bridge_call)

        self.request_headers_mutated = False

        # Create WebView with the configured context
        self.webview = webkit.WebView(user_content_manager=self.manager, web_context=web_context)
        self.webview.connect('notify::visible', self.on_webview_ready)
        self.webview.connect('load_changed', self.on_load_finish)
        self.webview.connect('decide-policy', self.on_navigation)
        self.webview.connect('drag-data-received', self.on_drag_data)
        self.webview.connect('resource-load-started', self.on_request)

        if settings['IGNORE_SSL_ERRORS']:
            web_context.set_tls_errors_policy(webkit.TLSErrorsPolicy.IGNORE)

        if settings['ALLOW_DOWNLOADS']:
            web_context.connect('download-started', self.on_download_started)

        webkit_settings = self.webview.get_settings().props
        user_agent = settings.get('user_agent') or _state['user_agent']
        if user_agent:
            webkit_settings.user_agent = user_agent

        webkit_settings.enable_media_stream = True
        webkit_settings.enable_mediasource = True
        webkit_settings.enable_webaudio = True
        webkit_settings.enable_webgl = True
        webkit_settings.javascript_can_access_clipboard = True
        webkit_settings.allow_file_access_from_file_urls = settings['ALLOW_FILE_URLS']

        if window.frameless:
            self.window.set_decorated(False)
            if window.easy_drag:
                self.move_progress = False
                self.webview.connect('button-release-event', self.on_mouse_release)
                self.webview.connect('button-press-event', self.on_mouse_press)
                self.window.connect('motion-notify-event', self.on_mouse_move)

        if window.on_top:
            self.window.set_keep_above(True)

        self.transparent = window.transparent
        if window.transparent:
            configure_transparency(self.window)
            configure_transparency(scrolled_window)
            configure_transparency(self.webview)
            wvbg = self.webview.get_background_color()
            wvbg.alpha = 0.0
            self.webview.set_background_color(wvbg)
        else:
            # Set webview background color for non-transparent windows
            self.webview.set_background_color(self.background_rgba)

        if _state['debug']:
            webkit_settings.enable_developer_extras = True

            if settings['OPEN_DEVTOOLS_IN_DEBUG']:
                self.webview.get_inspector().show()
        else:
            self.webview.connect('context-menu', lambda a, b, c, d: True)  # Disable context menu

        if _state['private_mode']:
            webkit_settings.enable_html5_database = False
            webkit_settings.enable_html5_local_storage = False

        self.webview.set_opacity(0.0)
        scrolled_window.add(self.webview)

        if _state['icon']:
            self.window.set_icon_from_file(_state['icon'])
            self.window.set_default_icon_from_file(_state['icon'])

        self.pywebview_window.events.before_show.set()

        if window.fullscreen:
            self.toggle_fullscreen()

        if window.real_url is not None:
            self.webview.load_uri(window.real_url)
        elif window.html:
            self.webview.load_html(window.html, '')
        else:
            self.webview.load_html(DEFAULT_HTML, '')

    def close_window(self, *data):
        should_cancel = self.pywebview_window.events.closing.set()

        if should_cancel:
            return True

        if self.pywebview_window.confirm_close:
            dialog = gtk.MessageDialog(
                parent=self.window,
                flags=gtk.DialogFlags.MODAL & gtk.DialogFlags.DESTROY_WITH_PARENT,
                type=gtk.MessageType.QUESTION,
                buttons=gtk.ButtonsType.OK_CANCEL,
                message_format=self.localization['global.quitConfirmation'],
            )
            result = dialog.run()
            dialog.destroy()
            if result == gtk.ResponseType.CANCEL:
                return True

        for res in self.js_results.values():
            res['semaphore'].release()

        self.window.destroy()
        del BrowserView.instances[self.uid]

        if self.pywebview_window in windows:
            windows.remove(self.pywebview_window)

        self.pywebview_window.events.closed.set()

        return False

    def on_drag_data(self, widget, drag_context, x, y, data, info, time):
        text = data.get_text()
        if _dnd_state['num_listeners'] > 0 and text:
            files = []
            for value in text.split('\n'):
                value = value.strip()
                if value.startswith('file://'):
                    # Handle 'file://' URIs (e.g., `file:///home/user/file.txt`)
                    path = value.replace('file://', '')
                    files.append((os.path.basename(path), path))
                elif value.startswith('/') and os.path.exists(value):
                    # Handle direct paths (e.g., `/home/user/file.txt`)
                    files.append((os.path.basename(value), value))

            _dnd_state['paths'] += files

        return False

    def on_window_state_change(self, window, window_state):
        if window_state.changed_mask == Gdk.WindowState.ICONIFIED:
            if (
                Gdk.WindowState.ICONIFIED & window_state.new_window_state
                == Gdk.WindowState.ICONIFIED
            ):
                self.pywebview_window.events.minimized.set()
            else:
                self.pywebview_window.events.restored.set()

        elif window_state.changed_mask == Gdk.WindowState.MAXIMIZED:
            if (
                Gdk.WindowState.MAXIMIZED & window_state.new_window_state
                == Gdk.WindowState.MAXIMIZED
            ):
                self.pywebview_window.events.maximized.set()
            else:
                self.pywebview_window.events.restored.set()

    def on_js_bridge_call(self, manager, message):
        body = json.loads(message.get_js_value().to_string())

        if body['funcName'] == '_pywebviewAlert':
            self.message_box(body['params'])
        else:
            js_bridge_call(self.pywebview_window, body['funcName'], body['params'], body['id'])

    def on_window_resize(self, window, allocation):
        if allocation.width != self._last_width or allocation.height != self._last_height:
            self._last_width = allocation.width
            self._last_height = allocation.height
            self.pywebview_window.events.resized.set(allocation.width, allocation.height)

    def on_window_configure(self, window, event):
        self.pywebview_window.events.moved.set(event.x, event.y)

    def on_webview_ready(self, arg1, arg2):
        # in webkit2 notify:visible fires after the window was closed and BrowserView object destroyed.
        # for a lack of better solution we check that BrowserView has 'webview_ready' attribute
        if 'shown' in dir(self):
            self.shown.set()

    def on_response(self, resource, _):
        response = resource.get_response()
        headers = response.get_http_headers()
        original_headers = self._headers_to_dict(headers)
        url = resource.get_uri()

        response_ = Response(url, response.get_status_code(), original_headers)
        self.pywebview_window.events.response_received.set(response_)

    def on_request(self, webview, resource, request):
        if len(self.pywebview_window.events.response_received) > 0:
            resource.connect('notify::response', self.on_response)

        if len(self.pywebview_window.events.request_sent) == 0:
            return

        headers = request.get_http_headers()
        original_headers = self._headers_to_dict(headers)
        url = request.get_uri()
        method = request.get_http_method()

        request_ = Request(url, method, original_headers)
        self.pywebview_window.events.request_sent.set(request_)

        if (
            request_.headers == original_headers
            or not headers
            or self.request_headers_mutated
            or url != self.pywebview_window.real_url
        ):
            return

        missing_headers = {
            k: v
            for k, v in request_.headers.items()
            if k not in original_headers or original_headers[k] != v
        }
        extra_headers = {
            k: str(v) for k, v in original_headers.items() if k not in request_.headers
        }

        for k, v in missing_headers.items():
            headers.append(k, v)

        for k in extra_headers:
            headers.remove(k)

        # headers.append('X-Handled', 'true')
        webview.stop_loading()
        self.request_headers_mutated = True
        webview.load_request(request)

    def on_load_finish(self, webview, status):
        # Show the webview if it's not already visible
        if not webview.props.opacity:
            glib.idle_add(webview.set_opacity, 1.0)

        if status == webkit.LoadEvent.FINISHED and not self.request_headers_mutated:
            inject_pywebview(renderer, self.js_bridge.window)

        if self.request_headers_mutated:
            self.request_headers_mutated = False

    def on_download_started(self, session, download):
        download.connect('decide-destination', self.on_download_decide_destination)

    def on_download_decide_destination(self, download, suggested_filename):
        destination = self.create_file_dialog(
            FileDialog.SAVE,
            glib.get_user_special_dir(glib.UserDirectory.DIRECTORY_DOWNLOAD),
            False,
            suggested_filename,
            (),
        )

        if destination:
            destination_uri = glib.filename_to_uri(destination[0])
            download.set_destination(destination_uri)
        else:
            download.cancel()

    def on_navigation(self, webview, decision, decision_type):
        if type(decision) is webkit.NavigationPolicyDecision:
            uri = decision.get_navigation_action().get_request().get_uri()

            if decision.get_navigation_action().get_frame_name() == '_blank':
                if settings['OPEN_EXTERNAL_LINKS_IN_BROWSER']:
                    webbrowser.open(uri, 2, True)
                    decision.ignore()
                else:
                    self.load_url(uri)
        elif type(decision) is webkit.ResponsePolicyDecision:
            if not decision.is_mime_type_supported():
                self._download_filename = decision.get_response().get_suggested_filename()
                decision.download()
            else:
                decision.use()

    def on_mouse_release(self, sender, event):
        self.move_progress = False

    def on_mouse_press(self, _, event):
        self.point_diff = [
            x - y for x, y in zip(self.window.get_position(), [event.x_root, event.y_root])
        ]
        self.move_progress = True

    def on_mouse_move(self, _, event):
        if self.move_progress:
            point = [x + y for x, y in zip((event.x_root, event.y_root), self.point_diff)]
            self.window.move(point[0], point[1])

    def show(self):
        self.window.show_all()

        if gtk.main_level() == 0:
            if self.pywebview_window.hidden:
                self.window.hide()
        else:
            glib.idle_add(self.window.show_all)

    def hide(self):
        glib.idle_add(self.window.hide)

    def destroy(self):
        self.window.emit('delete-event', Gdk.Event())

    def set_title(self, title):
        self.window.set_title(title)

    def toggle_fullscreen(self):
        if self.is_fullscreen:
            self.window.unfullscreen()
        else:
            self.window.fullscreen()

        self.is_fullscreen = not self.is_fullscreen

    def resize(self, width, height, fix_point):
        if fix_point & FixPoint.NORTH and fix_point & FixPoint.WEST:
            self.window.set_gravity(Gdk.Gravity.NORTH_WEST)
        elif fix_point & FixPoint.NORTH and fix_point & FixPoint.EAST:
            self.window.set_gravity(Gdk.Gravity.NORTH_EAST)
        elif fix_point & FixPoint.SOUTH and fix_point & FixPoint.EAST:
            self.window.set_gravity(Gdk.Gravity.SOUTH_EAST)
        elif fix_point & FixPoint.SOUTH and fix_point & FixPoint.WEST:
            self.window.set_gravity(Gdk.Gravity.SOUTH_WEST)
        elif fix_point & FixPoint.SOUTH:
            self.window.set_gravity(Gdk.Gravity.SOUTH)
        elif fix_point & FixPoint.NORTH:
            self.window.set_gravity(Gdk.Gravity.NORTH)
        elif fix_point & FixPoint.WEST:
            self.window.set_gravity(Gdk.Gravity.WEST)
        elif fix_point & FixPoint.EAST:
            self.window.set_gravity(Gdk.Gravity.EAST)

        self.window.resize(width, height)

    def move(self, x, y):
        self.window.move(self.screen.x + x, self.screen.y + y)

    def maximize(self):
        glib.idle_add(self.window.maximize)

    def minimize(self):
        glib.idle_add(self.window.iconify)

    def restore(self):
        def _restore():
            self.window.deiconify()
            self.window.present()

        glib.idle_add(_restore)

    def create_confirmation_dialog(self, title, message):
        dialog = gtk.MessageDialog(
            parent=self.window,
            flags=gtk.DialogFlags.MODAL & gtk.DialogFlags.DESTROY_WITH_PARENT,
            type=gtk.MessageType.QUESTION,
            text=title,
            message_format=message,
            buttons=gtk.ButtonsType.OK_CANCEL,
        )
        response = dialog.run()
        dialog.destroy()
        if response == gtk.ResponseType.OK:
            return True

        return False

    def create_file_dialog(self, dialog_type, directory, allow_multiple, save_filename, file_types):
        if dialog_type == FileDialog.FOLDER:
            gtk_dialog_type = gtk.FileChooserAction.SELECT_FOLDER
            title = self.localization['linux.openFolder']
            button = gtk.STOCK_OPEN
        elif dialog_type == FileDialog.OPEN:
            gtk_dialog_type = gtk.FileChooserAction.OPEN
            if allow_multiple:
                title = self.localization['linux.openFiles']
            else:
                title = self.localization['linux.openFile']

            button = gtk.STOCK_OPEN
        elif dialog_type == FileDialog.SAVE:
            gtk_dialog_type = gtk.FileChooserAction.SAVE
            title = self.localization['global.saveFile']
            button = gtk.STOCK_SAVE

        dialog = gtk.FileChooserDialog(
            title,
            self.window,
            gtk_dialog_type,
            (gtk.STOCK_CANCEL, gtk.ResponseType.CANCEL, button, gtk.ResponseType.OK),
        )

        dialog.set_select_multiple(allow_multiple)
        dialog.set_current_folder(directory)
        self._add_file_filters(dialog, file_types)

        if dialog_type == FileDialog.SAVE:
            dialog.set_current_name(save_filename)

        response = dialog.run()

        if response == gtk.ResponseType.OK:
            if dialog_type == FileDialog.SAVE:
                file_name = (dialog.get_filename(),)
            else:
                file_name = dialog.get_filenames()
        else:
            file_name = None

        dialog.destroy()

        return file_name

    def _add_file_filters(self, dialog, file_types):
        for s in file_types:
            description, extensions = parse_file_type(s)

            f = gtk.FileFilter()
            f.set_name(description)
            for e in extensions.split(';'):
                f.add_pattern(e)

            dialog.add_filter(f)

    def clear_cookies(self):
        def _clear_cookies():
            self.cookie_manager.delete_all_cookies()

        glib.idle_add(_clear_cookies)

    def get_cookies(self):
        def _get_cookies():
            self.cookie_manager.get_cookies(self.webview.get_uri(), None, callback, None)

        def callback(source, task, data):
            results = source.get_cookies_finish(task)

            for c in results:
                cookie = create_cookie(c.to_set_cookie_header())
                cookies.append(cookie)

            semaphore.release()

        cookies = []
        semaphore = Semaphore(0)
        glib.idle_add(_get_cookies)
        semaphore.acquire()

        return cookies

    def get_current_url(self):
        uri = self.webview.get_uri()
        return uri if uri != 'about:blank' else None

    def load_url(self, url):
        self.webview.load_uri(url)

    def load_html(self, content, base_uri):
        self.webview.load_html(content, base_uri)

    def evaluate_js(self, script, parse_json):
        def _evaluate_js():
            try:
                self.webview.evaluate_javascript(
                    script=script,
                    length=len(script),
                    world_name=None,
                    source_uri=None,
                    cancellable=None,
                    callback=_callback,
                )
            except Exception:
                logger.exception('Error evaluating JavaScript')
                result_semaphore.release()

        def _callback(webview, task):
            nonlocal result
            try:
                value = webview.evaluate_javascript_finish(task)
                res = self._convert_js_value(value)

                if parse_json and res:
                    try:
                        result = json.loads(res)
                    except Exception:
                        pass
                else:
                    result = res

            except Exception as e:
                logger.exception(e)

            result_semaphore.release()

        result_semaphore = Semaphore(0)
        result = None
        glib.idle_add(_evaluate_js)
        result_semaphore.acquire()

        return result

    def message_box(self, message):
        dialog = gtk.MessageDialog(
            parent=self.window,
            flags=gtk.DialogFlags.MODAL & gtk.DialogFlags.DESTROY_WITH_PARENT,
            type=gtk.MessageType.INFO,
            buttons=gtk.ButtonsType.OK,
            message_format=message,
        )
        dialog.run()
        dialog.destroy()

    def _convert_js_value(self, js_value):
        if not js_value or js_value.is_null() or js_value.is_undefined():
            return None
        elif js_value.is_boolean():
            return js_value.to_boolean()
        elif js_value.is_number():
            return js_value.to_double()
        elif js_value.is_string():
            return js_value.to_string()
        elif js_value.is_object():
            return json.loads(js_value.to_json(2))
        else:
            logger.error(f'Unsupported JavaScriptCore.Value type: {js_value}')
            return js_value.to_string()

    def _headers_to_dict(self, headers):
        def _assign(k, v):
            headers_dict[k] = v

        headers_dict = {}

        if headers:
            headers.foreach(_assign)
        return headers_dict

    def _hex_to_rgba(self, hex_color):
        """Convert hex color (#RRGGBB or #RGB) to Gdk.RGBA object."""
        hex_color = hex_color.lstrip('#')

        # Convert 3-digit to 6-digit format
        if len(hex_color) == 3:
            hex_color = ''.join([c * 2 for c in hex_color])

        # Parse hex values
        r = int(hex_color[0:2], 16) / 255.0
        g = int(hex_color[2:4], 16) / 255.0
        b = int(hex_color[4:6], 16) / 255.0

        return Gdk.RGBA(r, g, b, 1.0)


def setup_app():
    def set_menubar(app):
        app.set_menubar(app_menu)

    global _app
    if _app is not None:
        return

    _app = gtk.Application.new(None, 0)

    if _state['menu']:
        app_menu = create_menu(_state['menu'])
        _app.connect('startup', set_menubar)


def create_window(window):
    global _app

    def create():
        browser = BrowserView(window)
        browser.show()

    def create_master_callback(app):
        create()

    if window.uid == 'master':
        main_thread().pydev_do_not_trace = True  # vs code debugger hang fix
        _app.connect('activate', create_master_callback)
        _app.run()
        _app = None
    else:
        # _app will already have been activated by this point
        glib.idle_add(create)


def set_title(title, uid):
    def _set_title():
        i.set_title(title)

    i = BrowserView.instances.get(uid)
    if i:
        glib.idle_add(_set_title)


def destroy_window(uid):
    def _destroy_window():
        i.close_window()

    i = BrowserView.instances.get(uid)
    if i:
        glib.idle_add(_destroy_window)


def toggle_fullscreen(uid):
    def _toggle_fullscreen():
        i.toggle_fullscreen()

    i = BrowserView.instances.get(uid)
    if i:
        glib.idle_add(_toggle_fullscreen)


def add_tls_cert(certfile):
    global cert
    cert = Gio.TlsCertificate.new_from_file(certfile)


def set_on_top(uid, top):
    def _set_on_top():
        i.window.set_keep_above(top)

    i = BrowserView.instances.get(uid)
    if i:
        glib.idle_add(_set_on_top)


def resize(width, height, uid, fix_point):
    def _resize():
        i.resize(width, height, fix_point)

    i = BrowserView.instances.get(uid)
    if i:
        glib.idle_add(_resize)


def move(x, y, uid):
    def _move():
        i.move(x, y)

    i = BrowserView.instances.get(uid)
    if i:
        glib.idle_add(_move)


def hide(uid):
    i = BrowserView.instances.get(uid)
    if i:
        glib.idle_add(i.hide)


def show(uid):
    i = BrowserView.instances.get(uid)
    if i:
        glib.idle_add(i.show)


def maximize(uid):
    i = BrowserView.instances.get(uid)
    if i:
        glib.idle_add(i.maximize)


def minimize(uid):
    i = BrowserView.instances.get(uid)
    if i:
        glib.idle_add(i.minimize)


def restore(uid):
    i = BrowserView.instances.get(uid)
    if i:
        glib.idle_add(i.restore)


def clear_cookies(uid):
    i = BrowserView.instances.get(uid)
    if i:
        i.clear_cookies()


def get_cookies(uid):
    i = BrowserView.instances.get(uid)
    if i:
        cookies = i.get_cookies()
        return cookies


def get_current_url(uid):
    def _get_current_url():
        result['url'] = i.get_current_url()
        semaphore.release()

    result = {}
    semaphore = Semaphore(0)

    i = BrowserView.instances.get(uid)
    if not i:
        return

    glib.idle_add(_get_current_url)
    semaphore.acquire()

    return result['url']


def load_url(url, uid):
    def _load_url():
        i.load_url(url)

    i = BrowserView.instances.get(uid)
    if i:
        glib.idle_add(_load_url)


def load_html(content, base_uri, uid):
    def _load_html():
        i.load_html(content, base_uri)

    i = BrowserView.instances.get(uid)
    if i:
        glib.idle_add(_load_html)


def create_confirmation_dialog(title, message, uid):
    def _create():
        nonlocal result
        result = i.create_confirmation_dialog(title, message)
        result_semaphore.release()

    i = BrowserView.instances.get(uid)
    result_semaphore = Semaphore(0)
    result = -1

    if i:
        glib.idle_add(_create)
        result_semaphore.acquire()

    return result


def create_menu(app_menu_list):
    def action_callback(action, parameter):
        function = _app_actions.get(action.get_name())
        if function is None:
            return
        # Don't run action function on main thread
        Thread(target=function).start()

    def create_submenu(title, line_items, supermenu, action_prepend=''):
        m = Gio.Menu.new()
        current_section = Gio.Menu.new()
        action_prepend = f'{action_prepend}_{title}'
        for menu_line_item in line_items:
            if isinstance(menu_line_item, MenuSeparator):
                m.append_section(None, current_section)
                current_section = Gio.Menu.new()
            elif isinstance(menu_line_item, MenuAction):
                action_label = f'{action_prepend}_{menu_line_item.title}'.replace(' ', '_')
                while action_label in _app_actions.keys():
                    action_label += '_'
                _app_actions[action_label] = menu_line_item.function
                new_action = Gio.SimpleAction.new(action_label, None)
                new_action.connect('activate', action_callback)
                _app.add_action(new_action)
                current_section.append(menu_line_item.title, 'app.' + action_label)
            elif isinstance(menu_line_item, Menu):
                create_submenu(
                    menu_line_item.title,
                    menu_line_item.items,
                    current_section,
                    action_prepend=action_prepend,
                )

        m.append_section(None, current_section)

        supermenu.append_submenu(title, m)

    menubar = Gio.Menu()

    for app_menu in app_menu_list:
        # Ignore '__app__' menus (macOS-only feature)
        if app_menu.title == '__app__':
            continue
        create_submenu(app_menu.title, app_menu.items, menubar)

    return menubar


def get_active_window():
    active_window = None
    try:
        active_window = _app.get_active_window()
    except Exception:
        return None

    active_window_number = active_window.get_id()

    for uid, browser_view_instance in BrowserView.instances.items():
        if browser_view_instance.window.get_id() == active_window_number:
            return browser_view_instance.pywebview_window

    return None


def create_file_dialog(dialog_type, directory, allow_multiple, save_filename, file_types, uid):
    i = BrowserView.instances.get(uid)
    file_name_semaphore = Semaphore(0)
    file_names = []

    def _create():
        result = i.create_file_dialog(
            dialog_type, directory, allow_multiple, save_filename, file_types
        )
        if result is None:
            file_names.append(None)
        else:
            file_names.append(tuple(result))

        file_name_semaphore.release()

    glib.idle_add(_create)
    file_name_semaphore.acquire()

    return file_names[0]


def evaluate_js(script, uid, parse_json=True):
    i = BrowserView.instances.get(uid)

    if i:
        return i.evaluate_js(script, parse_json)


def get_position(uid):
    def _get_position():
        result['position'] = i.window.get_position()
        semaphore.release()

    i = BrowserView.instances.get(uid)
    if not i:
        return

    result = {}
    semaphore = Semaphore(0)

    glib.idle_add(_get_position)
    semaphore.acquire()

    return result['position']


def get_size(uid):
    def _get_size():
        result['size'] = i.window.get_size()
        semaphore.release()

    i = BrowserView.instances.get(uid)
    if not i:
        return

    result = {}
    semaphore = Semaphore(0)

    glib.idle_add(_get_size)
    semaphore.acquire()

    return result['size']


def get_screens():
    display = Gdk.Display.get_default()
    n = display.get_n_monitors()
    monitors = [Gdk.Display.get_monitor(display, i) for i in range(n)]
    geometries = [Gdk.Monitor.get_geometry(m) for m in monitors]
    scales = [Gdk.Monitor.get_scale_factor(m) for m in monitors]
    screens = [
        Screen(geom.x, geom.y, geom.width, geom.height, geom, scale)
        for geom, scale in zip(geometries, scales)
    ]

    return screens


def configure_transparency(c):
    c.set_visual(c.get_screen().get_rgba_visual())
    c.override_background_color(gtk.StateFlags.ACTIVE, Gdk.RGBA(0, 0, 0, 0))
    c.override_background_color(gtk.StateFlags.BACKDROP, Gdk.RGBA(0, 0, 0, 0))
    c.override_background_color(gtk.StateFlags.DIR_LTR, Gdk.RGBA(0, 0, 0, 0))
    c.override_background_color(gtk.StateFlags.DIR_RTL, Gdk.RGBA(0, 0, 0, 0))
    c.override_background_color(gtk.StateFlags.FOCUSED, Gdk.RGBA(0, 0, 0, 0))
    c.override_background_color(gtk.StateFlags.INCONSISTENT, Gdk.RGBA(0, 0, 0, 0))
    c.override_background_color(gtk.StateFlags.INSENSITIVE, Gdk.RGBA(0, 0, 0, 0))
    c.override_background_color(gtk.StateFlags.NORMAL, Gdk.RGBA(0, 0, 0, 0))
    c.override_background_color(gtk.StateFlags.PRELIGHT, Gdk.RGBA(0, 0, 0, 0))
    c.override_background_color(gtk.StateFlags.SELECTED, Gdk.RGBA(0, 0, 0, 0))
    transparentWindowStyleProvider = gtk.CssProvider()
    transparentWindowStyleProvider.load_from_data(
        b"""
        GtkWindow {
            background-color:rgba(0,0,0,0);
            background-image:none;
        }"""
    )
    c.get_style_context().add_provider(
        transparentWindowStyleProvider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
    )
