from __future__ import annotations

import json
import logging
import os
import urllib
import uuid
import webbrowser
from threading import Semaphore, Thread, main_thread

import AppKit
import Foundation
import WebKit
from objc import nil, super
from PyObjCTools import AppHelper

from webview import FileDialog, _state, windows
from webview import settings as webview_settings
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,
    stringify_headers,
)
from webview.window import FixPoint

# This lines allow to load non-HTTPS resources, like a local app as: http://127.0.0.1:5000
bundle = AppKit.NSBundle.mainBundle()
info = bundle.localizedInfoDictionary() or bundle.infoDictionary()
info['NSAppTransportSecurity'] = {'NSAllowsArbitraryLoads': Foundation.YES}
info['NSRequiresAquaSystemAppearance'] = Foundation.NO  # Enable dark mode support for Mojave

# Fallbacks, in case these constants are not wrapped by PyObjC
try:
    NSFullSizeContentViewWindowMask = AppKit.NSFullSizeContentViewWindowMask
except AttributeError:
    NSFullSizeContentViewWindowMask = 1 << 15

try:
    NSWindowTitleHidden = AppKit.NSWindowTitleHidden
except AttributeError:
    NSWindowTitleHidden = 1

logger = logging.getLogger('pywebview')
logger.debug('Using Cocoa')

renderer = 'wkwebview'


class BrowserView:
    instances = {}
    app = AppKit.NSApplication.sharedApplication()
    app.setActivationPolicy_(0)
    current_menu = None

    cascade_loc = Foundation.NSMakePoint(100.0, 0.0)
    _shared_app_delegate = None

    class AppDelegate(AppKit.NSObject):
        def applicationShouldTerminate_(self, app):
            should_close = True
            for i in BrowserView.instances.values():
                should_close = should_close and BrowserView.should_close(i.pywebview_window)
            return Foundation.YES if should_close else Foundation.NO

        def applicationSupportsSecureRestorableState_(self, app):
            return Foundation.YES

    class WindowHost(AppKit.NSWindow):
        def canBecomeKeyWindow(self):
            return self.focus

    class WindowDelegate(AppKit.NSObject):
        def windowDidBecomeKey_(self, notification):
            i = BrowserView.get_instance('window', notification.object())

            if i and i.menu and BrowserView.current_menu != i.menu:
                BrowserView.current_menu = i.menu
                new_menu = i._recreate_menus(BrowserView.current_menu)
                BrowserView.app.setMainMenu_(new_menu)

            if not i:
                logger.error('No BrowserView instance found for windowDidBecomeKey_')

        def windowShouldClose_(self, window):
            i = BrowserView.get_instance('window', window)
            return BrowserView.should_close(i.pywebview_window)

        def windowWillClose_(self, notification):
            i = BrowserView.get_instance('window', notification.object())

            # Clear delegates before removing from instances dict so no
            # callbacks can arrive after get_instance would return None.
            i.webview.setNavigationDelegate_(None)
            i.webview.setUIDelegate_(None)

            del BrowserView.instances[i.uid]

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

            # this seems to be a bug in WkWebView, so we need to load blank html
            # see https://stackoverflow.com/questions/27410413/wkwebview-embed-video-keeps-playing-sound-after-release
            i.webview.loadHTMLString_baseURL_('', None)
            i.webview.removeFromSuperview()
            i.webview = None

            i.closed.set()
            if BrowserView.instances == {}:
                BrowserView.app.setDelegate_(None)
                BrowserView._shared_app_delegate = None
                BrowserView.app.stop_(self)
                BrowserView.app.abortModal()

        def windowDidResize_(self, notification):
            i = BrowserView.get_instance('window', notification.object())

            if i:
                size = i.window.frame().size
                i.pywebview_window.events.resized.set(size.width, size.height)

        def windowDidMiniaturize_(self, notification):
            i = BrowserView.get_instance('window', notification.object())

            if i:
                i.pywebview_window.events.minimized.set()

        def windowDidDeminiaturize_(self, notification):
            i = BrowserView.get_instance('window', notification.object())

            if i:
                i.pywebview_window.events.restored.set()

        def windowDidEnterFullScreen_(self, notification):
            i = BrowserView.get_instance('window', notification.object())
            if i:
                i.pywebview_window.events.maximized.set()

        def windowDidExitFullScreen_(self, notification):
            i = BrowserView.get_instance('window', notification.object())
            if i:
                i.pywebview_window.events.restored.set()

        def windowDidMove_(self, notification):
            i = BrowserView.get_instance('window', notification.object())
            if i:
                frame = i.window.frame()
                screen = i.window.screen().frame()
                flipped_y = screen.size.height - frame.size.height - frame.origin.y
                i.pywebview_window.events.moved.set(frame.origin.x, flipped_y)

    class JSBridge(AppKit.NSObject):
        def initWithObject_(self, window):
            super(BrowserView.JSBridge, self).init()
            self.window = window
            return self

        def userContentController_didReceiveScriptMessage_(self, controller, message):
            body = json.loads(message.body())
            if body['params'] is WebKit.WebUndefined.undefined():
                body['params'] = None
            js_bridge_call(self.window, body['funcName'], body['params'], body['id'])

    class DownloadDelegate(AppKit.NSObject):
        # Download delegate to handle links with download attribute set
        def download_decideDestinationUsingResponse_suggestedFilename_completionHandler_(
            self, download, decideDestinationUsingResponse, suggestedFilename, completionHandler
        ):
            save_dlg = AppKit.NSSavePanel.savePanel()
            directory = Foundation.NSSearchPathForDirectoriesInDomains(
                Foundation.NSDownloadsDirectory, Foundation.NSUserDomainMask, True
            )[0]
            save_dlg.setDirectoryURL_(Foundation.NSURL.fileURLWithPath_(directory))
            save_dlg.setNameFieldStringValue_(suggestedFilename)
            if save_dlg.runModal() == AppKit.NSFileHandlingPanelOKButton:
                filename = save_dlg.filename()
                url = Foundation.NSURL.fileURLWithPath_(filename)
                completionHandler(url)
            else:
                completionHandler(None)

    class BrowserDelegate(AppKit.NSObject):
        # Display a JavaScript alert panel containing the specified message
        def webView_runJavaScriptAlertPanelWithMessage_initiatedByFrame_completionHandler_(
            self, webview, message, frame, handler
        ):
            AppKit.NSRunningApplication.currentApplication().activateWithOptions_(
                AppKit.NSApplicationActivateIgnoringOtherApps
            )
            alert = AppKit.NSAlert.alloc().init()
            alert.setInformativeText_(str(message))
            alert.runModal()

            handler()

        def webView_didReceiveAuthenticationChallenge_completionHandler_(
            self, webview, challenge, handler
        ):
            # Prevent `ObjCPointerWarning: PyObjCPointer created: ... type ^{__SecTrust=}`

            # this allows any server cert
            if webview_settings['IGNORE_SSL_ERRORS'] or _state['ssl']:
                credential = AppKit.NSURLCredential.credentialForTrust_(
                    challenge.protectionSpace().serverTrust()
                )
                handler(AppKit.NSURLSessionAuthChallengeUseCredential, credential)
            else:
                handler(AppKit.NSURLSessionAuthChallengePerformDefaultHandling, nil)

        # Display a JavaScript confirm panel containing the specified message
        def webView_runJavaScriptConfirmPanelWithMessage_initiatedByFrame_completionHandler_(
            self, webview, message, frame, handler
        ):
            i = BrowserView.get_instance('webview', webview)
            ok = i.localization['global.ok']
            cancel = i.localization['global.cancel']

            result = BrowserView.display_confirmation_dialog(ok, cancel, message)
            handler(result)

        # Display a Javascript input panel
        def webView_runJavaScriptTextInputPanelWithPrompt_defaultText_initiatedByFrame_completionHandler_(
            self, webview, prompt, default_text, frame, handler
        ):
            i = BrowserView.get_instance('webview', webview)
            ok = i.localization['global.ok']
            cancel = i.localization['global.cancel']

            result = BrowserView.display_input_dialog(ok, cancel, prompt, default_text)
            handler(result)

        # Display an open panel for <input type="file"> element
        def webView_runOpenPanelWithParameters_initiatedByFrame_completionHandler_(
            self, webview, param, frame, handler
        ):
            i = list(BrowserView.instances.values())[0]
            file_filter = param._acceptedMIMETypes()

            files = i.create_file_dialog(
                FileDialog.OPEN,
                '',
                param.allowsMultipleSelection(),
                '',
                file_filter,
                main_thread=True,
            )

            if files:
                urls = [Foundation.NSURL.fileURLWithPath_(i) for i in files]
                handler(urls)
            else:
                handler(nil)

        # Open target="_blank" links in external browser
        def webView_createWebViewWithConfiguration_forNavigationAction_windowFeatures_(
            self, webview, config, action, features
        ):
            if action.navigationType() == getattr(WebKit, 'WKNavigationTypeLinkActivated', 0):
                if webview_settings['OPEN_EXTERNAL_LINKS_IN_BROWSER']:
                    webbrowser.open(action.request().URL().absoluteString(), 2, True)
                else:
                    webview.loadRequest_(action.request())
            return nil

        # WKNavigationDelegate method, invoked when a navigation decision needs to be made
        def webView_decidePolicyForNavigationAction_decisionHandler_(
            self, webview, action, handler
        ):
            # The event that might have triggered the navigation
            event = AppKit.NSApp.currentEvent()

            # Handle links with the download attribute set to recommend a file name
            if action.shouldPerformDownload() and webview_settings['ALLOW_DOWNLOADS']:
                handler(getattr(WebKit, 'WKNavigationActionPolicyDownload', 2))
                return

            """ Disable back navigation on pressing the Delete key: """
            # Check if the requested navigation action is Back/Forward
            if action.navigationType() == getattr(WebKit, 'WKNavigationTypeBackForward', 2):
                # Check if the event is a Delete key press (keyCode = 51)
                if event and event.type() == AppKit.NSKeyDown and event.keyCode() == 51:
                    # If so, ignore the request and return
                    handler(getattr(WebKit, 'WKNavigationActionPolicyCancel', 0))
                    return

            request = action.request()
            url = request.URL()

            original_headers = dict(request.allHTTPHeaderFields())
            i = BrowserView.get_instance('webview', webview)

            if (
                len(i.pywebview_window.events.request_sent) > 0
                and 'X-Handled' not in original_headers
                and str(url) != 'about:blank'
            ):
                request_ = Request(str(url), request.HTTPMethod(), original_headers)
                i.pywebview_window.events.request_sent.set(request_)
                request_.headers['X-Handled'] = 'true'

                new_request = Foundation.NSMutableURLRequest.requestWithURL_(url)
                new_request.setHTTPMethod_(request.HTTPMethod())
                new_request.setAllHTTPHeaderFields_(
                    AppKit.NSDictionary(stringify_headers(request_.headers))
                )
                new_request.setHTTPBody_(request.HTTPBody())
                new_request.setHTTPBodyStream_(request.HTTPBodyStream())
                new_request.setHTTPShouldHandleCookies_(request.HTTPShouldHandleCookies())
                new_request.setHTTPShouldUsePipelining_(request.HTTPShouldUsePipelining())
                new_request.setMainDocumentURL_(request.mainDocumentURL())
                new_request.setNetworkServiceType_(request.networkServiceType())
                webview.loadRequest_(new_request)
                handler(getattr(WebKit, 'WKNavigationActionPolicyCancel', 1))
            else:
                # Normal navigation, allow
                handler(getattr(WebKit, 'WKNavigationActionPolicyAllow', 1))

        def webView_navigationAction_didBecomeDownload_(self, webview, navigationAction, download):
            download.setDelegate_(BrowserView.DownloadDelegate.alloc().init().retain())

        def webView_decidePolicyForNavigationResponse_decisionHandler_(
            self, webview, navigationResponse, decisionHandler
        ):
            if navigationResponse.canShowMIMEType():
                response_ = navigationResponse.response()
                headers = dict(response_.allHeaderFields())
                response = Response(
                    response_.URL().absoluteString(), response_.statusCode(), headers
                )
                webview.pywebview_window.events.response_received.set(response)

                decisionHandler(WebKit.WKNavigationResponsePolicyAllow)
            elif webview_settings['ALLOW_DOWNLOADS']:
                decisionHandler(WebKit.WKNavigationResponsePolicyCancel)

                save_filename = navigationResponse.response().suggestedFilename()

                save_dlg = AppKit.NSSavePanel.savePanel()
                save_dlg.setTitle_(webview.pywebview_window.localization['global.saveFile'])
                directory = Foundation.NSSearchPathForDirectoriesInDomains(
                    Foundation.NSDownloadsDirectory, Foundation.NSUserDomainMask, True
                )[0]
                save_dlg.setDirectoryURL_(Foundation.NSURL.fileURLWithPath_(directory))

                if save_filename:  # set file name
                    save_dlg.setNameFieldStringValue_(save_filename)

                if save_dlg.runModal() == AppKit.NSFileHandlingPanelOKButton:
                    self._file_name = save_dlg.filename()
                    dataTask = Foundation.NSURLSession.sharedSession().downloadTaskWithURL_completionHandler_(
                        navigationResponse.response().URL(), self.download_completionHandler_error_
                    )
                    dataTask.resume()
                else:
                    self._file_name = None
            else:
                decisionHandler(WebKit.WKNavigationResponsePolicyCancel)

        def download_completionHandler_error_(self, temporaryLocation, response, error):
            if error is not None:
                logger.error('Download error:', error)
            else:
                if temporaryLocation is not None:
                    destinationURL = Foundation.NSURL.fileURLWithPath_(self._file_name)
                    fileManager = Foundation.NSFileManager.defaultManager()
                    try:
                        fileManager.moveItemAtURL_toURL_error_(
                            temporaryLocation, destinationURL, None
                        )
                    except Foundation.NSError as moveError:
                        logger.exception(moveError)

        # Show the webview when it finishes loading
        def webView_didFinishNavigation_(self, webview, nav):
            # Add the webview to the window if it's not yet the contentView
            i = BrowserView.get_instance('webview', webview)

            if i:
                if not webview.window():
                    i.window.setContentView_(webview)
                    i.window.makeFirstResponder_(webview)

                inject_pywebview('cocoa', i.js_bridge.window)

                if _state['debug'] and webview_settings['OPEN_DEVTOOLS_IN_DEBUG']:
                    BrowserView._open_web_inspector(webview)

        # Handle JavaScript window.print()
        def userContentController_didReceiveScriptMessage_(self, controller, message):
            if message.body() == 'print':
                i = BrowserView.get_instance('_browserDelegate', self)
                BrowserView.print_webview(i.webview)

    class FileFilterChooser(AppKit.NSPopUpButton):
        def initWithFilter_(self, file_filter):
            super(BrowserView.FileFilterChooser, self).init()
            self.filter = file_filter

            self.addItemsWithTitles_([i[0] for i in self.filter])
            self.setAction_('onChange:')
            self.setTarget_(self)
            return self

        def setFileDialog_(self, file_dlg):
            self.file_dlg = file_dlg

        def onChange_(self, sender):
            option = sender.indexOfSelectedItem()
            self.file_dlg.setAllowedFileTypes_(self.filter[option][1])

    class WebKitHost(WebKit.WKWebView):
        def performDragOperation_(self, sender):
            if sender.draggingSource() is None and _dnd_state['num_listeners'] > 0:
                pboard = sender.draggingPasteboard()
                classes = [AppKit.NSURL]
                options = {
                    AppKit.NSPasteboardURLReadingFileURLsOnlyKey: AppKit.NSNumber.numberWithBool_(
                        True
                    )
                }
                urls = pboard.readObjectsForClasses_options_(classes, options) or []
                files = [
                    (
                        os.path.basename(os.path.dirname(file_path))
                        if os.path.isdir(file_path)
                        else os.path.basename(file_path),
                        file_path,
                    )
                    for url in urls
                    for file_path in [
                        urllib.parse.unquote(
                            url.filePathURL().absoluteString().replace('file://', '')
                        )
                    ]
                    if os.path.isdir(file_path) or os.path.isfile(file_path)
                ]

                _dnd_state['paths'] += files

            return super(BrowserView.WebKitHost, self).performDragOperation_(sender)

        def addSubview_(self, event):
            # translate the event from the web coordinate space into the window coordinate space
            # basically flip the y axis since the WKWebView/WebKitHost implements a flipped view
            # (but seems to miss this flip back when adding a subview)
            event_frame = event.frame()
            flipped_y = self.frame().size.height - (event_frame.origin.y + event_frame.size.height)
            event.setFrameOrigin_(AppKit.NSPoint(event_frame.origin.x, flipped_y))

            super(BrowserView.WebKitHost, self).addSubview_(event)

        def mouseDown_(self, event):
            i = BrowserView.get_instance('webview', self)
            window = self.window()

            if i.frameless and i.easy_drag:
                windowFrame = window.frame()
                if windowFrame is None:
                    raise RuntimeError('Failed to obtain screen')

                self.initialLocation = window.convertBaseToScreen_(event.locationInWindow())
                self.initialLocation.x -= windowFrame.origin.x
                self.initialLocation.y -= windowFrame.origin.y

            super(BrowserView.WebKitHost, self).mouseDown_(event)

        def mouseDragged_(self, event):
            i = BrowserView.get_instance('webview', self)
            window = self.window()

            if i.frameless and i.easy_drag:
                screenFrame = i.screen
                if screenFrame is None:
                    raise RuntimeError('Failed to obtain screen')

                windowFrame = window.frame()
                if windowFrame is None:
                    raise RuntimeError('Failed to obtain frame')

                currentLocation = window.convertBaseToScreen_(
                    window.mouseLocationOutsideOfEventStream()
                )
                newOrigin = AppKit.NSMakePoint(
                    (currentLocation.x - self.initialLocation.x),
                    (currentLocation.y - self.initialLocation.y),
                )
                if (newOrigin.y + windowFrame.size.height) > (
                    screenFrame.origin.y + screenFrame.size.height
                ):
                    newOrigin.y = screenFrame.origin.y + (
                        screenFrame.size.height + windowFrame.size.height
                    )
                window.setFrameOrigin_(newOrigin)

            if event.modifierFlags() & getattr(AppKit, 'NSEventModifierFlagControl', 1 << 18):
                i = BrowserView.get_instance('webview', self)
                if not _state['debug']:
                    return

            super(BrowserView.WebKitHost, self).mouseDragged_(event)

        def willOpenMenu_withEvent_(self, menu, event):
            if not _state['debug']:
                menu.removeAllItems()

        def keyDown_(self, event):
            if event.modifierFlags() & AppKit.NSCommandKeyMask:
                responder = self.window().firstResponder()
                if responder is not None:
                    range_ = responder.selectedRange()
                    hasSelectedText = len(range_) > 0

                    char = event.characters()

                    if char == 'x' and hasSelectedText:  # cut
                        responder.cut_(self)
                        return
                    elif char == 'c' and hasSelectedText:  # copy
                        responder.copy_(self)
                        return
                    elif char == 'v':  # paste
                        responder.paste_(self)
                        return
                    elif char == 'a':  # select all
                        responder.selectAll_(self)
                        return
                    elif char == 'z':  # undo
                        if responder.undoManager().canUndo():
                            responder.undoManager().undo()
                        return
                    elif char == 'q':  # quit
                        BrowserView.app.stop_(self)
                        return
                    elif char == 'w':  # close
                        self.window().performClose_(event)
                        return

            super(BrowserView.WebKitHost, self).keyDown_(event)

    def __init__(self, window):
        BrowserView.instances[window.uid] = self
        self.uid = window.uid
        self.pywebview_window = window

        self.js_bridge = None
        self._file_name = None
        self._file_name_semaphore = Semaphore(0)
        self._current_url_semaphore = Semaphore(0)
        self.closed = window.events.closed
        self.closing = window.events.closing
        self.shown = window.events.shown
        self.loaded = window.events.loaded
        self.confirm_close = window.confirm_close
        self.title = window.title
        self.is_fullscreen = False
        self.hidden = window.hidden
        self.minimized = window.minimized
        self.maximized = window.maximized
        self.localization = window.localization

        if window.screen:
            self.screen = window.screen.frame
        else:
            self.screen = AppKit.NSScreen.mainScreen().frame()

        rect = AppKit.NSMakeRect(0.0, 0.0, window.initial_width, window.initial_height)
        window_mask = (
            AppKit.NSTitledWindowMask
            | AppKit.NSClosableWindowMask
            | AppKit.NSMiniaturizableWindowMask
        )

        if window.resizable:
            window_mask = window_mask | AppKit.NSResizableWindowMask

        if window.frameless:
            window_mask = (
                window_mask
                | NSFullSizeContentViewWindowMask
                | AppKit.NSTexturedBackgroundWindowMask
            )

        self.menu = window.menu or _state['menu']

        self.window = BrowserView.WindowHost.alloc().initWithContentRect_styleMask_backing_defer_(
            rect, window_mask, AppKit.NSBackingStoreBuffered, False
        )
        self.window.setReleasedWhenClosed_(False)
        self.pywebview_window.native = self.window

        self.window.focus = window.focus
        self.window.setTitle_(window.title)
        self.window.setMinSize_(AppKit.NSSize(window.min_size[0], window.min_size[1]))
        self.window.setAnimationBehavior_(AppKit.NSWindowAnimationBehaviorDocumentWindow)
        BrowserView.cascade_loc = self.window.cascadeTopLeftFromPoint_(BrowserView.cascade_loc)

        frame = self.window.frame()
        frame.size.width = window.initial_width
        frame.size.height = window.initial_height
        self.window.setFrame_display_(frame, True)

        config = WebKit.WKWebViewConfiguration.alloc().init()
        self.webview = BrowserView.WebKitHost.alloc().initWithFrame_configuration_(rect, config)
        self.webview.pywebview_window = window

        self._browserDelegate = BrowserView.BrowserDelegate.alloc().init()
        self._windowDelegate = BrowserView.WindowDelegate.alloc().init()

        if BrowserView._shared_app_delegate is None:
            BrowserView._shared_app_delegate = BrowserView.AppDelegate.alloc().init()
        BrowserView.app.setDelegate_(BrowserView._shared_app_delegate)
        self.webview.setUIDelegate_(self._browserDelegate)
        self.webview.setNavigationDelegate_(self._browserDelegate)
        self.window.setDelegate_(self._windowDelegate)

        config.userContentController().addScriptMessageHandler_name_(
            self._browserDelegate, 'browserDelegate'
        )

        self.datastore = WebKit.WKWebsiteDataStore.defaultDataStore()

        if _state['icon'] and os.path.isfile(_state['icon']):
            ns_image = AppKit.NSImage.alloc().initByReferencingFile_(_state['icon'])
            BrowserView.app.setApplicationIconImage_(ns_image)

        if _state['private_mode']:
            # nonPersisentDataStore preserves cookies for some unknown reason. For this reason we use default datastore
            # and clear all the cookies beforehand

            def dummy_completion_handler():
                pass

            data_types = WebKit.WKWebsiteDataStore.allWebsiteDataTypes()
            from_start = WebKit.NSDate.dateWithTimeIntervalSince1970_(0)
            config.setWebsiteDataStore_(self.datastore)
            self.datastore.removeDataOfTypes_modifiedSince_completionHandler_(
                data_types, from_start, dummy_completion_handler
            )
        else:
            config.setWebsiteDataStore_(self.datastore)

        try:
            config.preferences().setValue_forKey_(False, 'backspaceKeyNavigationEnabled')
        except KeyError:
            pass  # backspaceKeyNavigationEnabled does not exist prior to macOS Mojave

        config.preferences().setValue_forKey_(
            webview_settings['ALLOW_FILE_URLS'], 'allowFileAccessFromFileURLs'
        )

        if _state['debug']:
            config.preferences().setValue_forKey_(True, 'developerExtrasEnabled')

        self.js_bridge = BrowserView.JSBridge.alloc().initWithObject_(window)
        config.userContentController().addScriptMessageHandler_name_(self.js_bridge, 'jsBridge')

        user_agent = webview_settings.get('user_agent') or _state['user_agent']
        if user_agent:
            self.webview.setCustomUserAgent_(user_agent)

        self.window.setFrameOrigin_(self.screen.origin)

        if window.initial_x is not None and window.initial_y is not None:
            self.move(window.initial_x, window.initial_y)
        else:
            self.center()

        if window.transparent:
            self.window.setOpaque_(False)
            self.window.setHasShadow_(False)
            self.window.setBackgroundColor_(
                BrowserView.nscolor_from_hex(window.background_color, 0)
            )
            self.webview.setValue_forKey_(True, 'drawsTransparentBackground')
        else:
            self.window.setBackgroundColor_(BrowserView.nscolor_from_hex(window.background_color))

        if window.vibrancy:
            frame_vibrancy = AppKit.NSMakeRect(0, 0, frame.size.width, frame.size.height)
            visualEffectView = AppKit.NSVisualEffectView.new()
            visualEffectView.setAutoresizingMask_(
                AppKit.NSViewWidthSizable | AppKit.NSViewHeightSizable
            )
            visualEffectView.setWantsLayer_(True)
            visualEffectView.setFrame_(frame_vibrancy)
            visualEffectView.setState_(AppKit.NSVisualEffectStateActive)
            visualEffectView.setBlendingMode_(AppKit.NSVisualEffectBlendingModeBehindWindow)
            self.webview.addSubview_positioned_relativeTo_(
                visualEffectView, AppKit.NSWindowBelow, self.webview
            )

        self.frameless = window.frameless
        self.easy_drag = window.easy_drag

        if window.frameless:
            # Make content full size and titlebar transparent
            self.window.setTitlebarAppearsTransparent_(True)
            self.window.setTitleVisibility_(NSWindowTitleHidden)
            self.window.standardWindowButton_(AppKit.NSWindowCloseButton).setHidden_(True)
            self.window.standardWindowButton_(AppKit.NSWindowMiniaturizeButton).setHidden_(True)
            self.window.standardWindowButton_(AppKit.NSWindowZoomButton).setHidden_(True)
        else:
            # Set the titlebar color (so that it does not change with the window color)
            self.window.contentView().superview().subviews().lastObject().setBackgroundColor_(
                AppKit.NSColor.windowBackgroundColor()
            )

        if window.on_top:
            self.window.setLevel_(AppKit.NSStatusWindowLevel)

        self.pywebview_window.events.before_show.set()

        if window.real_url:
            self.url = window.real_url
            self.load_url(window.real_url)
        elif window.html:
            self.load_html(window.html, '')
        else:
            self.load_html(DEFAULT_HTML, '')
        if window.fullscreen:
            self.toggle_fullscreen()

    def first_show(self):
        if not self.hidden:
            self.window.makeKeyAndOrderFront_(self.window)

        if self.maximized:
            self.maximize()
        elif self.minimized:
            self.minimize()

        self.shown.set()

        if not BrowserView.app.isRunning():
            new_menu = self._recreate_menus(self.menu)
            BrowserView.app.setMainMenu_(new_menu)

            BrowserView.app.activateIgnoringOtherApps_(Foundation.YES)
            AppHelper.installMachInterrupt()
            BrowserView.app.run()

    def show(self):
        def _show():
            self.window.makeKeyAndOrderFront_(self.window)
            BrowserView.app.activateIgnoringOtherApps_(Foundation.YES)

        AppHelper.callAfter(_show)

    def hide(self):
        AppHelper.callAfter(self.window.orderOut_, self.window)

    def destroy(self):
        AppHelper.callAfter(self.window.close)

    def set_title(self, title):
        AppHelper.callAfter(self.window.setTitle_, title)

    def toggle_fullscreen(self):
        def toggle():
            if self.is_fullscreen:
                window_behaviour = 1 << 2  # NSWindowCollectionBehaviorManaged
            else:
                window_behaviour = 1 << 7  # NSWindowCollectionBehaviorFullScreenPrimary

            self.window.setCollectionBehavior_(window_behaviour)
            self.window.toggleFullScreen_(None)

        AppHelper.callAfter(toggle)
        self.is_fullscreen = not self.is_fullscreen

    def resize(self, width, height, fix_point):
        def _resize():
            frame = self.window.frame()

            if fix_point and fix_point & FixPoint.EAST:
                # Keep the right of the window in the same place
                frame.origin.x += frame.size.width - width

            if fix_point and fix_point & FixPoint.NORTH:
                # Keep the top of the window in the same place
                frame.origin.y += frame.size.height - height

            frame.size.width = width
            frame.size.height = height

            if not fix_point:  # used in maximized
                frame.origin.x = 0
                frame.origin.y = 0

            self.window.setFrame_display_(frame, True)

        AppHelper.callAfter(_resize)

    def maximize(self):
        width = self.screen.size.width
        height = self.screen.size.height
        self.resize(width, height, None)

    def minimize(self):
        AppHelper.callAfter(self.window.miniaturize_, self)

    def restore(self):
        AppHelper.callAfter(self.window.deminiaturize_, self)

    def move(self, x, y):
        flipped_y = self.screen.size.height - y
        self.window.setFrameTopLeftPoint_(
            AppKit.NSPoint(self.screen.origin.x + x, self.screen.origin.y + flipped_y)
        )

    def center(self):
        window_frame = self.window.frame()

        window_frame.origin.x = (
            self.screen.origin.x + (self.screen.size.width - window_frame.size.width) / 2
        )
        window_frame.origin.y = (
            self.screen.origin.y + (self.screen.size.height - window_frame.size.height) / 2
        )

        self.window.setFrameOrigin_(window_frame.origin)

    def clear_cookies(self):
        def clear():
            self.datastore.removeDataOfTypes_modifiedSince_completionHandler_(
                WebKit.WKWebsiteDataStore.allWebsiteDataTypes(),
                Foundation.NSDate.dateWithTimeIntervalSince1970_(0),
                lambda: None,
            )

        AppHelper.callAfter(clear)

    def get_cookies(self):
        def handler(cookies):
            for c in cookies:
                domain = c.domain()[1:] if c.domain().startswith('.') else c.domain()
                if domain not in self.url:
                    continue

                data = {
                    'name': c.name(),
                    'value': c.value(),
                    'path': c.path(),
                    'domain': c.domain(),
                    'expires': c.expiresDate(),
                    'secure': c.isSecure(),
                    'httponly': c.isHTTPOnly(),
                    'samesite': c.SameSitePolicy(),
                }

                cookie = create_cookie(data)
                _cookies.append(cookie)

            cookie_semaphore.release()

        _cookies = []
        AppHelper.callAfter(self.datastore.httpCookieStore().getAllCookies_, handler)
        cookie_semaphore = Semaphore(0)
        cookie_semaphore.acquire()

        return _cookies

    def get_current_url(self):
        def get():
            self._current_url = str(self.webview.URL())
            self._current_url_semaphore.release()

        AppHelper.callAfter(get)

        self._current_url_semaphore.acquire()
        return None if self._current_url == 'about:blank' else self._current_url

    def load_url(self, url):
        def load(url):
            page_url = Foundation.NSURL.URLWithString_(BrowserView.quote(url))
            req = Foundation.NSURLRequest.requestWithURL_(page_url)
            self.webview.loadRequest_(req)

        self.url = url
        AppHelper.callAfter(load, url)

    def load_html(self, content, base_uri):
        def load(content, url):
            url = Foundation.NSURL.URLWithString_(BrowserView.quote(url))
            self.webview.loadHTMLString_baseURL_(content, url)

        AppHelper.callAfter(load, content, base_uri)

    def evaluate_js(self, script, parse_json):
        def eval():
            self.webview.evaluateJavaScript_completionHandler_(script, handler)

        def handler(result, error):
            if parse_json and result:
                try:
                    JSResult.result = json.loads(result)
                except Exception:
                    logger.exception('Failed to parse JSON: ' + result)
                    JSResult.result = result
            else:
                JSResult.result = result

            JSResult.result_semaphore.release()

        class JSResult:
            result = None
            result_semaphore = Semaphore(0)

        AppHelper.callAfter(eval)
        JSResult.result_semaphore.acquire()
        return JSResult.result

    def create_file_dialog(
        self, dialog_type, directory, allow_multiple, save_filename, file_filter, main_thread=False
    ):
        def create_dialog(*args):
            dialog_type = args[0]

            if dialog_type == FileDialog.SAVE:
                save_filename = args[2]

                save_dlg = AppKit.NSSavePanel.savePanel()
                save_dlg.setTitle_(self.localization['global.saveFile'])

                if directory:  # set initial directory
                    save_dlg.setDirectoryURL_(Foundation.NSURL.fileURLWithPath_(directory))

                if save_filename:  # set file name
                    save_dlg.setNameFieldStringValue_(save_filename)

                if save_dlg.runModal() == AppKit.NSFileHandlingPanelOKButton:
                    self._file_name = save_dlg.filename()
                else:
                    self._file_name = None
            else:
                allow_multiple = args[1]

                open_dlg = AppKit.NSOpenPanel.openPanel()

                # Enable the selection of files in the dialog.
                open_dlg.setCanChooseFiles_(dialog_type != FileDialog.FOLDER)

                # Enable the selection of directories in the dialog.
                open_dlg.setCanChooseDirectories_(dialog_type == FileDialog.FOLDER)

                # Enable creating new folders in folder dialogs (macOS)
                if dialog_type == FileDialog.FOLDER:
                    open_dlg.setCanCreateDirectories_(True)

                # Enable / disable multiple selection
                open_dlg.setAllowsMultipleSelection_(allow_multiple)

                # Set allowed file extensions
                if file_filter:
                    if isinstance(file_filter, Foundation.WKNSArray):
                        try:
                            import UniformTypeIdentifiers

                            UTType = UniformTypeIdentifiers.UTType
                        except ImportError:
                            UTType = None  # Fallback if UTType is not available

                        if UTType is not None:
                            # Map wildcard MIME types to supertype UTIs because
                            # typeWithMIMEType_ returns unusable dynamic types for them.
                            _wildcard_uti = {
                                'image/*': 'public.image',
                                'video/*': 'public.movie',
                                'audio/*': 'public.audio',
                                'text/*': 'public.text',
                            }
                            content_types = []
                            for mime in file_filter:
                                mime_str = str(mime)
                                uti_id = _wildcard_uti.get(mime_str)
                                if uti_id:
                                    ut = UTType.typeWithIdentifier_(uti_id)
                                else:
                                    ut = UTType.typeWithMIMEType_(mime_str)
                                if ut is not None and not str(ut).startswith('dyn.'):
                                    content_types.append(ut)
                            if content_types:
                                open_dlg.setAllowedContentTypes_(content_types)
                    else:
                        open_dlg.setAllowedFileTypes_(file_filter[0][1])

                        if len(file_filter) > 1:
                            filter_chooser = BrowserView.FileFilterChooser.alloc().initWithFilter_(
                                file_filter
                            )
                            filter_chooser.setFileDialog_(open_dlg)
                            open_dlg.setAccessoryView_(filter_chooser)
                            open_dlg.setAccessoryViewDisclosed_(True)

                if directory:  # set initial directory
                    open_dlg.setDirectoryURL_(Foundation.NSURL.fileURLWithPath_(directory))

                if open_dlg.runModal() == AppKit.NSFileHandlingPanelOKButton:
                    files = open_dlg.filenames()
                    self._file_name = tuple(files)
                else:
                    self._file_name = None

            if not main_thread:
                self._file_name_semaphore.release()

        if main_thread:
            create_dialog(dialog_type, allow_multiple, save_filename)
        else:
            AppHelper.callAfter(create_dialog, dialog_type, allow_multiple, save_filename)
            self._file_name_semaphore.acquire()

        return self._file_name

    def _recreate_menus(self, user_menu):
        main_menu = self._clear_main_menu()

        # Filter out app menu items (menus with title '__app__')
        app_menu_items = None
        regular_menus = None

        if user_menu:
            app_menu_items = []
            regular_menus = []

            for menu in user_menu:
                if isinstance(menu, Menu) and menu.title == '__app__':
                    # This is an app menu - extract its items
                    app_menu_items.extend(menu.items)
                else:
                    # Regular menu
                    regular_menus.append(menu)

            # If no app menu items were found, set to None
            if not app_menu_items:
                app_menu_items = None

            # If no regular menus, set to None
            if not regular_menus:
                regular_menus = None

        self._add_app_menu(main_menu, app_menu_items)

        if webview_settings['SHOW_DEFAULT_MENUS']:
            self._add_view_menu(main_menu)
            self._add_edit_menu(main_menu)

        self._add_custom_menu(main_menu, regular_menus)

        return main_menu

    def _clear_main_menu(self):
        """
        Remove all items from the main menu or create a new one if it doesn't exist.
        """
        mainMenu = BrowserView.app.mainMenu()
        if mainMenu:
            mainMenu.removeAllItems()

        mainMenu = AppKit.NSMenu.alloc().init()

        return mainMenu

    def _add_app_menu(self, mainMenu, custom_items=None):
        """
        Create a default Cocoa menu that shows 'Services', 'Hide',
        'Hide Others', 'Show All', and 'Quit'. Will append the application name
        to some menu items if it's available.
        """

        # Create an application menu and make it a submenu of the main menu
        mainAppMenuItem = AppKit.NSMenuItem.alloc().init()
        # The application menu is the first item, so add this menu ast the first item
        mainMenu.insertItem_atIndex_(mainAppMenuItem, 0)
        appMenu = AppKit.NSMenu.alloc().init()
        mainAppMenuItem.setSubmenu_(appMenu)

        appMenu.addItemWithTitle_action_keyEquivalent_(
            self._append_app_name(self.localization['cocoa.menu.about']),
            'orderFrontStandardAboutPanel:',
            '',
        )

        # Add custom app menu items if provided (between About and Services)
        if custom_items:
            appMenu.addItem_(AppKit.NSMenuItem.separatorItem())
            self._process_menu_items(custom_items, appMenu)

        appMenu.addItem_(AppKit.NSMenuItem.separatorItem())

        # Set the 'Services' menu for the app and create an app menu item
        appServicesMenu = AppKit.NSMenu.alloc().init()
        BrowserView.app.setServicesMenu_(appServicesMenu)
        servicesMenuItem = appMenu.addItemWithTitle_action_keyEquivalent_(
            self.localization['cocoa.menu.services'], nil, ''
        )
        servicesMenuItem.setSubmenu_(appServicesMenu)

        appMenu.addItem_(AppKit.NSMenuItem.separatorItem())

        # Append the 'Hide', 'Hide Others', and 'Show All' menu items
        appMenu.addItemWithTitle_action_keyEquivalent_(
            self._append_app_name(self.localization['cocoa.menu.hide']), 'hide:', 'h'
        )
        hideOthersMenuItem = appMenu.addItemWithTitle_action_keyEquivalent_(
            self.localization['cocoa.menu.hideOthers'], 'hideOtherApplications:', 'h'
        )
        hideOthersMenuItem.setKeyEquivalentModifierMask_(
            AppKit.NSAlternateKeyMask | AppKit.NSCommandKeyMask
        )
        appMenu.addItemWithTitle_action_keyEquivalent_(
            self.localization['cocoa.menu.showAll'], 'unhideAllApplications:', ''
        )

        appMenu.addItem_(AppKit.NSMenuItem.separatorItem())

        # Append a 'Quit' menu item
        appMenu.addItemWithTitle_action_keyEquivalent_(
            self._append_app_name(self.localization['cocoa.menu.quit']), 'terminate:', 'q'
        )

    def _add_view_menu(self, mainMenu):
        """
        Create a default View menu that shows 'Enter Full Screen'.
        """

        # Create an View menu and make it a submenu of the main menu
        viewMenu = AppKit.NSMenu.alloc().init()
        viewMenu.setTitle_(self.localization['cocoa.menu.view'])
        viewMenuItem = AppKit.NSMenuItem.alloc().init()
        viewMenuItem.setSubmenu_(viewMenu)
        # Make the view menu the first item after the application menu
        mainMenu.insertItem_atIndex_(viewMenuItem, 1)

        # TODO: localization of the Enter fullscreen string has no effect
        fullScreenMenuItem = viewMenu.addItemWithTitle_action_keyEquivalent_(
            self.localization['cocoa.menu.fullscreen'], 'toggleFullScreen:', 'f'
        )
        fullScreenMenuItem.setKeyEquivalentModifierMask_(
            AppKit.NSControlKeyMask | AppKit.NSCommandKeyMask
        )

    def _add_edit_menu(self, mainMenu):
        """
        Create a default Edit menu that shows Copy/Paste/etc.
        """
        # Create an Edit menu and make it a submenu of the main menu
        editMenu = AppKit.NSMenu.alloc().init()
        editMenu.setTitle_(self.localization['cocoa.menu.edit'])
        editMenuItem = AppKit.NSMenuItem.alloc().init()
        editMenuItem.setSubmenu_(editMenu)
        # Make the edit menu the first item after the application menu
        mainMenu.insertItem_atIndex_(editMenuItem, 1)

        for title, action, keyEquivalent in [
            (self.localization['cocoa.menu.cut'], 'cut:', 'x'),
            (self.localization['cocoa.menu.copy'], 'copy:', 'c'),
            (self.localization['cocoa.menu.paste'], 'paste:', 'v'),
            (self.localization['cocoa.menu.selectAll'], 'selectAll:', 'a'),
        ]:
            editMenu.addItemWithTitle_action_keyEquivalent_(title, action, keyEquivalent)

    def _process_menu_items(self, menu_items, parent_menu):
        """
        Process menu items and add them to the parent menu.
        Used for both custom menus and app menu items.
        """
        for item in menu_items:
            if isinstance(item, MenuSeparator):
                parent_menu.addItem_(AppKit.NSMenuItem.separatorItem())
            elif isinstance(item, MenuAction):
                # Actions must be registered before application start. Otherwise they are disabled.
                # Menu handler is a workaround to register actions after application start
                random_id = str(uuid.uuid4())[:6]
                # Handle functools.partial objects which don't have __name__ attribute
                if hasattr(item.function, '__name__'):
                    func_name = item.function.__name__
                elif hasattr(item.function, 'func') and hasattr(item.function.func, '__name__'):
                    func_name = item.function.func.__name__
                else:
                    func_name = 'anonymous_function'
                action_id = func_name + '.' + random_id
                menu_handler.register_action(action_id, item.function)

                menu_item = AppKit.NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
                    item.title, 'handleMenuAction:', ''
                )
                menu_item.setTarget_(menu_handler)
                menu_item.setRepresentedObject_(action_id)
                parent_menu.addItem_(menu_item)
            elif isinstance(item, Menu):
                submenu = AppKit.NSMenu.alloc().init()
                submenu.setTitle_(item.title)
                menu_item = AppKit.NSMenuItem.alloc().init()
                menu_item.setTitle_(item.title)
                menu_item.setSubmenu_(submenu)
                parent_menu.addItem_(menu_item)

                self._process_menu_items(item.items, submenu)

    def _add_custom_menu(self, mainMenu, app_menu_list):
        """
        Create a custom menu for the app menu (MacOS bar menu)
        """

        if app_menu_list is None:
            return

        for app_menu in app_menu_list:
            submenu = AppKit.NSMenu.alloc().init()
            submenu.setTitle_(app_menu.title)
            menu_item = AppKit.NSMenuItem.alloc().init()
            menu_item.setTitle_(app_menu.title)
            menu_item.setSubmenu_(submenu)
            mainMenu.addItem_(menu_item)
            self._process_menu_items(app_menu.items, submenu)

    def _append_app_name(self, val):
        """
        Append the application name to a string if it's available. If not, the
        string is returned unchanged.

        :param str val: The string to append to
        :return: String with app name appended, or unchanged string
        :rtype: str
        """
        if 'CFBundleName' in info:
            val += ' {}'.format(info['CFBundleName'])
        return val

    @staticmethod
    def nscolor_from_hex(hex_string, alpha=1.0):
        """
        Convert given hex color to NSColor.

        :hex_string: Hex code of the color as #RGB or #RRGGBB
        """

        hex_string = hex_string[1:]  # Remove leading hash
        if len(hex_string) == 3:
            hex_string = ''.join([c * 2 for c in hex_string])  # 3-digit to 6-digit

        hex_int = int(hex_string, 16)
        rgb = (
            (hex_int >> 16) & 0xFF,  # Red byte
            (hex_int >> 8) & 0xFF,  # Blue byte
            (hex_int) & 0xFF,  # Green byte
        )
        rgb = [i / 255.0 for i in rgb]  # Normalize to range(0.0, 1.0)

        return AppKit.NSColor.colorWithSRGBRed_green_blue_alpha_(rgb[0], rgb[1], rgb[2], alpha)

    @staticmethod
    def get_instance(attr, value):
        """
        Return a BrowserView instance by the :value of its given :attribute,
        and None if no match is found.
        """
        for i in list(BrowserView.instances.values()):
            try:
                if getattr(i, attr) == value:
                    return i
            except AttributeError:
                break

        return None

    @staticmethod
    def display_confirmation_dialog(first_button, second_button, message):
        AppKit.NSApplication.sharedApplication()
        AppKit.NSRunningApplication.currentApplication().activateWithOptions_(
            AppKit.NSApplicationActivateIgnoringOtherApps
        )
        alert = AppKit.NSAlert.alloc().init()
        alert.addButtonWithTitle_(first_button)
        alert.addButtonWithTitle_(second_button)
        alert.setMessageText_(message)
        alert.setAlertStyle_(AppKit.NSWarningAlertStyle)

        return alert.runModal() == AppKit.NSAlertFirstButtonReturn

    @staticmethod
    def display_input_dialog(first_button, second_button, prompt, default_text):
        AppKit.NSApplication.sharedApplication()
        AppKit.NSRunningApplication.currentApplication().activateWithOptions_(
            AppKit.NSApplicationActivateIgnoringOtherApps
        )
        alert = AppKit.NSAlert.alloc().init()
        text_field = AppKit.NSTextField.alloc().initWithFrame_(AppKit.NSMakeRect(0, 0, 240, 24))
        text_field.cell().setScrollable_(True)
        text_field.setStringValue_(default_text)
        alert.setAccessoryView_(text_field)
        alert.addButtonWithTitle_(first_button)
        alert.addButtonWithTitle_(second_button)
        alert.setMessageText_(prompt)
        alert.setAlertStyle_(AppKit.NSWarningAlertStyle)

        if alert.runModal() == AppKit.NSAlertFirstButtonReturn:
            return text_field.stringValue()
        else:
            return None

    @staticmethod
    def should_close(window):
        quit = window.localization['global.quit']
        cancel = window.localization['global.cancel']
        msg = window.localization['global.quitConfirmation']

        should_cancel = window.events.closing.set()
        if should_cancel:
            return Foundation.NO
        if not window.confirm_close or BrowserView.display_confirmation_dialog(quit, cancel, msg):
            return Foundation.YES
        return Foundation.NO

    @staticmethod
    def print_webview(webview):
        info = AppKit.NSPrintInfo.sharedPrintInfo().copy()

        # default print settings used by Safari
        info.setHorizontalPagination_(AppKit.NSFitPagination)
        info.setHorizontallyCentered_(Foundation.NO)
        info.setVerticallyCentered_(Foundation.NO)

        imageableBounds = info.imageablePageBounds()
        paperSize = info.paperSize()
        if Foundation.NSWidth(imageableBounds) > paperSize.width:
            imageableBounds.origin.x = 0
            imageableBounds.size.width = paperSize.width
        if Foundation.NSHeight(imageableBounds) > paperSize.height:
            imageableBounds.origin.y = 0
            imageableBounds.size.height = paperSize.height

        info.setBottomMargin_(Foundation.NSMinY(imageableBounds))
        info.setTopMargin_(
            paperSize.height
            - Foundation.NSMinY(imageableBounds)
            - Foundation.NSHeight(imageableBounds)
        )
        info.setLeftMargin_(Foundation.NSMinX(imageableBounds))
        info.setRightMargin_(
            paperSize.width
            - Foundation.NSMinX(imageableBounds)
            - Foundation.NSWidth(imageableBounds)
        )

        # show the print panel
        print_op = webview._printOperationWithPrintInfo_(info)
        print_op.runOperationModalForWindow_delegate_didRunSelector_contextInfo_(
            webview.window(), nil, nil, nil
        )

    @staticmethod
    def _open_web_inspector(webview):
        """
        Programmatically open the Web Inspector for the given WKWebView.
        Uses private WebKit APIs that may not work on all macOS versions.
        """
        try:
            if hasattr(webview, '_inspector'):
                inspector = webview._inspector()
                if inspector and hasattr(inspector, 'show'):
                    inspector.show()
                    return True

            return False

        except Exception:
            return False

    @staticmethod
    def quote(string):
        return string.replace(' ', '%20')


class MenuHandler:
    def __init__(self):
        self.actions = {}

    def handleMenuAction_(self, sender):
        action_id = sender.representedObject()
        if action_id in self.actions:
            Thread(target=self.actions[action_id]).start()
        else:
            logger.warning(f'Menu cction {action_id} not found')

    def register_action(self, action_id, action_callable):
        self.actions[action_id] = action_callable


menu_handler = MenuHandler()


def setup_app():
    pass


def get_active_window():
    active_window = BrowserView.app.keyWindow()
    if active_window is None:
        return None

    active_window_number = active_window.windowNumber()

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

    return None


def create_window(window):
    def create():
        browser = BrowserView(window)
        browser.first_show()

    if window.uid == 'master':
        main_thread().pydev_do_not_trace = True  # vs code debugger hang fix
        create()

    else:
        AppHelper.callAfter(create)


def set_title(title, uid):
    i = BrowserView.instances.get(uid)
    if i:
        i.set_title(title)


def create_confirmation_dialog(title, message, uid):
    def _confirm():
        nonlocal result

        i = BrowserView.instances.get(uid)
        ok = i.localization['global.ok']
        cancel = i.localization['global.cancel']

        result = BrowserView.display_confirmation_dialog(ok, cancel, message)
        semaphore.release()

    result = False

    semaphore = Semaphore(0)
    AppHelper.callAfter(_confirm)
    semaphore.acquire()

    return result


def create_file_dialog(dialog_type, directory, allow_multiple, save_filename, file_types, uid):
    file_filter = []

    # Parse file_types to obtain allowed file extensions
    for s in file_types:
        description, extensions = parse_file_type(s)
        file_extensions = [i.lstrip('*.') for i in extensions.split(';') if i != '*.*']
        file_filter.append([description, file_extensions or []])

    i = BrowserView.instances.get(uid)
    return i.create_file_dialog(dialog_type, directory, allow_multiple, save_filename, file_filter)


def load_url(url, uid):
    i = BrowserView.instances.get(uid)
    if i:
        i.load_url(url)


def load_html(content, base_uri, uid):
    i = BrowserView.instances.get(uid)
    if i:
        i.load_html(content, base_uri)


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


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


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


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


def set_on_top(uid, top):
    def _set_on_top():
        level = AppKit.NSStatusWindowLevel if top else AppKit.NSNormalWindowLevel
        i.window.setLevel_(level)

    i = BrowserView.instances.get(uid)
    if i:
        AppHelper.callAfter(_set_on_top)


def resize(width, height, uid, fix_point):
    i = BrowserView.instances.get(uid)
    if i:
        i.resize(width, height, fix_point)


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


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


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


def move(x, y, uid):
    i = BrowserView.instances.get(uid)
    if i:
        AppHelper.callAfter(i.move, x, y)


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


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:
        return i.get_cookies()


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 _position(coordinates):
        screen_frame = i.screen

        if screen_frame is None:
            raise RuntimeError('Failed to obtain screen')

        window = i.window
        frame = window.frame()
        coordinates[0] = int(frame.origin.x)
        coordinates[1] = int(screen_frame.size.height - frame.origin.y - frame.size.height)
        semaphore.release()

    coordinates = [None, None]
    semaphore = Semaphore(0)

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

    try:
        _position(coordinates)
    except Exception:
        AppHelper.callAfter(_position, coordinates)
        semaphore.acquire()

    return coordinates


def get_size(uid):
    def _size(dimensions):
        size = i.window.frame().size
        dimensions[0] = size.width
        dimensions[1] = size.height
        semaphore.release()

    dimensions = [None, None]
    semaphore = Semaphore(0)

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

    try:
        _size(dimensions)
    except Exception:
        AppHelper.callAfter(_size, dimensions)
        semaphore.acquire()

    return dimensions


def get_screens():
    screens = [
        Screen(
            s.frame().origin.x,
            s.frame().origin.y,
            s.frame().size.width,
            s.frame().size.height,
            s.frame(),
            s.backingScaleFactor(),
        )
        for s in AppKit.NSScreen.screens()
    ]
    return screens


def add_tls_cert(certfile):
    # does not auth against the certfile
    # see webView_didReceiveAuthenticationChallenge_completionHandler_
    pass
