import pathlib

from qgis.core import (
    QgsPointCloudLayer,
    QgsProcessingAlgorithm,
    QgsProcessingException,
    QgsProcessingFeedback,
    QgsProcessingParameterBoolean,
    QgsProcessingParameterFolderDestination,
    QgsProcessingParameterPointCloudLayer,
    QgsProcessingParameterVectorLayer,
    QgsProviderRegistry,
    QgsVectorLayer,
)
from qgis.PyQt.QtCore import QCoreApplication

from CityForge.toolbelt.preferences import PlgOptionsManager
from CityForge.toolbelt.ProgressEmitter import ProgressEmitter

from ..reconstruct import JsonConstructor


def pad(log: str):
    return "\n".join([f"> {s}" for s in log.splitlines()])


class BuildingModelBuilder(QgsProcessingAlgorithm):

    def tr(self, message: str):
        """
        Returns a translatable string with the self.tr() function.
        """
        return QCoreApplication.translate(self.__class__.__name__, message)

    def createInstance(self):
        return BuildingModelBuilder()

    def name(self):
        """
        Returns the algorithm name, used for identifying the algorithm. This
        string should be fixed for the algorithm, and must not be localised.
        The name should be unique within each provider. Names should contain
        lowercase alphanumeric characters only and no spaces or other
        formatting characters.
        """
        return "-cityjson-builder"

    def displayName(self):
        """
        Returns the translated algorithm name, which should be used for any
        user-visible display of the algorithm name.
        """
        return self.tr("Generate CityJSON file")

    def group(self):
        """
        Returns the name of the group this algorithm belongs to. This string
        should be localised.
        """
        return self.tr("CityForge")

    def shortHelpString(self):
        """
        Returns a localised short helper string for the algorithm. This string
        should provide a basic description about what the algorithm does and the
        parameters and outputs associated with it.
        """

        return self.tr(
            """Uses geoflow for generating a CityJSON file.
            Takes the point cloud and the 2D polygon of one or multiple buildings.
             Please provide an extent file (shp or gpkg) and the corresponding point cloud file (copc.laz, laz,las).
             The processing always saves a JSON file.
             You can choose to:

                 - Generate .OBJ files

                 - Generate GPKG: with LOD 0

                 - Generate only LOD 2.2 : by default, LOD 0, 1.2, 1.3, and 2.2 are generated
                """
        )

    def initAlgorithm(self, config=None):  # pylint: disable=unused-argument
        """
        Here we define the inputs and output of the algorithm, along
        with some other properties.
        """
        self.ptc = "pointcloud"
        self.ftp = "footprint"
        self.json = "json"
        self.lod22 = "lod22"
        self.obj = "obj"
        self.gpkg = "gpkg"

        self.addParameter(
            QgsProcessingParameterPointCloudLayer(
                self.ptc,
                self.tr("Select PointCloud file las or laz"),
            )
        )

        self.addParameter(
            QgsProcessingParameterVectorLayer(
                self.ftp,
                self.tr("Select footprint input file"),
            )
        )

        self.addParameter(
            QgsProcessingParameterFolderDestination(
                self.json, self.tr("Output directory")
            )
        )

        self.addParameter(
            QgsProcessingParameterBoolean(self.obj, self.tr("Generate obj"), False)
        )
        self.addParameter(
            QgsProcessingParameterBoolean(self.gpkg, self.tr("Generate gpkg"), False)
        )

        self.addParameter(
            QgsProcessingParameterBoolean(
                self.lod22, self.tr("Generate only LOD 2.2"), False
            )
        )

    def get_source_path(
        self, layer: QgsPointCloudLayer | QgsVectorLayer
    ) -> pathlib.Path:
        try:
            decodedUri = QgsProviderRegistry.instance().decodeUri(
                layer.dataProvider().name(), layer.source()
            )
            path = decodedUri["path"]
        except AttributeError:
            # Can be triggered if provider not supported (e.g. PDAL), fallback
            path = layer.source()
        return pathlib.Path(path)

    def processAlgorithm(self, parameters, context, feedback: QgsProcessingFeedback):

        self.plg_settings = PlgOptionsManager()
        settings = self.plg_settings.get_plg_settings()
        geoflow_install = settings.geoflow_install
        docker_path = settings.docker_path
        executable_path = settings.executable_path
        debug_mode = settings.debug_mode

        cityjsonReconstruct = JsonConstructor()

        footprint = self.parameterAsVectorLayer(parameters, self.ftp, context)
        pointcloud = self.parameterAsPointCloudLayer(parameters, self.ptc, context)
        json_file = self.parameterAsFile(parameters, self.json, context)
        lod = self.parameterAsBool(parameters, self.lod22, context)
        obj = self.parameterAsBool(parameters, self.obj, context)
        gpkg = self.parameterAsBool(parameters, self.gpkg, context)

        footprint_filepath = self.get_source_path(footprint)
        pointcloud_filepath = self.get_source_path(pointcloud)
        json_filepath = pathlib.Path(json_file)

        progress_emitter = ProgressEmitter()
        progress_emitter.progress_updated.connect(
            lambda progress: feedback.setProgress(progress)
        )

        crs = self.get_crs(footprint, pointcloud)
        if not self.is_classified(pointcloud):
            raise QgsProcessingException(
                self.tr(
                    "PointCloud ERROR: "
                    "The point cloud is not classified. "
                    "It should contain at least buildings and ground."
                )
            )

        try:
            feedback.pushInfo(
                self.tr("Running reconstruction, this can take a while...")
            )
            res = cityjsonReconstruct.reconstruct(
                footprint_filepath,
                pointcloud_filepath,
                json_filepath,
                progress_emitter,
                crs,
                geoflow_install,
                docker_path,
                executable_path,
                obj,
                gpkg,
                debug_mode,
            )
            if res.stdout:
                feedback.pushConsoleInfo(
                    self.tr("Geoflow output:\n{}").format(pad(res.stdout))
                )
            if res.stderr:
                feedback.pushConsoleInfo(
                    self.tr("Geoflow warnings:\n{}").format(pad(res.stderr))
                )

            if res.returncode != 0:
                raise QgsProcessingException(
                    self.tr("Geoflow exited with non-zero code {}").format(
                        res.returncode
                    )
                )

            json_file = json_filepath / "model.json"
            feedback.pushInfo(self.tr("Checking file exists at {}").format(json_file))
            if not json_file.exists():
                raise QgsProcessingException(
                    self.tr(
                        "Geoflow did not output any data, or the file is not readable"
                    )
                )

            if lod:
                cityjsonReconstruct.fixup(json_filepath, lod, progress_emitter)
            feedback.pushInfo(self.tr("Success, file saved at: {}").format(json_file))
            return {"OUTPUT": str(json_file)}
        except FileNotFoundError:
            raise QgsProcessingException(
                self.tr(
                    "Check your CityForge parameters regarding how you installed Geoflow"
                )
            )

    def is_classified(self, pointcloud_layer: QgsPointCloudLayer) -> bool:
        classifications = pointcloud_layer.statistics().classesOf("Classification")
        if not classifications or 6 not in classifications or 2 not in classifications:
            # Not classified
            return False
        return True

    def get_crs(
        self, footprint_layer: QgsVectorLayer, pointcloud_layer: QgsPointCloudLayer
    ):
        footprint_crs = footprint_layer.crs().authid()
        pointcloud_crs = pointcloud_layer.crs().authid()

        if footprint_crs == "":
            raise QgsProcessingException(
                self.tr(
                    "CRS ERROR: "
                    "could not find the CRS of the footprint layer, "
                    "the input format may not be supported"
                )
            )

        if pointcloud_crs == "":
            raise QgsProcessingException(
                self.tr(
                    "CRS ERROR: "
                    "could not find the CRS of the pointcloud layer, "
                    "the input format may not be supported"
                )
            )

        if footprint_crs != pointcloud_crs:
            raise QgsProcessingException(
                self.tr(
                    "CRS ERROR: "
                    "the CRS of the footprint layer '{}' and the pointcloud layer '{}' "
                    "don't match".format(footprint_crs, pointcloud_crs)
                )
            )

        crs = footprint_crs.split(":")[-1]
        return crs
