class GeoServerWidget(ServerWidgetBase, BASE, WIDGET):

    def __init__(self, parent, server_type):
        super().__init__(parent, server_type)
        self.setupUi(self)

        self.geoserverAuth = QgsAuthConfigSelect()
        self.geoserverAuth.selectedConfigIdChanged.connect(self.setDirty)
        self.addAuthWidget()

        self.populateStorageCombo()
        self.comboStorageType.currentIndexChanged.connect(self.datastoreChanged)
        self.btnRefreshDatabases.clicked.connect(partial(self.updateDbServersCombo, True))
        self.btnAddDatastore.clicked.connect(self.addPostgisDatastore)
        self.txtGeoserverName.textChanged.connect(self.setDirty)
        self.txtGeoserverUrl.textChanged.connect(self.setDirty)
        self.chkUseOriginalDataSource.stateChanged.connect(self.setDirty)
        self.chkUseVectorTiles.stateChanged.connect(self.setDirty)
        self.comboGeoserverDatabase.currentIndexChanged.connect(self.setDirty)

        # Declare progress dialog
        self._pgdialog = None

    def createServerInstance(self):
        """ Reads the settings form fields and returns a new server instance with these settings. """
        db = None
        storage = self.comboStorageType.currentIndex()
        if storage in (GeoserverStorage.POSTGIS_BRIDGE, GeoserverStorage.POSTGIS_GEOSERVER):
            db = self.comboGeoserverDatabase.currentText()

        try:
            name = self.txtGeoserverName.text().strip()
            url = self.txtGeoserverUrl.text().strip()
            if not name:
                raise RuntimeError(f'missing {self.serverType.getLabel()} name')
            if not url:
                raise RuntimeError(f'missing {self.serverType.getLabel()} URL')

            return self.serverType(
                name=name,
                authid=self.geoserverAuth.configId() or None,
                url=url,
                storage=storage,
                postgisdb=db,
                useOriginalDataSource=self.chkUseOriginalDataSource.isChecked(),
                useVectorTiles=self.chkUseVectorTiles.isChecked()
            )
        except Exception as e:
            self.parent.logError(f"Failed to create {self.serverType.getLabel()} instance: {e}")
            return None

    def newFromName(self, name: str):
        """ Sets the name field and keeps all others empty. """
        self.txtGeoserverName.setText(name)
        self.txtGeoserverUrl.clear()
        self.geoserverAuth.setConfigId(None)

        # Set datastore and database comboboxes
        self.comboStorageType.blockSignals(True)
        self.comboStorageType.setCurrentIndex(GeoserverStorage.FILE_BASED)
        self.datastoreChanged(GeoserverStorage.FILE_BASED)
        self.chkUseOriginalDataSource.setChecked(False)
        self.chkUseVectorTiles.setChecked(False)
        self.comboStorageType.blockSignals(False)

    def loadFromInstance(self, server):
        """ Populates the form fields with the values from the given server instance. """
        self.txtGeoserverName.setText(server.serverName)
        self.txtGeoserverUrl.setText(server.baseUrl)
        self.geoserverAuth.setConfigId(server.authId)

        # Set datastore and database comboboxes
        self.comboStorageType.blockSignals(True)
        self.comboStorageType.setCurrentIndex(server.storage)
        self.datastoreChanged(server.storage, server.postgisdb)
        self.chkUseOriginalDataSource.setChecked(server.useOriginalDataSource)
        self.chkUseVectorTiles.setChecked(server.useVectorTiles)
        self.comboStorageType.blockSignals(False)

        # After the data has loaded, the form is "clean"
        self.setClean()

    def addAuthWidget(self):
        layout = QHBoxLayout()
        layout.setContentsMargins(0, 3, 0, 0)
        layout.addWidget(self.geoserverAuth)
        self.geoserverAuthWidget.setLayout(layout)
        self.geoserverAuthWidget.setFixedHeight(self.txtGeoserverUrl.height())

    def populateStorageCombo(self):
        self.comboStorageType.clear()
        self.comboStorageType.addItems(GeoserverStorage.values())

    def datastoreChanged(self, storage, init_value=None):
        """ Called each time the database combobox selection changed. """
        if storage is None:
            storage = GeoserverStorage[self.comboStorageType.currentIndex()]
        if storage == GeoserverStorage.POSTGIS_BRIDGE:
            self.updateDbServersCombo(False, init_value)
            self.comboGeoserverDatabase.setVisible(True)
            self.labelGeoserverDatastore.setText('Database')
            self.labelGeoserverDatastore.setVisible(True)
            self.datastoreControls.setVisible(False)
        elif storage == GeoserverStorage.POSTGIS_GEOSERVER:
            self.comboGeoserverDatabase.setVisible(True)
            self.labelGeoserverDatastore.setText('Datastore')
            self.labelGeoserverDatastore.setVisible(True)
            self.datastoreControls.setVisible(True)
            self.updateDbServersCombo(True, init_value)
        elif storage == GeoserverStorage.FILE_BASED:
            self.comboGeoserverDatabase.setVisible(False)
            self.labelGeoserverDatastore.setVisible(False)
            self.datastoreControls.setVisible(False)
        self.setDirty()

    def addGeoserverPgDatastores(self, current, result):
        if self._pgdialog and self._pgdialog.isVisible():
            self._pgdialog.hide()
        if result:
            # Worker result might be a list of lists, so we should flatten it
            datastores = list(chain.from_iterable(result))
            self.comboGeoserverDatabase.addItems(datastores)
            if current:
                self.comboGeoserverDatabase.setCurrentText(current)
        else:
            self.parent.showWarningBar("Warning", "No PostGIS datastores on server or could not retrieve them")

    def showProgressDialog(self, text, length, handler):
        self._pgdialog = QProgressDialog(text, "Cancel", 0, length, self)
        self._pgdialog.setWindowTitle(getAppName())
        self._pgdialog.setWindowModality(QtCore.Qt.WindowModal)
        self._pgdialog.canceled.connect(handler, type=QtCore.Qt.DirectConnection)
        self._pgdialog.forceShow()

    def updateDbServersCombo(self, managed_by_geoserver: bool, init_value=None):
        """ (Re)populate the combobox with database-driven datastores.

        :param managed_by_geoserver:    If True, GeoServer manages the DB connection. If False, Bridge manages it.
        :param init_value:              When the combobox shows for the first time and no databases have been loaded,
                                        this value can be set immediately as the only available and selected item.
                                        Doing so prevents a full refresh of GeoServer datastores.
        """
        if managed_by_geoserver and init_value:
            if self.comboGeoserverDatabase.count() == 0:
                # Only add the given init_value item to the empty combo (user should manually refresh)
                self.comboGeoserverDatabase.addItem(init_value)
                self.comboGeoserverDatabase.setCurrentText(init_value)
                return
            # If combo has values, try and find the init_value and set it to that (user should manually refresh)
            index = self.comboGeoserverDatabase.findText(init_value)
            if index >= 0:
                self.comboGeoserverDatabase.setCurrentIndex(index)
                return

        current_db = self.comboGeoserverDatabase.currentText() or init_value
        self.comboGeoserverDatabase.clear()

        if managed_by_geoserver:
            # Database is managed by GeoServer: instantiate server and retrieve datastores
            # TODO: only PostGIS datastores are supported for now
            server = self.createServerInstance()
            if not server:
                self.parent.showErrorBar("Error", "Bad values in server definition")
                return

            try:
                # Retrieve workspaces (single REST request)
                workspaces = gui.execute(server.getWorkspaces)
                if not workspaces:
                    return
                # Retrieve datastores for each workspace:
                # This is a potentially long-running operation and uses a separate QThread
                worker = gui.ItemProcessor(workspaces, server.getPostgisDatastores)
                self.showProgressDialog("Fetching PostGIS datastores...", len(workspaces), worker.stop)
                worker.progress.connect(self._pgdialog.setValue)
                worker.finished.connect(partial(self.addGeoserverPgDatastores, current_db))
                worker.run()
            except Exception as e:
                msg = f'Failed to retrieve datastores for {self.serverName}'
                if isinstance(e, HTTPError) and e.response.status_code == 401:
                    msg = f'{msg}: please check credentials'
                else:
                    msg = f'{msg}: {e}'
                self.parent.showErrorBar(msg)

        else:
            # Database is managed by Bridge: iterate over all user-defined database connections
            db_servers = self.parent.serverManager.getDbServerNames()
            self.comboGeoserverDatabase.addItems(db_servers)
            if current_db in db_servers:
                self.comboGeoserverDatabase.setCurrentText(current_db)

    def addPostgisDatastore(self):
        server = self.createServerInstance()
        if server is None:
            self.parent.showErrorBar("Error", "Wrong values in server definition")
            return
        dlg = GeoserverDatastoreDialog(self)
        dlg.exec_()
        name = dlg.name
        if name is None:
            return

        def _entry(k, v):
            return {"@key": k, "$": v}

        ds = {
            "dataStore": {
                "name": dlg.name,
                "type": "PostGIS",
                "enabled": True,
                "connectionParameters": {
                    "entry": [
                        _entry("schema", dlg.schema),
                        _entry("port", dlg.port),
                        _entry("database", dlg.database),
                        _entry("passwd", dlg.password),
                        _entry("user", dlg.username),
                        _entry("host", dlg.host),
                        _entry("dbtype", "postgis")
                    ]
                }
            }
        }
        try:
            gui.execute(partial(server.addPostgisDatastore, ds))
        except Exception as e:
            self.parent.showErrorBar("Error", "Could not create new PostGIS dataset", propagate=e)
        else:
            self.updateDbServersCombo(True)
def isochrone(
        database_name,
        host_name,
        port_number,
        user_name,
        password,
        network,
        network_geom,
        network_id_column,
        catchment,
        catchment_geom,
        catchment_id_column,
        style_checked,
        contour_interval,
        parent_dialog,
        progress_dialog=None):

    """Contains main logic on creating isochrone map
    :param database_name: Database name
    :type database_name: str

    :param host_name: Database host
    :type host_name: str

    :param port_number: Port number for the host
    :type port_number: str

    :param user_name: Username for connection with database
    :type user_name: str

    :param password: Password
    :type password: str

    :param network: Schema and Table containing the network.
    :type network: str

    :param network_geom: Geometry column in network.
    :type network_geom: str

    :param network_id_column: Id column in network.
    :type network_id_column: str

    :param catchment: Schema and Table containing catchment areas.
    :type catchment: str

    :param catchment_geom: Geometry column in catchment.
    :type catchment_geom: str

    :param catchment_id_column: Id column in catchment.
    :type catchment_id_column: str

    :param style_checked: Value for either to create a map style
    or not.
    :type  style_checked: boolean

    :param contour_interval: Interval between contour, if contour
    will be generated
    :type  contour_interval: int

    :param parent_dialog: A dialog that called this function.
    :type parent_dialog: QProgressDialog

    :param progress_dialog: A progess dialog .
    :type progress_dialog: QProgressDialog

    :returns layer_name: temporary path of the isochrones map layer
    :rtype layer_name: str
    """

    # Import files into database, have tables
    # connect to database
    # add the files get the tables name

    connection = psycopg2.connect(
        "dbname='" + str(database_name) + "' "
        "user='******' "
        "host='" + str(host_name) + "' "
        "port='" + str(port_number) + "' "
        "password='******' ")

    curr = connection.cursor()

    # Setting up progress window
    progress_dialog = QProgressDialog('Progress', 'Cancel', 0, 100)
    progress_dialog.forceShow()
    progress_dialog.setWindowModality(Qt.WindowModal)

    network_array = network.split('.')
    network_table = str(network_array[1])
    network_schema = network_array[0]
    catchment = catchment.split('.')
    catchment_table = catchment[1]
    catchment_schema = catchment[0]

    if not network_geom:
        network_geom = "geom"
    if not catchment_geom:
        catchment_geom = "geom"

    arguments = {}
    arguments["network_table"] = network_table
    arguments["network_geom"] = network_geom
    arguments["network_id"] = network_id_column
    arguments["catchment_table"] = catchment_table
    arguments["catchment_geom"] = catchment_geom
    arguments["catchment_id"] = catchment_id_column
    arguments["database_name"] = database_name
    arguments["port_number"] = port_number

    try:

        create_network_view(
            connection,
            curr,
            arguments,
            parent_dialog,
            progress_dialog)

        create_nodes(connection, curr, arguments, parent_dialog)

        # Create routable network
        progress_percentage = 10

        progress_dialog.setValue(progress_percentage)
        label_text = tr("Creating a routable network table")
        progress_dialog.setLabelText(label_text)

        if progress_dialog.wasCanceled():
            return

        create_routable_network(connection, curr, arguments, parent_dialog)

        # Find nearest nodes from the catchments
        progress_percentage = 30
        progress_dialog.setValue(progress_percentage)
        label_text = tr("Preparing the catchment table")
        progress_dialog.setLabelText(label_text)

        if progress_dialog.wasCanceled():
            return

        update_catchment(connection, curr, arguments, parent_dialog)

        # Calculate drivetimes for the nearest nodes

        progress_percentage = 50

        progress_dialog.setValue(progress_percentage)
        label_text = tr("Calculating drivetime for each catchment area")
        progress_dialog.setLabelText(label_text)

        if progress_dialog.wasCanceled():
            return

        progress_percentage = calculate_drivetimes(
            connection,
            curr,
            arguments,
            progress_dialog,
            parent_dialog,
            progress_percentage)

        if progress_dialog.wasCanceled():
            return

        prepare_drivetimes_table(connection, curr, arguments, parent_dialog)

        uri = QgsDataSourceUri()
        # set host name, port, database name, username and password
        uri.setConnection(
            host_name,
            port_number,
            database_name,
            user_name,
            password)
        # set database schema, table name, geometry column and optionally
        # subset (WHERE clause)
        uri.setDataSource(
            network_schema,
            "catchment_final_no_null",
            "the_geom")

        # Export table as a shapefile

        layer = QgsVectorLayer(uri.uri(), "isochrones", "ogr")
        temp_layer = QgsVectorLayer(uri.uri(), "isochrones", "postgres")

        QgsProject.instance().addMapLayers([temp_layer])

        if iface:
            iface.mapCanvas().refresh()

        layer_name = temp_layer.dataProvider().dataSourceUri()

        if style_checked:
            args = {}
            args['network_schema'] = network_schema
            args['network_table'] = network_table
            args['network_geom'] = network_geom
            args['catchment_schema'] = catchment_schema
            args['catchment_table'] = catchment_table
            args['catchment_geom'] = catchment_geom

            prepare_map_style(
                    uri,
                    progress_percentage,
                    contour_interval,
                    progress_dialog,
                    temp_layer,
                    parent_dialog,
                    args)

        if progress_dialog:
            progress_dialog.setValue(100)
            progress_dialog.done(QDialog.Accepted)

        return layer_name

    except (IsochroneDBError,
            IsochroneMapStyleError
            ) as exception:
        return None