"""Shows the CEF browser with a Panorama viewer.

This module contains the functionality to display an browser window with a view
to the street smart imagery. It is intended to work together with the qgis
plugin but can also work standalone (although with less functionality).

This module defines the following interface:

    - a PanoramaViewer class

"""
#  pylint: disable=c-extension-no-member
import argparse
import logging
import os
import platform
import queue
import signal
import socket
import subprocess
import sys
import tempfile
import threading
import time
from typing import Tuple

from cefpython3 import cefpython as cef

from logger import Logger  # pylint: disable=import-error
from logger import log  # pylint: disable=import-error

from command_getter import CommandGetter  # pylint: disable=import-error
from command_handler import CommandHandler  # pylint: disable=import-error

# log_with uses __name__ to fetch a logger, log_with is called when the module
# with the call is imported. Therefore the __name__ must be changed is soon as
# possible.
standalone = False
if __name__ == "__main__":
    # Hack to remove __main__ from logs
    __name__ = os.path.splitext(os.path.basename(__file__))[0]
    standalone = True


# constants
_DEBUG_CEF = True
_HOST = "localhost"
_INDEX_URI = "panorama_viewer_index.html"

# globals
logger = None

class _CombinedHandler():
    """Combined handler for both lifecycle and load events.

    CEF only allows one client handler per browser, so we need to combine
    the LifespanHandler and LoadHandler functionality into one class.
    """
    def __init__(self, panorama_viewer):
        self.panorama_viewer = panorama_viewer
        self.init_settings = None
        self.cef_browser_closed = False
        self.initialization_started = False  # Flag to prevent multiple initializations
        self.windowloaded_sent = False  # Flag to prevent sending windowloaded multiple times

    def OnBeforePopup(self, *args, **kwargs):  # pylint: disable=unused-argument
        """Handle popup creation for OAuth flow.

        Allow all popups - they share the same request context for OAuth.
        With the OnLoadEnd fix (browser ID 1 check), there should only be
        one OAuth popup now, matching the standalone example behavior.
        """
        # Extract parameters from kwargs (CEFPython passes them as kwargs, not args!)
        browser = kwargs.get('browser')
        frame = kwargs.get('frame')
        target_url = kwargs.get('target_url', '')

        logging.info("="*60)
        logging.info(f"POPUP DETECTED - Args count: {len(args)}, Kwargs: {list(kwargs.keys())}")
        if browser:
            logging.info(f"Source browser ID: {browser.GetIdentifier()}")
        logging.info(f"Target URL: {target_url[:150] if target_url else 'None'}")
        logging.info(f"Panorama initialized: {self.panorama_viewer.panorama_viewer_initialized}")

        # ALLOW about:blank - OAuth popups start as about:blank before navigating
        if not target_url or target_url == 'about:blank':
            logging.info("ALLOWING about:blank popup (OAuth will navigate it)")
            logging.info("="*60)
            return False  # Allow popup

        # ALLOW OAuth identity provider popups
        if 'identity.cyclomedia.com' in target_url:
            logging.info("ALLOWING OAuth identity popup")
            logging.info("="*60)
            return False  # Allow popup

        # ALLOW the proper login callback page
        if '/api/v25.7/login.html' in target_url:
            logging.info("ALLOWING OAuth callback popup")
            logging.info("="*60)
            return False  # Allow popup

        # BLOCK the problematic /login redirect (without /api/v25.7/)
        # This is Browser ID 3 that causes the error
        if target_url.endswith('/login') or '/login?' in target_url:
            logging.info(f"BLOCKING problematic /login popup: {target_url}")
            logging.info("="*60)
            return True  # Block popup

        # Allow all other popups by default
        logging.info(f"ALLOWING popup: {target_url}")
        logging.info("="*60)
        return False  # Allow popup

    @log(logging.INFO)
    def OnLoadEnd(self, browser, frame, http_code):  # pylint: disable=unused-argument
        """Injects initialization JavaScript after the page loads."""
        if not frame.IsMain():
            return

        browser_id = browser.GetIdentifier()
        frame_url = frame.GetUrl()

        # Log every OnLoadEnd event to track all browser/popup activity
        logging.info(f"OnLoadEnd - Browser ID: {browser_id}, URL: {frame_url[:100]}")

        # BLOCK the problematic third popup by closing it when it loads /login or /home/error
        # This popup (Browser ID 3) bypasses OnBeforePopup and shows an error
        # But don't close legitimate StreetSmart viewer URLs (api/v25.7)
        if browser_id != 1:
            # Close error popups
            if '/login' in frame_url or '/home/error' in frame_url:
                # But don't close the OAuth login callback
                if '/api/v25.7/login.html' not in frame_url:
                    logging.info(f"DETECTED PROBLEMATIC POPUP - Browser ID {browser_id}, closing it!")
                    try:
                        browser.CloseBrowser(True)  # Force close
                        logging.info("Successfully closed problematic popup")
                    except Exception as e:
                        logging.error(f"Failed to close popup: {e}")
                    return

        # CRITICAL FIX: Only process events from browser ID 1 (the main browser)
        # Browser ID 1 is always the first browser created (the main panorama window)
        # OAuth popups will have ID 2, 3, etc. and must be ignored
        # This matches the working standalone example
        if browser_id != 1:
            logging.info(f"Ignoring OnLoadEnd for non-main browser (id={browser_id})")
            return

        # Store reference to main browser on first load
        if self.panorama_viewer.cef_browser is None:
            print(f"Storing main browser reference (id={browser.GetIdentifier()})")
            self.panorama_viewer.cef_browser = browser

        print(f"Page loaded (direct loading mode)! URL: {frame.GetUrl()[:100]}")
        # Mark viewer as loaded now that the page is actually loaded
        self.panorama_viewer.panorama_viewer_loaded = True

        # Send windowloaded message to QGIS only once (first page load)
        # This prevents re-initialization when the page reloads after OAuth completes
        if not self.windowloaded_sent:
            self.panorama_viewer.command_sender.send_message("windowloaded")
            self.windowloaded_sent = True
            print("Sent windowloaded message to QGIS")
        else:
            print("Skipping windowloaded message (already sent)")

        # CRITICAL: Only inject initialization code ONCE
        # Check both initialization_started flag AND if we've already initialized successfully
        # This prevents duplicate initialization if the page reloads after OAuth
        if self.panorama_viewer.panorama_viewer_initialized:
            print("StreetSmart API already successfully initialized, skipping")
            return

        # If we have init settings and haven't started initialization yet, inject the JavaScript
        if self.init_settings and not self.initialization_started:
            print("Injecting initialization code")
            self.initialization_started = True
            js_code = self.panorama_viewer.generate_init_javascript(self.init_settings)
            frame.ExecuteJavascript(js_code)
        else:
            if self.initialization_started:
                print("Initialization already started, skipping")
            else:
                print("Waiting for init settings...")

    def _set_init_settings(self, settings):
        """Store the initialization settings to be used when page loads.

        NOTE: This must be a private method (with underscore) so CEFPython
        doesn't try to register it as a callback.
        """
        # If already successfully initialized, don't reinitialize
        if self.panorama_viewer.panorama_viewer_initialized:
            print("StreetSmart already initialized, ignoring duplicate init settings")
            return

        self.init_settings = settings
        print("Init settings stored, will inject on next page load or now if already loaded")

        # If page is already loaded and we haven't started initialization, inject immediately
        if (self.panorama_viewer.panorama_viewer_loaded and
            self.panorama_viewer.cef_browser and
            not self.initialization_started):
            print("Page already loaded, injecting now")
            self.initialization_started = True
            js_code = self.panorama_viewer.generate_init_javascript(settings)
            self.panorama_viewer.cef_browser.GetMainFrame().ExecuteJavascript(js_code)
        elif self.initialization_started:
            print("Initialization already started, ignoring duplicate settings")

    @log(logging.INFO)
    def OnBeforeClose(self, browser):  # pylint: disable=unused-argument
        """Sets attribute which indicates that the browser is closed.

        Only set the flag if the MAIN browser is closing, not popup windows (like OAuth).
        """
        # Check if this is the main browser window
        if browser.GetIdentifier() == self.panorama_viewer.cef_browser.GetIdentifier():
            print("Main browser window closed")
            self.cef_browser_closed = True
        else:
            print(f"Popup/child browser closed (id={browser.GetIdentifier()}), main browser stays open")

    @log(logging.INFO)
    def DoClose(self, browser):  # pylint: disable=unused-argument
        """Sets attribute which indicates that the browser is closed.

        Only set the flag if the MAIN browser is closing, not popup windows (like OAuth).
        """
        # Check if this is the main browser window
        if browser.GetIdentifier() == self.panorama_viewer.cef_browser.GetIdentifier():
            print("Main browser window DoClose")
            self.cef_browser_closed = True
            return True
        else:
            print(f"Popup/child browser DoClose (id={browser.GetIdentifier()}), allowing closure")
            return False  # Allow popup to close without affecting main window


class _LifespanHandler():  # pylint: disable=too-few-public-methods
    """LifespanHandler knows when the CEF browser is closed."""
    def __init__(self):
        self.cef_browser_closed = False

    @log(logging.INFO)
    def OnBeforeClose(self, browser):  # pylint: disable=unused-argument
        """Sets attribute which indicates that the browser is closed.

        Once the browser is closed, the servers must also be closed."""
        print("browser closed")
        self.cef_browser_closed = True

    @log(logging.INFO)
    def DoClose(self, browser):  # pylint: disable=unused-argument
        """Sets attribute which indicates that the browser is closed.

        Once the browser is closed, the servers must also be closed."""
        print("browser closed")
        self.cef_browser_closed = True
        return True

    '''def OnKeyEvent(self, browser, event, event_type, *args, **kwargs):
        if event_type == cef.KEYEVENT_RAWKEYDOWN:
            # Check for the Ctrl+C keyboard shortcut
            if event["code"] == "KeyC" and event["modifiers"] == cef.EVENTFLAG_CTRL_DOWN:
                # Get the selected text from the browser
                browser.GetFocusedFrame().ExecuteJavascript("document.execCommand('copy');")
        return False'''


class _SendToQGIS():  # pylint: disable=too-few-public-methods
    """Sends a message to the listeners. """
    def __init__(self, streetsmart_port: int):
        self.port = streetsmart_port

    @log(logging.DEBUG, print_args=True)
    def send_message(self, msg: str):
        """Sends a message to a TCP server in a parallel thread."""
        address = (_HOST, self.port)
        s = _SendMessageInThread(address, msg)
        s.start()


class _SendMessageInThread(threading.Thread):
    """ Class sends messages to QGIS """
    def __init__(self, address: Tuple[str, int], msg: str):
        threading.Thread.__init__(self)
        self.msg = msg
        self.address = address

    @log()
    def run(self):
        """Sends message """
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
            if not _try_to_connect_to_socket(sock, self.address):
                return
            if not _try_to_send_to_socket(sock, self.msg):
                return


class PanoramaViewer():
    """ Main PanoramaViewer class """
    @log()
    def __init__(self, command_sender, commandline_switches):
        self.commandline_switches = commandline_switches
        self.command_sender = command_sender
        self.lifespan_handler = _LifespanHandler()
        self.combined_handler = _CombinedHandler(self)
        self.command_queue = queue.Queue(maxsize=0)
        self.cef_browser = None
        self.pid_filename = None
        self.tcp_server = None
        self.panorama_viewer_loaded = False
        self.panorama_viewer_initialized = False
        self.use_direct_page_loading = False  # Flag to enable/disable LoadHandler approach

    @log(message="Viewer is running in process " + str(os.getpid()))
    def show(self):
        """ Show PanoramaViewer """

        logging.info(f"show() called, use_direct_page_loading={self.use_direct_page_loading}")

        if self.use_direct_page_loading:
            # Load Cyclomedia page directly (for OAuth support)
            logging.info("Using direct page loading mode")
            self.cef_browser = self.start_cef_browser_direct(self.combined_handler,
                                                             self.commandline_switches)
            logging.info("start_cef_browser_direct returned")
        else:
            # Traditional approach: load local HTTP server
            logging.info("Using traditional HTTP server mode")
            self.pid_filename = _start_http_server(0)
            port = _fetch_http_port_number(self.pid_filename)
            self.cef_browser = self.start_cef_browser(port, self.lifespan_handler,
                                                      self.commandline_switches)
            logging.info("start_cef_browser returned")

        logging.info("show() completed")

    @staticmethod
    @log(level=logging.DEBUG, print_return=True)
    def __create_html_url(port):
        """Creates the url for the start page."""
        return "http://{}:{}/{}".format(_HOST, str(port), _INDEX_URI)

    '''def start_cef_browser(self, port, lifespan_handler, commandline_switches):
        """ Function starts the browser """

        _check_versions()

        # Set excepthook to shutdown all CEF processes on error
        sys.excepthook = cef.ExceptHook
        #if _DEBUG_CEF:
        settings = {
            "debug": True,
            "log_severity": cef.LOGSEVERITY_VERBOSE,
            "log_file": r"c:\temp\debug.log",
            "uncaught_exception_stack_size": -1,
            "ignore_certificate_errors": True,
        }
        #else:
            #settings = {}

        cef.Initialize(settings=settings, switches=commandline_switches)

        index_url = self.__create_html_url(port)
        cef_browser = cef.CreateBrowserSync(
            url=index_url,
            window_title="Street Smart Panorama Viewer")

        cef_browser.SetClientHandler(lifespan_handler)

        self.set_javascript_bindings(cef_browser)

        return cef_browser'''




    @log(logging.INFO)
    def start_cef_browser_direct(self, combined_handler, commandline_switches):
        """ Function starts the browser with direct Cyclomedia page loading

        This approach loads the Cyclomedia-hosted API page directly to avoid
        cross-origin issues with OAuth authentication.
        """
        import os
        logging.info("Starting CEF browser with direct loading mode")
        base_path = os.path.dirname(os.path.abspath(__file__))
        cache_path = os.path.join(base_path, "cef_cache")

        settings = {
            "cache_path": cache_path,
            "persist_session_cookies": True,
            "persist_user_preferences": True,
            # CRITICAL for OAuth: Use a single shared request context for all browsers
            # This ensures cookies, local storage, and OAuth tokens are shared between
            # the main window and any popups (like the OAuth login window)
            "unique_request_context_per_browser": False,
        }

        logging.info("Initializing CEF...")
        cef.Initialize(settings=settings, switches=commandline_switches)
        logging.info("CEF initialized successfully")

        # Load Cyclomedia-hosted page directly
        # This avoids cross-origin issues with OAuth
        # IMPORTANT: Must match the version in redirect URIs (v25.7)
        streetsmart_url = "https://streetsmart.cyclomedia.com/api/v25.7/api-dotnet.html"
        logging.info(f"Creating browser with URL: {streetsmart_url}")

        # Create browser with explicit request context settings
        # This ensures all browsers (including popups) share the same storage
        browser_settings = {}  # Use default/global request context

        cef_browser = cef.CreateBrowserSync(
            url=streetsmart_url,
            window_title="Street Smart Panorama Viewer",
            settings=browser_settings)
        logging.info("Browser created successfully")

        # Set the combined handler FIRST (handles both lifecycle and load events)
        # This must be set before JavaScript bindings
        try:
            logging.info(f"About to set client handler, type={type(combined_handler)}")
            cef_browser.SetClientHandler(combined_handler)
            logging.info("Client handler set successfully")
        except Exception as e:
            logging.error(f"Failed to set client handler: {e}", exc_info=True)
            raise

        self.set_javascript_bindings(cef_browser)
        logging.info("JavaScript bindings set")

        # DON'T set panorama_viewer_loaded here - it will be set when the page actually loads
        # The CombinedHandler will take care of initialization
        logging.info(f"Returning browser, cef_browser_closed={combined_handler.cef_browser_closed}")

        return cef_browser

    def start_cef_browser(self, port, lifespan_handler, commandline_switches):
            """ Function starts the browser """

            #_check_versions()

            # Configure settings for OAuth support
            # OAuth requires session cookies and user preferences to be persisted
            # cache_path is required for OAuth to store authentication state
            import os
            base_path = os.path.dirname(os.path.abspath(__file__))
            cache_path = os.path.join(base_path, "cef_cache")

            settings = {
                "cache_path": cache_path,
                "persist_session_cookies": True,
                "persist_user_preferences": True,
            }

            cef.Initialize(settings=settings, switches=commandline_switches)

            index_url = self.__create_html_url(port)
            cef_browser = cef.CreateBrowserSync(
                url=index_url,
                window_title="Street Smart Panorama Viewer")

            self.set_javascript_bindings(cef_browser)

            cef_browser.SetClientHandler(lifespan_handler)



            return cef_browser



    def _setup_message_loop(self):
        """Sets things up for the message loop to start.

        Starts the TCP server to receive commands.
        """
        self.tcp_server = _start_tcp_server(self.command_queue)
        server_port = self.tcp_server.server_address[1]
        self.command_sender.send_message("serveron|{}".format(server_port))

    @log(logging.DEBUG)
    def _shutdown_message_loop(self):
        """Closes everything after the panoramaviewer is closed."""
        # Send stop to QGIS TCP server
        self.command_sender.send_message("stop")

        # Stop HTTP server if it was started (not used in direct loading mode)
        if self.pid_filename:
            self.__stop_http_server(self.pid_filename)
            os.remove(self.pid_filename)

        # Always stop TCP server
        if self.tcp_server:
            self.__stop_tcp_server(self.tcp_server)

        # Shutdown CEF
        cef.Shutdown()

    @log(logging.INFO)
    def exec_(self):
        """Starts message loop."""

        logging.info("exec_() called")
        self._setup_message_loop()

        # Start Message loop
        # Use combined_handler if in direct loading mode, otherwise use lifespan_handler
        handler = self.combined_handler if self.use_direct_page_loading else self.lifespan_handler
        logging.info(f"Starting do_work with direct_loading={self.use_direct_page_loading}, handler={type(handler).__name__}")
        logging.info(f"Handler cef_browser_closed={handler.cef_browser_closed}")

        self.do_work(self.command_queue, handler,
                     self.cef_browser, log_port)

        logging.info("exec_() completed")
        self._shutdown_message_loop()

    @log(logging.DEBUG)
    def add_overlay(self, geojson, name, srs, sld_text, color):
        """ Adds overlay to the panoramaviewer """
        self.cef_browser.ExecuteFunction(
            "addOverlay", geojson, name, srs, sld_text, color)

    def js_measure(self, msg):
        """ Function call from the browser when a measurement is changed """
        self.command_sender.send_message("measure|{}".format(msg))

    def js_cone(self, msg):
        """ Function called from the browser """
        self.command_sender.send_message("cone|{}".format(msg))

    def js_window_loaded(self):
        """ Sends message to QGIS to indicate that the browser is loaded.
        Function called from the browser
        """
        self.panorama_viewer_loaded = True
        self.command_sender.send_message("windowloaded")

    def js_initialized(self):
        """ Function called from the browser """
        self.panorama_viewer_initialized = True

    def set_init_settings(self, settings_json):
        """Store initialization settings for LoadHandler approach.

        This method is called when using the LoadHandler to inject JavaScript
        after loading the Cyclomedia-hosted page directly.

        Args:
            settings_json: JSON string containing userSettings, addressSettings, configSettings
        """
        import json
        settings = json.loads(settings_json)
        self.combined_handler._set_init_settings(settings)

    def generate_init_javascript(self, settings):
        """Generate JavaScript code to initialize StreetSmart API.

        This mimics the approach from the working example where JavaScript
        is injected after the page loads.

        Args:
            settings: Dict with userSettings, addressSettings, configSettings

        Returns:
            String containing JavaScript code
        """
        import json

        user_settings = settings.get('userSettings', {})
        address_settings = settings.get('addressSettings', {})
        config_settings = settings.get('configSettings', {})

        # Prepare values for injected JavaScript (matching standalone example)
        login_oauth = bool(user_settings.get('useOAuth'))
        client_id = user_settings.get('clientId', '') or ''
        api_key = user_settings.get('apiKey', '') or ''
        username = user_settings.get('username', '') or ''
        password = user_settings.get('password', '') or ''
        srs_code = config_settings.get('srs', '') or ''
        locale = config_settings.get('locale', '') or ''
        config_url = config_settings.get('configUrl', '') or ''
        address_locale = address_settings.get('locale', 'en') or 'en'
        address_database = address_settings.get('database', 'Nokia') or 'Nokia'

        def js_string(value):
            return json.dumps(value if value is not None else "")

        if login_oauth:
            # For OAuth authentication - DON'T set redirect URIs, let API use defaults
            # After analysis of logs: Browser 3 opens /login (not /api/v25.7/login.html)
            # This suggests StreetSmart API is doing a secondary operation after OAuth
            # The standalone script works WITHOUT setting redirect URIs, so match that
            auth_lines = f"""
                loginOauth: true,
                clientId: {js_string(client_id)},
                doOAuthLogoutOnDestroy: false,
                loginOauthSilentOnly: false,
            """
        else:
            auth_lines = f"""
                loginOauth: false,
                username: {js_string(username)},
                password: {js_string(password)},
            """

        js_code = f"""
            console.log('=== LoadHandler JavaScript START ===');
            console.log('Current URL:', window.location.href);
            console.log('typeof StreetSmartApi:', typeof StreetSmartApi);
            console.log('window.streetSmartInitialized:', window.streetSmartInitialized);

            // Safety check: Only initialize if StreetSmartApi is available
            // This prevents errors if this code runs in OAuth popup windows
            if (typeof StreetSmartApi === 'undefined') {{
                console.log('StreetSmartApi not available (probably OAuth popup), skipping initialization');
            }} else if (window.streetSmartInitialized === true) {{
                console.log('StreetSmartApi already initialized, skipping duplicate initialization');
            }} else {{
                // Mark that we're initializing to prevent duplicate init calls
                window.streetSmartInitialized = true;
                console.log('Set window.streetSmartInitialized = true');

                var domElement = document.createElement('div');
                domElement.setAttribute('id', 'streetsmart-api-container');
                domElement.setAttribute('style', 'width:100%;height:100%;position:absolute;top:0px;left:0px');
                document.body.appendChild(domElement);
                console.log('DOM element created and appended');

                var initOpts = {{
                    targetElement: domElement,
{auth_lines}
                    apiKey: {js_string(api_key)},
                    srs: {js_string(srs_code)},
                    locale: {js_string(locale)},
                    addressSettings: {{
                        locale: {js_string(address_locale)},
                        database: {js_string(address_database)}
                    }}
                }};

                console.log('About to call StreetSmartApi.init() - popup should appear if no cached tokens...');
                console.log('Init options:', initOpts);
                console.log('window.location.origin:', window.location.origin);
                console.log('window.location.href:', window.location.href);
                console.log('Calling StreetSmartApi.init()...');
                StreetSmartApi.init(initOpts).then(
                    function() {{
                        console.log('Api: init: success!');

                        // Initialize overlay layers array
                        window.overlayLayers = [];

                        // Helper function to create cone string for viewer position
                        window.coneString = function(panoramaViewer) {{
                            // Safety check: panoramaViewer can be null during unmount
                            if (!panoramaViewer) {{
                                console.warn('coneString called with null panoramaViewer');
                                return null;
                            }}
                            try {{
                                var rotation = panoramaViewer.getOrientation().yaw;
                                var x = panoramaViewer.getRecording().xyz[0];
                                var y = panoramaViewer.getRecording().xyz[1];
                                var z = panoramaViewer.getRecording().xyz[2];
                                var id = panoramaViewer.getRecording().id;
                                var srs = panoramaViewer.getRecording().srs;
                                var r = panoramaViewer.getViewerColor()[0];
                                var g = panoramaViewer.getViewerColor()[1];
                                var b = panoramaViewer.getViewerColor()[2];
                                var a = panoramaViewer.getViewerColor()[3] * 255;
                                return [id, x, y, z, srs, rotation, r, g, b, a].toString();
                            }} catch (e) {{
                                console.warn('Error in coneString:', e);
                                return null;
                            }}
                        }};

                        // Initialize event handlers for panorama viewer
                        window.initEvents = function() {{
                            console.log("Setting up measurement and view events");
                            StreetSmartApi.on(StreetSmartApi.Events.measurement.MEASUREMENT_CHANGED, function(e) {{
                                if (typeof js_measure === 'function') {{
                                    js_measure(JSON.stringify(e.detail.activeMeasurement));
                                }}
                            }});

                            window.panoramaViewer.on(StreetSmartApi.Events.panoramaViewer.VIEW_CHANGE, function(e) {{
                                if (typeof js_cone === 'function' && window.panoramaViewer) {{
                                    var coneData = window.coneString(window.panoramaViewer);
                                    if (coneData) {{
                                        js_cone(coneData);
                                    }}
                                }}
                            }}).on(StreetSmartApi.Events.panoramaViewer.VIEW_LOAD_END, function(e) {{
                                if (typeof js_cone === 'function' && window.panoramaViewer) {{
                                    var coneData = window.coneString(window.panoramaViewer);
                                    if (coneData) {{
                                        js_cone(coneData);
                                    }}
                                }}
                            }});
                        }};

                        // Open panorama at specified point
                        window.openPanorama = function(point) {{
                            console.log('openPanorama called with:', point);
                            point = JSON.parse(point);
                            StreetSmartApi.open(point.point, {{
                                viewerType: [StreetSmartApi.ViewerType.PANORAMA],
                                srs: point.crs,
                                panoramaViewer: {{ replace: true }},
                            }}).then(
                                function(result) {{
                                    if (result && result[0]) {{
                                        for (let i = 0; i < result.length; i++) {{
                                            console.log('Opened a panorama viewer through API!', result[i]);
                                            if(result[i].getType() === StreetSmartApi.ViewerType.PANORAMA) {{
                                                window.panoramaViewer = result[i];
                                                window.initEvents();
                                            }}
                                        }}
                                    }}
                                }}
                            ).catch(
                                function(reason) {{
                                    console.error('Error opening panorama viewer: ' + reason);
                                }}
                            );
                        }};

                        // Get current measurement (obsolete but kept for compatibility)
                        window.getMeasure = function() {{
                            console.log("getMeasure called");
                            var activeMeasure = StreetSmartApi.getActiveMeasurement(window.panoramaViewer);
                            console.log(activeMeasure);
                        }};

                        // Start measurement mode
                        window.startMeasure = function(geometryType) {{
                            var measurementtype = '';
                            if (geometryType == 'point') {{
                                measurementtype = StreetSmartApi.MeasurementGeometryType.POINT;
                            }} else if (geometryType == 'polyline') {{
                                measurementtype = StreetSmartApi.MeasurementGeometryType.LINESTRING;
                            }} else if (geometryType == 'polygon') {{
                                measurementtype = StreetSmartApi.MeasurementGeometryType.POLYGON;
                            }}

                            if (measurementtype == '') {{
                                StreetSmartApi.startMeasurementMode(window.panoramaViewer);
                            }} else {{
                                StreetSmartApi.startMeasurementMode(window.panoramaViewer, {{geometry: measurementtype}});
                            }}
                        }};

                        // Stop measurement mode
                        window.stopMeasure = function() {{
                            StreetSmartApi.stopMeasurementMode();
                        }};

                        // Remove all overlay layers
                        window.removeOverlay = function() {{
                            var len = window.overlayLayers.length;
                            for (var i = 0; i < len; i++) {{
                                StreetSmartApi.removeOverlay(window.overlayLayers[i].id);
                            }}
                            window.overlayLayers = [];
                        }};

                        // Add overlay layer
                        window.addOverlay = function(geojson, name, srs, sldText, color) {{
                            console.log("Adding overlay:", name);
                            var options = {{
                                "geojson": JSON.parse(geojson),
                                "visible": true,
                                "id": '',
                            }};
                            if (name !== undefined && name !== "") {{
                                options.name = name;
                            }}
                            if (srs !== undefined && srs !== "") {{
                                options.sourceSrs = srs;
                            }}
                            if (sldText !== undefined && sldText !== "") {{
                                options.sldXMLtext = sldText;
                            }}
                            if (color !== undefined && color !== "") {{
                                options.color = color;
                            }}
                            try {{
                                window.overlayLayers.push(StreetSmartApi.addOverlay(options));
                            }} catch (e) {{
                                console.error('Error adding overlay:', e);
                            }}
                        }};

                        // Alias for compatibility with existing code that expects 'open'
                        window.open = window.openPanorama;

                        // Notify Python that initialization is complete
                        if (typeof js_initialized === 'function') {{
                            js_initialized();
                        }}
                    }},
                    function(err) {{
                        console.error('Api: init: failed. Error: ', err);
                        // Reset flag on failure so user can try again
                        window.streetSmartInitialized = false;
                        alert('Api Init Failed: ' + err);
                    }}
                );
            }}
        """

        return js_code

    @log(level=logging.DEBUG)
    def set_javascript_bindings(self, browser):
        """ Sets the functions which can be called from javascript """
        bindings = cef.JavascriptBindings(
            bindToFrames=False, bindToPopups=False)
        bindings.SetFunction("js_cone", self.js_cone)
        bindings.SetFunction("js_measure", self.js_measure)
        bindings.SetFunction("js_initialized", self.js_initialized)
        bindings.SetFunction("js_window_loaded", self.js_window_loaded)
        browser.SetJavascriptBindings(bindings)

    @staticmethod
    @log(print_args=True, print_return=True)
    def __kill_process(pid):
        """Kills the given process."""
        if pid != 0:
            try:
                os.kill(pid, 15)
            except OSError:
                # TODO: log exception
                # self.logger.exception("Could not stop http process %d", pid)
                return "Could not stop http process {}".format(pid)

    @staticmethod
    @log()
    def __stop_tcp_server(tcp_server):
        """Stops a server."""
        tcp_server.shutdown()

    @log(print_args=True)
    def __stop_http_server(self, pid_filename):
        """Stops the http server."""
        pid = _get_httpserver_pid(pid_filename)
        self.__kill_process(pid)

    @log(print_args=True)
    def shutdown(self, pid_filename, tcp_server):
        """ Shutdown all servers and the browser """
        # Send stop to QGIS TCP server
        self.command_sender.send_message("stop")

        # Stop HTTP server only if pid_filename is provided
        if pid_filename:
            self.__stop_http_server(pid_filename)

        # Stop TCP server if provided
        if tcp_server:
            self.__stop_tcp_server(tcp_server)

        cef.Shutdown()

    @log(level=logging.INFO)
    def do_work(self,
                command_queue: queue.Queue,
                lifespan_handler: _LifespanHandler,
                cef_browser, log_port=0):
        """
        Handles the commands given to the browser

        These commands the user gives through mouse and keyboard as well as
        the commands which are given via the tcp socket
        """
        command_handler = CommandHandler(cef_browser, log_port, self)

        print(f"Entering do_work loop. Handler closed: {lifespan_handler.cef_browser_closed}, killer: {killer.kill_now}")

        iteration = 0
        while (lifespan_handler and not lifespan_handler.cef_browser_closed
               and not killer.kill_now):
            if iteration < 5:  # Only print first 5 iterations
                print(f"do_work iteration {iteration}, closed: {lifespan_handler.cef_browser_closed}")
            iteration += 1
            cef.MessageLoopWork()
            self.localMessageLoopWork(command_queue, command_handler)
            time.sleep(0.01)

        # TODO: close httpserver and tcp server if browser is closed
        print(f"Stopped do_work loop. Iterations: {iteration}, Handler closed: {lifespan_handler.cef_browser_closed}, killer: {killer.kill_now}")
        logging.info("Stopped do_work loop")

    @staticmethod
    @log(logging.INFO)
    def __add_command_to_wait_list(comm):
        """Adds commands to a list to be processed when the communication
        between Viewer and QGIS is established."""
        temp_list.append(comm)

    @staticmethod
    @log(logging.INFO)
    def __process_wait_list(command_handler):
        """Process the commands which were given before the communication
        between Viewer and QGIS was established."""
        comm = temp_list.pop()
        _try_to_execute_command(command_handler, comm)

    def localMessageLoopWork(self,
                             command_queue: queue.Queue,
                             command_handler: CommandHandler):
        """ Processes one command from the queue

        First command must be an 'initialize'. Keep all other commands waiting
        in a temp_list.

        Method runs every *interval* seconds(DEFAULT = 0.1s)
        """
        while self.panorama_viewer_loaded and not command_queue.empty():
            try:
                comm = command_queue.get_nowait()
            except queue.Empty:
                break

            if (not self.panorama_viewer_initialized and not
                    comm[0].startswith("init")):
                self.__add_command_to_wait_list(comm)
            else:
                _try_to_execute_command(command_handler, comm)

        while self.panorama_viewer_initialized and len(temp_list) > 0:
            comm = temp_list.pop()
            _try_to_execute_command(command_handler, comm)


@log(logging.DEBUG, print_args=True, print_return=True)
def _try_to_connect_to_socket(sock: socket,
                              address: Tuple[str, int]) -> bool:
    """Tries to connect to a socket.

    Return True if connected
    """
    try:
        sock.connect(address)
        return True
    except OSError:
        # logger.exception("Cannot connect to %s", address)
        return False


@log(logging.DEBUG, print_args=True, print_return=True)
def _try_to_send_to_socket(sock: socket, msg: str) -> bool:
    """Tries to send the message to the socket."""
    try:
        sock.sendall(bytes(msg + '\n', "utf-8"))
        # logger.debug("Send %s", msg)
        return True
    except OSError:
        # logger.exception("Error sending to socket")
        return False


@log(level=logging.DEBUG, print_return=True)
def _fetch_http_port_number(filename):
    """Fetches the port nummer from the httpd temporary file.

    Must wait till port number is written to the file.
    """
    while True:
        with open(filename, 'r') as process_file:
            lines = process_file.readlines()

        if len(lines) >= 2:
            return int(lines[1].strip())


def _create_temp_filename():
    """ Create a tempfile and return the name """
    handle, filename = tempfile.mkstemp()
    os.close(handle)
    return filename


@log(level=logging.DEBUG, print_return=True)
def _determine_http_server_exe_path():
    """Determines http server path"""
    base_path = os.path.dirname(os.path.abspath(__file__))
    return os.path.join(base_path, "httpserver.py")


@log(level=logging.DEBUG, print_return=True)
def _determine_python_exe_path():
    """Determines python executable path"""
    return os.path.join(sys.base_exec_prefix, 'python.exe')


@log()
def _start_http_server(port):
    """Starts http server in another process."""
    file_to_execute = _determine_http_server_exe_path()
    python_exe = _determine_python_exe_path()
    temp_filename = _create_temp_filename()
    subprocess.Popen([python_exe, file_to_execute, str(port),  # nosec B603 - args are plugin-derived paths and ports
                      temp_filename, str(log_port)], creationflags=subprocess.CREATE_NO_WINDOW)

    # logger.debug("HTTP Server started with pid %s", proc.pid)
    return temp_filename


def _get_httpserver_pid(filename):
    """ Returns the pid of the http server """
    with open(filename, 'r') as f:
        file_content = f.readline()

    try:
        pid = int(file_content)
    except ValueError:
        pid = 0

    return pid


@log()
def _start_tcp_server(command_queue):
    """Starts the server to listen for calls from QGIS.  """
    tcp_server = CommandGetter(command_queue)
    tcp_server.start()

    return tcp_server


temp_list = []


def write(message):
    """ Write to log file"""
    print("Logger: ", message)
    with open(r"c:\temp\simplelog.log", "a") as flog:
        flog.write(message + '\n')


@log(level=logging.DEBUG, print_args=True)
def _try_to_execute_command(command_handler: CommandHandler,
                            command: Tuple[str, str]):  # , logger: Logger):
    """Tries to execute a command."""
    try:
        command_handler.execute(command)
    except Exception as ex:  # Catch unhandled errors from commands. pylint: disable=broad-except
        write("Unhandled exception in command {}, {}".format(command, ex))
        # logger.exception('Unhandled exception in command %s', command)
        # logger.exception('Unhandled exception in command %s', command)


def _check_versions():
    """Checks CEF versions """
    ver = cef.GetVersion()
    print("[hello_world.py] CEF Python %s", ver["version"])
    print("[hello_world.py] Chromium %s", ver["chrome_version"])
    print("[hello_world.py] CEF %s", ver["cef_version"])
    print("[hello_world.py] Python %s %s",
          platform.python_version(),
          platform.architecture()[0])
    if cef.__version__ < "57.0":
        raise RuntimeError("CEF Python v57.0+ required to run this")


@log(print_args=True, print_return=True)
def create_commandline_switches(args):
    print("Args")
    print(args)
    rv = {}
    if args.proxy_bypass_list:
        rv["proxy-bypass-list"] = args.proxy_bypass_list
    if args.proxy_server:
        rv["proxy-server"] = args.proxy_server
    if args.no_proxy_server:
        rv["no-proxy-server"] = ""
    if args.proxy_auto_detect:
        rv["proxy_auto_detect"] = ""

        # GPU settings
    rv["use-gl"] = "Desktop"  # Using OpenGL for GPU acceleration swiftshader
    rv["enable-webgl"] = ""
    rv["no-sandbox"] = ""
    #rv["disable-gpu-blacklist"] = ""
    rv["enable-gpu-rasterization"] = ""
    rv["enable-zero-copy"] = ""
    rv["disable-popup-blocking"] = ""

    # REMOVED: single-process mode breaks popup windows
    # REMOVED: disable-web-security interferes with OAuth popup communication
    # The working example (Cefpython1.py) doesn't use these switches
    # OAuth works correctly with just the cache settings and default browser security

    return rv


@log(print_args=True)
def main(args):
    """ Starts a browser and initialize the bindings """
    Logger(__name__, args.log_port).get()  # Needed to initialize the SocketLogger
    sender = _SendToQGIS(args.command_port)
    commandline_switches = create_commandline_switches(args)
    p = PanoramaViewer(sender, commandline_switches)

    # Enable direct page loading for OAuth if requested
    if hasattr(args, 'use_direct_loading') and args.use_direct_loading:
        print("Enabling direct page loading for OAuth support")
        p.use_direct_page_loading = True

    p.show()
    p.exec_()


@log(print_return=True)
def parse_arguments():
    """Parses the command line arguments."""
    parser = argparse.ArgumentParser()
    parser.add_argument("command_port",
                        help="port to which commands will be send",
                        type=int)
    parser.add_argument("log_port", help="port to which logging will be send",
                        type=int)
    group = parser.add_mutually_exclusive_group()
    group.add_argument("--no-proxy-server", help="no proxy server",
                       action="store_true")
    group.add_argument("--proxy-auto-detect", help="proxy auto detect",
                       action="store_true")
    group.add_argument("--proxy-server", help="proxy server")
    group.add_argument("--proxy-pac-url", help="proxy pac url")
    parser.add_argument("--proxy-bypass-list", help="proxy bypass list")
    parser.add_argument("--use-direct-loading",
                       help="use direct Cyclomedia page loading (for OAuth)",
                       action="store_true")

    ar = parser.parse_args()
    return ar


class GracefullKiller:
    """Class responsible for cleaning up when process is killed

    https://stackoverflow.com/questions/18499497/how-to-process-sigterm-signal-gracefully
    """
    kill_now = False

    def __init__(self):
        signal.signal(signal.SIGINT, self.exit_gracefully)
        signal.signal(signal.SIGTERM, self.exit_gracefully)
        signal.signal(signal.SIGBREAK, self.exit_gracefully)

    def exit_gracefully(self, signum, frame):
        """Sets kill flag"""
        print("Caught signal, set stop flag")
        self.kill_now = True


# __name__ is redefined so attribute standalone must be checked
if __name__ == '__main__' or standalone:
    killer = GracefullKiller()
    print('Argument List:', str(sys.argv))
    cmd_line_args = parse_arguments()
    log_port = cmd_line_args.log_port  # TODO: Remove this. Start http server in main, not in PV class
    main(cmd_line_args)

    print("Gracefully stopped")
    logging.shutdown()
