# -*- coding: utf-8 -*-
"""
/***************************************************************************
 Riverscpaes Viewer
                                 A QGIS plugin
 Explore symbolized Riverscapes projects
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                              -------------------
        begin                : 2021-04-13
        git sha              : $Format:%H$
        copyright            : (C) 2021 by North Arrow Research
        email                : info@northarrowresearch.com
 ***************************************************************************/
"""
import sys
import os.path
from time import time
from functools import partial
from pathlib import Path
import re

from qgis.utils import showPluginHelp
from qgis.core import QgsApplication, QgsProject, QgsMessageLog, Qgis, QgsMapLayer

from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication, Qt, QUrl, pyqtSignal
from qgis.PyQt.QtGui import QIcon, QDesktopServices
from qgis.PyQt.QtWidgets import QAction, QFileDialog, QToolButton, QMenu, QMessageBox

from .classes.settings import Settings, CONSTANTS
from .classes.net_sync import NetSync
from .classes.basemaps import BaseMaps
from .classes.map import get_map_center, get_zoom_level


# Initialize Qt resources from file resources.py
# Import the code for the dialog
from .options_dialog import OptionsDialog
from .about_dialog import AboutDialog
from .dock_widget import QRAVEDockWidget
from .classes.data_exchange.DataExchangeAPI import DataExchangeAPI
from .meta_widget import QRAVEMetaWidget
from .frm_project_bounds import FrmProjectBounds
from .remote_project_dialog import RemoteProjectDialog
from .project_download_dialog import ProjectDownloadDialog

# initialize Qt resources from file resources.py
from . import resources
from ..__version__ import __version__

RESOURCES_DIR = os.path.join(os.path.dirname(__file__), '..', 'resources')

# BASE is the name we want to use inside the settings keys
MESSAGE_CATEGORY = CONSTANTS['logCategory']


class QRAVE:
    """QGIS Plugin Implementation."""

    def __init__(self, iface):
        """Constructor.

        :param iface: An interface instance that will be passed to this class
            which provides the hook by which you can manipulate the QGIS
            application at run time.
        :type iface: QgsInterface
        """
        # Save reference to the QGIS interface
        self.iface = iface
        self.tm = QgsApplication.taskManager()
        self.qproject = QgsProject.instance()
        self.settings = Settings(iface=self.iface)

        self.settings.setValue('DEBUG', os.environ.get("RS_DEBUG", "False").lower() == "true")
        self.settings.setValue('Staging', os.environ.get("RS_STAGING", "False").lower() == "true")

        self.pluginIsActive = False

        self.dockwidget = QRAVEDockWidget()
        self.metawidget = QRAVEMetaWidget()

        # Populated on load from a URL
        self.acknowledgements = None

        # initialize plugin directory
        self.plugin_dir = os.path.dirname(__file__)
        # initialize locale
        locale = QSettings().value('locale/userLocale')[0:2]
        locale_path = os.path.join(
            self.plugin_dir,
            'i18n',
            'QRAVE_{}.qm'.format(locale))

        if os.path.exists(locale_path):
            self.translator = QTranslator()
            self.translator.load(locale_path)
            QCoreApplication.installTranslator(self.translator)

        # Declare instance attributes
        self.actions = []
        self.menu = self.tr(u'&Riverscapes Viewer Plugin')

        # TODO: We are going to let the user set this up in a future iteration
        self.toolbar = self.iface.addToolBar(u'Riverscapes Viewer')
        self.toolbar.setObjectName(u'Riverscapes Viewer')
        self.menu = QMenu()

        self.debugpy = None
        self._enable_debug()

    def _enable_debug(self):
        debug_port = 5678
        debug_host = "localhost"
        DEBUG_ON = self.settings.getValue('DEBUG')
        if not DEBUG_ON:
            return

        if self.debugpy is None:
            try:
                import debugpy
                self.debugpy = debugpy
            except:
                self.settings.log("Need install debugpy: pip3 install debugpy", Qgis.Warning)
                pass

        if self.debugpy is None:
            return
        else:
            try:
                # if we are in OSX then this is the path
                if sys.platform == 'darwin':
                    python_path = os.path.join(Path(os.__file__).parents[2], 'bin', Path(os.__file__).parent.name)
                elif sys.platform == 'win32':
                    python_path = os.path.join(Path(os.__file__).parents[1], 'python.exe')
                self.settings.log(f"debugpy imported and attached to: {python_path}", Qgis.Success)
                debugpy.configure(python=python_path)
            except Exception as e:
                self.settings.log("Error initializing debugpy: {}".format(e), Qgis.Critical)
                raise e

        msgPort = f'"request": "attach", "Port": {debug_port}, "host": "{debug_host}"'
        if self.debugpy.is_client_connected():
            self.settings.log(f"ALREADY ACTIVE: Remote Debug for Visual Studio is active({debug_port})", Qgis.Warning)
            return
        else:
            try:
                t_, new_port = self.debugpy.listen((debug_host, debug_port))
            except Exception as e:
                self.settings.log(f"Error starting debugpy: {e}", Qgis.Critical)
                return
            msgPort = f'"request": "enable_attach", "Port": {new_port}, "host": "{debug_host}"'
            self.settings.log(f"Remote Debug for Visual Studio is running({msgPort})", Qgis.Success)

    def tr(self, message):
        """Get the translation for a string using Qt translation API.

        We implement this ourselves since we do not inherit QObject.

        :param message: String for translation.
        :type message: str, QString

        :returns: Translated version of message.
        :rtype: QString
        """
        # noinspection PyTypeChecker,PyArgumentList,PyCallByClass
        return QCoreApplication.translate('Riverscapes Viewer', message)

    def initGui(self):
        """Create the menu entries and toolbar icons inside the QGIS GUI."""
        self.qproject.readProject.connect(self.onProjectLoad)

        self.openAction = QAction(QIcon(':/plugins/qrave_toolbar/viewer-icon.svg'),
                                  self.tr(u'Show Riverscapes Viewer Panel'), self.iface.mainWindow())
        self.openAction.triggered.connect(self.toggle_widget)
        self.openAction.setStatusTip('Toggle the project viewer')
        self.openAction.setWhatsThis('Toggle the project viewer')
        self.actions.append(self.openAction)

        self.openProjectAction = QAction(QIcon(
            ':/plugins/qrave_toolbar/open.svg'), self.tr(u'Open Riverscapes Project'), self.iface.mainWindow())
        self.openProjectAction.triggered.connect(self.projectBrowserDlg)
        self.openProjectAction.setStatusTip('Open Riverscapes project')
        self.openProjectAction.setWhatsThis('Open Riverscapes project')
        self.actions.append(self.openProjectAction)

        self.openRemoteProjectAction = QAction(QIcon(
            ':/plugins/qrave_toolbar/data-exchange-icon.svg'), self.tr(u'Open Remote Project'), self.iface.mainWindow())
        self.openRemoteProjectAction.triggered.connect(self.remoteProjectDlg)
        self.openRemoteProjectAction.setStatusTip('Open Remote Riverscapes project')
        self.openRemoteProjectAction.setWhatsThis('Open Remote Riverscapes project')
        self.actions.append(self.openRemoteProjectAction)

        self.closeAllProjectsAction = QAction(QIcon(':/plugins/qrave_toolbar/close.png'), self.tr(
            u'Close All Riverscapes Projects'), self.iface.mainWindow())
        self.closeAllProjectsAction.triggered.connect(self.closeAllProjects)
        self.closeAllProjectsAction.setStatusTip('Close all open Riverscapes projects')
        self.closeAllProjectsAction.setWhatsThis('Close all open Riverscapes projects')
        self.actions.append(self.closeAllProjectsAction)

        self.browseExchangeProjectsAction = QAction(QIcon(':/plugins/qrave_toolbar/data-exchange-icon.svg'), self.tr(u'Browse Data Exchange Projects in Map Area'), self.iface.mainWindow())
        self.browseExchangeProjectsAction.triggered.connect(self.browseExchangeProjects)
        self.actions.append(self.browseExchangeProjectsAction)

        self.downloadProjectAction = QAction(QIcon(
            ':/plugins/qrave_toolbar/download.svg'), self.tr(u'Download Riverscapes Project'), self.iface.mainWindow())
        self.downloadProjectAction.triggered.connect(lambda: self.downloadProjectDlg())
        self.downloadProjectAction.setStatusTip('Download a Riverscapes project from the Data Exchange')
        self.downloadProjectAction.setWhatsThis('Download a Riverscapes project from the Data Exchange')
        self.actions.append(self.downloadProjectAction)

        self.toolsButton = QToolButton()
        self.toolsButton.setToolButtonStyle(Qt.ToolButtonTextOnly)
        self.toolsButton.setMenu(QMenu())
        self.toolsButton.setPopupMode(QToolButton.MenuButtonPopup)
        self.toolsButton.setText('Tools')

        m_tools = self.toolsButton.menu()
        m_tools.setTitle("Tools")
        m_tools.setIcon(QIcon(':/plugins/qrave_toolbar/tools'))

        self.raveOptionsAction = QAction(
            QIcon(':/plugins/qrave_toolbar/Options.png'),
            self.tr('Settings'),
            self.iface.mainWindow()
        )
        self.raveOptionsAction.triggered.connect(self.options_load)

        self.net_sync_action = QAction(
            QIcon(':/plugins/qrave_toolbar/refresh.png'),
            self.tr('Update resources'),
            self.iface.mainWindow()
        )
        self.net_sync_action.triggered.connect(
            lambda: self.net_sync_load(force=True))

        self.find_resources_action = QAction(
            QIcon(':/plugins/qrave_toolbar/BrowseFolder.png'),
            self.tr('Open Resources folder'),
            self.iface.mainWindow()
        )
        self.find_resources_action.triggered.connect(self.locateResources)

        self.generate_project_bounds = QAction(
            QIcon(':/plugins/qrave_toolbar/bounds'),
            self.tr('Generate Project Bounds'),
            self.iface.mainWindow()
        )
        self.actions.append(self.generate_project_bounds)

        # Open a project bounds dialog
        self.generate_project_bounds.triggered.connect(
            lambda: self.show_project_bounds())

        m_tools.addAction(self.net_sync_action)
        m_tools.addAction(self.find_resources_action)
        m_tools.addSeparator()
        m_tools.addAction(self.generate_project_bounds)
        m_tools.addSeparator()
        m_tools.addAction(self.raveOptionsAction)

        self.helpButton = QToolButton()
        self.helpButton.setToolButtonStyle(Qt.ToolButtonTextOnly)
        self.helpButton.setMenu(QMenu())
        self.helpButton.setPopupMode(QToolButton.MenuButtonPopup)

        m = self.helpButton.menu()
        m.setTitle("Help")
        m.setIcon(QIcon(':/plugins/qrave_toolbar/Help.png'))

        self.helpAction = QAction(
            QIcon(':/plugins/qrave_toolbar/Help.png'),
            self.tr('Riverscapes Viewer Online Help'),
            self.iface.mainWindow()
        )
        self.helpAction.triggered.connect(lambda: QDesktopServices.openUrl(QUrl("https://viewer.riverscapes.net/software-help/help-qgis/")))

        self.about_action = QAction(
            QIcon(':/plugins/qrave_toolbar/viewer-icon.svg'),
            self.tr('About Riverscapes Viewer'),
            self.iface.mainWindow()
        )
        self.about_action.triggered.connect(self.about_load)
        self.actions.append(self.about_action)
        self.actions.append(self.raveOptionsAction)
        self.actions.append(self.net_sync_action)
        self.actions.append(self.find_resources_action)
        self.actions.append(self.helpAction)

        m.addAction(self.helpAction)
        m.addAction(self.about_action)
        self.helpButton.setDefaultAction(self.helpAction)

        # Add your actions to self.menu instead of directly to the toolbar

        self.menu.addAction(self.openProjectAction)
        self.menu.addAction(self.openRemoteProjectAction)
        self.menu.addAction(self.closeAllProjectsAction)

        # Add a separator and your tools/help submenus if desired
        self.menu.addSeparator()
        self.menu.addAction(self.browseExchangeProjectsAction)
        self.menu.addAction(self.downloadProjectAction)
        self.menu.addAction(self.openAction)
        self.menu.addMenu(self.toolsButton.menu())
        self.menu.addMenu(self.helpButton.menu())

        # Create a single QToolButton with icon and text, and set the menu
        self.viewer_button = QToolButton(self.toolbar)
        self.viewer_button.setText('  Riverscapes Viewer')
        self.viewer_button.setIcon(QIcon(':/plugins/qrave_toolbar/viewer-icon.svg'))
        self.viewer_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        self.viewer_button.setMenu(self.menu)
        self.viewer_button.setPopupMode(QToolButton.InstantPopup)  # Clicking anywhere opens the menu

        self.toolbar.addWidget(self.viewer_button)

        # Do a check to see if the stored version is different than the current version
        lastVersion = self.settings.getValue('pluginVersion')

        # This does a lazy netsync (i.e. it will run it if it feels like it)
        versionChange = lastVersion != __version__
        self.net_sync_load(force=versionChange)

        if versionChange:
            QgsMessageLog.logMessage("Version change detected: {} ==> {}".format(
                lastVersion, __version__), MESSAGE_CATEGORY, level=Qgis.Info)
            self.settings.setValue('pluginVersion', __version__)

        # Restore the dock widget visibility if it was open last time
        if self.settings.getValue('dockVisible') is True:
            # Note that toggle_widget will also restore the height
            self.toggle_widget(forceOn=True)
            if self.dockwidget is not None:
                self.dockwidget.reload_tree()

    def onProjectLoad(self, doc):
        # If the project has the plugin enabled then restore it.
        qrave_enabled, type_conversion_ok = self.qproject.readEntry(
            CONSTANTS['settingsCategory'],
            'enabled'
        )
        if type_conversion_ok and qrave_enabled == '1':
            self.dockwidget.fix_broken_project_paths()
            self.toggle_widget(forceOn=True)
            self.settings.setValue('dockVisible', True)
            if self.dockwidget is not None:
                self.dockwidget.reload_tree()

    def onClosePlugin(self):
        """Cleanup necessary items here when plugin dockwidget is closed"""
        if self.metawidget is not None:
            self.metawidget.hide()
        if self.dockwidget is not None:
            self._save_dock_height()
            self.dockwidget.hide()

        # disconnects
        try:
            self.dockwidget.closingPlugin.disconnect(self.onClosePlugin)
        except TypeError:
            pass  # Signal was not connected

        try:
            self.qproject.readProject.disconnect(self.onProjectLoad)
        except TypeError:
            pass  # Signal was not connected

        # remove this statement if dockwidget is to remain
        self.pluginIsActive = False

    def unload(self):
        """Removes the plugin menu item and icon from QGIS GUI."""
        if self.metawidget is not None:
            self.metawidget.hide()
            self.iface.removeDockWidget(self.metawidget)
            self.metawidget.deleteLater()

        if self.dockwidget is not None:
            self._save_dock_height()
            self.dockwidget.hide()
            self.iface.removeDockWidget(self.dockwidget)
            self.dockwidget.deleteLater()

        for action in self.actions:
            self.iface.removePluginMenu(
                self.tr(u'&Riverscapes Viewer Plugin'),
                action)
            self.iface.removeToolBarIcon(action)

        # Disconnect signals
        try:
            self.qproject.readProject.disconnect(self.onProjectLoad)
        except TypeError:
            pass

        # remove the toolbar
        if self.toolbar is not None:
            self.iface.mainWindow().removeToolBar(self.toolbar)
            self.toolbar.deleteLater()
            self.toolbar = None

    def toggle_widget(self, forceOn=False):
        """Toggle the widget open and closed when clicking the toolbar"""
        if not self.pluginIsActive:
            self.pluginIsActive = True

            # Hook metadata changes up to the metawidget
            self.dockwidget.metaChange.connect(self.metawidget.load)

            # Run a network sync operation to get the latest stuff. Don't force it.
            #  This is just a quick check
            self.net_sync_load()

            # connect to provide cleanup on closing of dockwidget
            self.dockwidget.closingPlugin.connect(self.onClosePlugin)
            dock_location  = Qt.LeftDockWidgetArea if self.settings.getValue('dockLocation') == "left" else Qt.RightDockWidgetArea

            # show the dockwidget
            self.iface.addDockWidget(dock_location, self.dockwidget)
            self.iface.addDockWidget(dock_location, self.metawidget)
            self.dockwidget.show()
            self._restore_dock_height()

        else:
            if self.dockwidget is not None:
                if self.dockwidget.isHidden():
                    self.dockwidget.show()
                    self._restore_dock_height()
                elif forceOn is False:
                    self._save_dock_height()
                    self.dockwidget.hide()

        # The metawidget always starts hidden
        if self.metawidget is not None:
            self.metawidget.hide()

        if self.dockwidget is not None and not self.dockwidget.isHidden():
            
            # Check if the project is already enabled. 'readEntry' might return '1' or True/False
            qrave_enabled, type_conversion_ok = self.qproject.readEntry(
                CONSTANTS['settingsCategory'],
                'enabled'
            )
            already_enabled = type_conversion_ok and (qrave_enabled == '1' or qrave_enabled is True)
            
            # If we are just restoring the window state on startup (forceOn=True) and the project 
            # is a clean/new project, we should NOT write to the project to avoid dirtying it.
            # This prevents the "Do you want to save..." dialog when opening QGIS with the plugin active.
            is_pristine = not self.qproject.fileName() and not self.qproject.isDirty()
            
            should_write = True
            if already_enabled:
                should_write = False
            elif forceOn and is_pristine:
                should_write = False

            if should_write:
                self.qproject.writeEntry(
                    CONSTANTS['settingsCategory'], 'enabled', True)
            
            self.settings.setValue('dockVisible', True)
        else:
            self.qproject.removeEntry(CONSTANTS['settingsCategory'], 'enabled')
            self.settings.setValue('dockVisible', False)

    def _save_dock_height(self):
        """Save the height of the dock widget to settings."""
        if self.dockwidget is not None:
            h = self.dockwidget.height()
            # If the widget is hidden, height() might return 0 or its last size.
            # We only save if it's positive.
            if h > 0:
                self.settings.setValue('dockHeight', h)

    def _restore_dock_height(self):
        """Restore the height of the dock widget from settings."""
        saved_height = self.settings.getValue('dockHeight')
        if saved_height and self.dockwidget:
            self.iface.mainWindow().resizeDocks([self.dockwidget], [saved_height], Qt.Vertical)

    def net_sync_load(self, force=False):
        """
        Periodically check for new files
        """

        lastDigestSync = self.settings.getValue('lastDigestSync')
        lastVersion = self.settings.getValue('pluginVersion')

        currTime = int(time())  # timestamp in seconds
        plugin_init = self.settings.getValue('initialized')
        autoUpdate = self.settings.getValue('autoUpdate')

        self.netsync = NetSync('Sync Riverscapes resource files')

        perform_sync = False

        # setting the force flag overrides everything else
        if force:
            perform_sync = True

        # Otherwise you only get a sync if 'autoUpdate' is turned on
        elif autoUpdate:
            # If this is an old version or the plugin is uninitialized
            if not plugin_init or lastVersion != __version__ or self.netsync.need_sync:
                perform_sync = True
            # If we haven't checked for more than `digestSyncFreqHours` hours
            elif isinstance(lastDigestSync, int) \
                    and ((currTime - lastDigestSync) / 3600) > CONSTANTS['digestSyncFreqHours']:
                perform_sync = True

        if perform_sync is False:
            self.netsync = None
            return

        # Trigger the dockwidget to repaint after the netsync
        if self.dockwidget is not None:
            self.netsync.taskCompleted.connect(self.dockwidget.reload_tree)

        # FOR DEBUGGING ONLY. NEVER IN PRODUCTION
        # self.netsync.run()

        # COMMENT THIS OUT AND USE THE LINE ABOVE FOR SYNCHRONOUS DEBUGGING
        self.tm.addTask(self.netsync)

    def projectBrowserDlg(self):
        """
        Browse for a project directory
        :return:
        """
        last_browse_path = self.settings.getValue('lastBrowsePath')
        last_dir = os.path.dirname(
            last_browse_path) if last_browse_path is not None else None

        dialog_return = QFileDialog.getOpenFileName(
            self.dockwidget, "Open a Riverscapes project", last_dir, self.tr("Riverscapes Project files (project.rs.xml)"))
        if dialog_return is not None and len(dialog_return[0]) > 0:
            # Remember this path for next time
            self.settings.setValue('lastBrowsePath', dialog_return[0])
        if dialog_return is not None and dialog_return[0] != "" and os.path.isfile(dialog_return[0]):
            # We set the proect path in the project settings. This way it will be saved with the QgsProject file
            if self.dockwidget is None or self.dockwidget.isHidden() is True:
                self.toggle_widget(forceOn=True)
            self.dockwidget.add_project(dialog_return[0])

    def downloadProjectDlg(self, project_id: str = None, local_path: str = None):
        """
        Open the download dialog
        """
        dialog = ProjectDownloadDialog(self.iface.mainWindow(), project_id=project_id, local_path=local_path)
        dialog.projectDownloaded.connect(self.dockwidget.add_project)
        dialog.exec_()

    def remoteProjectDlg(self):
        """
        Open a dialog to enter a project ID or URL
        """
        dialog = RemoteProjectDialog(self.iface.mainWindow())
        if dialog.exec_():
            text = dialog.get_text()
            ok = True
        else:
            text = ""
            ok = False
        
        if ok and text:
            # Extract project ID from URL if necessary
            project_id = text.strip()
            if '/' in project_id:
                project_id = project_id.rstrip('/').split('/')[-1]
            
            # Validate that this is a proper guid like 4dd028c6-3e9f-4b14-a317-fe74903ed279
            if not re.match(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', project_id):
                QMessageBox.warning(self.iface.mainWindow(), "Invalid Project ID", "The project ID you entered is invalid.")
                return

            if self.dockwidget is None or self.dockwidget.isHidden() is True:
                self.toggle_widget(forceOn=True)

            if self.dockwidget:
                self.dockwidget.show_loading(project_id)

            # Use DataExchangeAPI to fetch the project
            self.dataExchangeAPI = DataExchangeAPI(on_login=lambda task: self._on_remote_login(task, project_id))

    def _on_remote_login(self, task, project_id):
        if task.success:
            self.dataExchangeAPI.get_remote_project(project_id, self._on_remote_project_fetched)
        else:
            if self.dockwidget:
                self.dockwidget.hide_loading()
            QMessageBox.critical(self.iface.mainWindow(), "Login Failed", "Could not log in to Riverscapes API.")

    def _on_remote_project_fetched(self, task, response):
        if self.dockwidget:
            self.dockwidget.hide_loading()

        if task.success and response and 'data' in response and response['data']['project']:
            if self.dockwidget is None or self.dockwidget.isHidden() is True:
                self.toggle_widget(forceOn=True)
            self.dockwidget.add_remote_project(response)
        else:
            err_msg = f"Could not find project with ID: {task.variables.get('id')}"
            if task.error:
                err_msg += f"\n\nError: {task.error}"
            elif response and 'errors' in response:
                import json
                err_msg += f"\n\nAPI Errors:\n{json.dumps(response['errors'], indent=2)}"
            elif response and 'data' in response and response['data']['project'] is None:
                err_msg += "\n\nNote: The API returned null for this project ID. It may be private or deleted."
            
            QMessageBox.warning(self.iface.mainWindow(), "Project Not Found", err_msg)

    def closeAllProjects(self):
        """Close all open projects"""
        if self.dockwidget is not None:
            if len(self.dockwidget._get_projects()) > 0:
                msgBox = QMessageBox()
                msgBox.setWindowTitle("Close All Riverscapes Projects?")
                msgBox.setIcon(QMessageBox.Question)
                msgBox.setText(
                    "Are you sure that you want to close all Riverscapes projects? This will also remove the layers related to these projects from your current map document.")
                # msgBox.setInformativeText("Are you sure that you want to close all Riverscapes projects? This will also remove the layers related to these projects from your current map document.")
                msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
                msgBox.setDefaultButton(QMessageBox.No)
                response = msgBox.exec_()
                if response == QMessageBox.Yes:
                    self.dockwidget.close_all()
                    if self.metawidget is not None:
                        self.metawidget.clear_and_hide()

    def show_project_bounds(self):
        """
        Open the project bounds dialog
        """
        # Check if there are any Vector layers in the project
        layers = QgsProject.instance().mapLayers()
        if not any(layer.type() == QgsMapLayer.VectorLayer for layer in layers.values()):
            QMessageBox.information(None, "No layers", "There are no Vector Layers in the map. Please add at least one Vector Layer to the map before generating project bounds.")
            return

        dialog = FrmProjectBounds()
        dialog.exec_()

    def locateResources(self):
        """This the OS-agnostic "show in Finder" or "show in explorer" equivalent
        It should open the folder of the item in question

        Args:
            fpath (str): [description]
        """
        qurl = QUrl.fromLocalFile(RESOURCES_DIR)
        QDesktopServices.openUrl(qurl)

    def options_load(self):
        """
        Open the options/settings dialog
        """
        dialog = OptionsDialog()
        if self.dockwidget:
            dialog.dataChange.connect(self.dockwidget.dataChange)
            dialog.dataChange.connect(self.redock_widgets)
        dialog.exec_()

    def about_load(self):
        """
        Open the About dialog
        """
        dialog = AboutDialog()
        if self.acknowledgements is None:
            self.acknowledgements = '<a href="{0}">{0}</a>'.format('https://viewer.riverscapes.net/about/acknowledgements')

        dialog.acknowledgements.setText(self.acknowledgements)
        dialog.exec_()

    def redock_widgets(self):
        """Redock the widget according to the saved settings.
        If the widget is hidden, set the new dock location but do not show it.
        """
        dock_location = Qt.LeftDockWidgetArea if self.settings.getValue('dockLocation') == "left" else Qt.RightDockWidgetArea

        # Redock main dockwidget
        if self.dockwidget is not None:
            was_visible = not self.dockwidget.isHidden()
            self.iface.removeDockWidget(self.dockwidget)
            self.iface.addDockWidget(dock_location, self.dockwidget)
            if not was_visible:
                self.dockwidget.hide()

        # Redock metadata widget
        if self.metawidget is not None:
            was_visible = not self.metawidget.isHidden()
            self.iface.removeDockWidget(self.metawidget)
            self.iface.addDockWidget(dock_location, self.metawidget)
            if not was_visible:
                self.metawidget.hide()

    def browseExchangeProjects(self):

        # Get the center and zoom level to build the search url
        canvas = self.iface.mapCanvas()
        center = get_map_center(canvas)
        zoom = get_zoom_level(canvas)
        search_url = f"{CONSTANTS['warehouseUrl']}/s?type=Project&bounded=1&view=map&geo={center.x()}%2C{center.y()}%2C{zoom}"
        # Open the URL in the default web browser
        QDesktopServices.openUrl(QUrl(search_url))
