# -*- encoding=utf-8 -*-
""" Spatialite graph builder implementation
"""
from __future__ import absolute_import, annotations

from builtins import object
import os
import logging

from .places import PlaceBuilder, build_edges_graph
from . import edge_properties as props
from .errors import DatabaseNotFound
from .sql import SQL, execute_sql, delete_table, connect_database, set_srid
from .layers import check_layer, import_as_layer, export_as_geopakage
from .processing_on_layer import process_layer_before_exporting_to_database
from qgis._core import QgsWkbTypes, QgsProject
from qgis.core import QgsVectorFileWriter, QgsVectorLayer
from sqlite3.dbapi2 import Connection


class BuildOptions():
    def __init__(self, conn: Connection, table: str):
        self.connection = conn
        self.table = table
        self.snap_distance: float = 0.20  # Snap minimum distance
        self.min_edge_length: float = 4.00  # Minimum length for small edges
        self.is_topologically_correct: bool = False
        self.attribute = None  # Attribute to import from original data
        self.output: str | None = None


class SpatialiteBuilder(object):

    version = "1.0"
    description = "Spatialite graph builder"

    def __init__(self, db_path, table=None):
        """ Initialize builder

            :param db_path: the path of the database
            :param table: name of the table containing input data
        """
        logging.info(f"Opening database {db_path}")
        self._conn = connect_database(db_path)
        self._db_path = db_path
        self._basename = os.path.basename(os.path.splitext(db_path)[0])

        self._input_table = table or self._basename.lower()
        self._way_builder = None

    @property
    def way_builder(self):
        if self._way_builder is None:
            from .ways import WayBuilder
            self._way_builder = WayBuilder(self._conn)
        return self._way_builder

    @property
    def connection(self) -> Connection:
        return self._conn

    def build_options(self) -> BuildOptions:
        return BuildOptions(self._conn, self._input_table)

    def build_graph(self, options: BuildOptions) -> None:
        """ Build morpheo topological graph

            This method will build the topological graph
            - The vertices table
            - The edges table

            :param snap_distance: The snap distance used to sanitize  the graph,
                 If the snap_distance is > 0 the graph will be sanitized (merge close vertices,
                 remove unconnected features, the result will be a topological graph
            :param min_edge_length: The minimum edge length - edge below this length will be removed
            :param way_attribute: The attribute which will be used to build way from street name.
                If defined, this attribute has to be imported into the topological graph.
        """
        from .sanitize import sanitize

        snap_distance = options.snap_distance
        output = options.output
        way_attribute = options.attribute

        # sanitize graph
        if snap_distance > 0:
            logging.info("Builder: sanitizing graph")
            working_table = sanitize(options)

            # Because we still have rounding errors
            # We need to round coordinates using
            precision = snap_distance/2.0

            logging.info(f"Builder: rounding coordinates to {precision} m precision")
#            cur = self._conn.cursor()
#            update_request = f"UPDATE {working_table} SET GEOMETRY = (SELECT ST_SnapToGrid({working_table}.GEOMETRY,{precision}))"
#            cur.execute(SQL(update_request))
#            cur.close()

            if output is not None:
                self._conn.commit()
                logging.info("Builder: saving sanitized graph")
                export_as_geopakage(self._db_path, working_table, output)
        else:
            working_table = self._input_table

        # Compute edges, way, vertices
        logging.info("Builder: Computing vertices and edges")
        self.execute_sql('graph.sql', input_table=working_table)

        # Copy attribute to graph edge
        if way_attribute:
            cur = self._conn.cursor()
            update_request = f"""
                                UPDATE edges SET NAME = (
                                    SELECT {way_attribute} 
                                    FROM {working_table} AS b
                                    WHERE edges.OGC_FID = b.OGC_FID)
                              """
            cur.execute(SQL(update_request))
            cur.close()

        # Update parameters
        self._conn.commit()
        if output is not None:
            logging.info("Builder: saving edges and vertices")
            export_as_geopakage(self._db_path, 'edges', output)
            export_as_geopakage(self._db_path, 'vertices', output)

            snap_distance = options.snap_distance
            min_edge_length = options.min_edge_length
            self.write_manifest(output, 'build', snap_distance=snap_distance, min_edge_length=min_edge_length)

    def write_manifest(self, output, suffix, **kwargs) -> None:
        """ Write  manifest as key=value file 
        """
        with open(os.path.join(output, f"{self._basename}_{suffix}.manifest"), 'w') as f:
            for k, v in kwargs.items():
                f.write(f"{k}={v}\n")

    def build_edges_graph(self, output) -> None:
        """ Build and export edge graph
        """
        build_edges_graph(self._conn, output)

    def build_ways_graph(self, output: str) -> None:
        """ Build and export way line graph
        """
        builder = self.way_builder
        builder.save_line_graph(output, create=True)

    def build_places(self, buffer_size, places=None, output=None) -> None:
        """ Build places

            Build places from buffer and/or external places definition.
            If buffer is defined and > 0 then a buffer is applied to all vertices for defining
            'virtual' places in the edge graph.

            If places definition is used, these definition are used like the 'virtual' places definition. Intersecting
            places definition and 'virtual' places are merged.

            :param buffer_size: buffer size applied to vertices
            :param places: path of an external shapefile containing places definitions
            :param output: path of a shapefile to write computed places to.
        """
        input_places_table = None
        if places is not None:
            input_places_table = 'input_places'
            # Delete table if it exists
            delete_table(self._conn.cursor(), input_places_table)
            import_as_layer(self._db_path, places, input_places_table)
            # Force srid
            set_srid(self._conn.cursor(), input_places_table, 'vertices')

        builder = PlaceBuilder(self._conn)
        builder.build_places(buffer_size, input_places_table)

        if output is not None:
            builder.export(self._db_path, output, export_graph=True)
            self.write_manifest(output, 'places', buffer_size=buffer_size, input_file=places)

    def build_ways(self,  threshold, output=None, attributes=False, rtopo=False, **kwargs) -> None:
        """ Build way's hypergraph

            :param threshold: Angle treshold
            :param output: output shapefile to store results
            :param attributes: compute attributes
        """
        builder = self.way_builder
        builder.build_ways(threshold)

        if rtopo:
            builder.compute_topological_radius()

        if attributes:
            self.compute_way_attributes(**kwargs)

        if output is not None:
            builder.export(self._db_path, output, export_graph=True)
            self.write_manifest(output, 'ways', angle_threshold=threshold)

    def compute_way_attributes(self, orthogonality, betweenness, closeness, stress, classes=10, rtopo=False, output=None) -> None:
        """ Compute attributes for ways:

            :param orthogonality: If True, compute orthogonality.
            :param betweenness:   If True, compute betweenness centrality.
            :param stress:        If True, compute stress centrality.
            :param closeness:     If True, compute closeness.
        """
        builder = self.way_builder
        builder.compute_local_attributes(orthogonality=orthogonality, classes=classes)

        if rtopo:
            builder.compute_topological_radius()

        if any((betweenness, closeness, stress)):
            builder.compute_global_attributes(
                    betweenness=betweenness,
                    closeness=closeness,
                    stress=stress,
                    classes=classes)

        if output is not None:
            builder.export(self._db_path, output)

    def compute_edge_attributes(self, path, orthogonality, betweenness, closeness, stress, classes=10, output=None) -> None:
        """ Compute attributes for edges:

            :param orthogonality: If True, compute orthogonality.
            :param betweenness:   If True, compute betweenness centrality.
            :param stress:        If True, compute stress centrality.
            :param closeness:     If True, compute closeness.
        """
        props.compute_local_attributes(self._conn, orthogonality=orthogonality, classes=classes)

        if any((betweenness, closeness, stress)):
            props.compute_global_attributes(
                    self._conn,
                    path,
                    betweenness=betweenness,
                    closeness=closeness,
                    stress=stress,
                    classes=classes)

        if output is not None:
            export_as_geopakage(self._db_path, 'place_edges', output)

    def build_ways_from_attribute(self, attribute, output=None, attributes=False, rtopo=False, export_graph=False, **kwargs) -> None:
        """ Build way's hypergraph from street names.

            Note that the attribute name need to be specified
            when  computing the topological graph

            :param output_file: Output shapefile to store result
        """
        builder = self.way_builder
        builder.build_ways_from_attribute(attribute)

        if rtopo:
            builder.compute_topological_radius()

        if attributes:
            self.compute_way_attributes(**kwargs)

        if output is not None:
            builder.export(self._db_path, output, export_graph=export_graph)

    def execute_sql(self, name: str, **kwargs) -> None:
        """ Execute statements from sql file
        """
        execute_sql(self._conn, name, **kwargs)

    def way_graph(self) -> None:
        """ Return the way line graph
        """
        return self.way_builder.get_line_graph()

    @staticmethod
    def from_layer(layer: QgsVectorLayer, db_path: str, is_test_case: bool = False) -> SpatialiteBuilder:
        """ Build graph from qgis layer
        """

        preprocessed_layer = process_layer_before_exporting_to_database(layer)

        # Ensure layer validity
        if not is_test_case:
            check_layer(preprocessed_layer, (QgsWkbTypes.LineString25D, QgsWkbTypes.LineString, QgsWkbTypes.LineStringZ, QgsWkbTypes.LineStringM, QgsWkbTypes.LineStringZM))

        # Ensure the database can be created
        if os.path.isfile(db_path):
            logging.info(f"Removing existing database {db_path}")
            os.remove(db_path)

        # Create database from layer
        logging.info(f"Creating database '{db_path}' from layer")
        transform_context = QgsProject.instance().transformContext()
        save_options = QgsVectorFileWriter.SaveVectorOptions()
        save_options.driverName = "SpatiaLite"
        save_options.fileEncoding = "UTF-8"
        error = QgsVectorFileWriter.writeAsVectorFormatV3(preprocessed_layer, db_path, transform_context, save_options)

        # Handle errors
        if error[0] != 0:
            raise IOError(f"Failed to create database '{db_path}': error {error}")

        return SpatialiteBuilder(db_path)

    @staticmethod
    def from_database(dbname: str) -> SpatialiteBuilder:
        """" Open existing database

             :param dbname: Path of the database:
             :returns: A builder object
        """
        dbname = f"{dbname}.sqlite"
        dbname_gpkg = f"{dbname}.gpkg"
        if not os.path.isfile(dbname):
            if not os.path.isfile(dbname_gpkg):
                raise DatabaseNotFound(dbname)
            return SpatialiteBuilder(dbname_gpkg)
        return SpatialiteBuilder(dbname)






