from pathlib import Path
from typing import List

from qgis.core import Qgis, QgsProject, QgsVectorLayer

from openlog.__about__ import DIR_PLUGIN_ROOT


class LayersInterface:
    """
    Interface for layers use.

    Get collar or collar trace layer :

    - :meth:`get_collar_layer`
    - :meth:`get_collar_trace_layer`

    Define path for default style for layers:

    - :meth:`get_collar_layer_style_file`
    - :meth:`get_collar_trace_layer_style_file`

    Get collar layer name:

    - :meth:`get_collar_layer_name`
    - :meth:`get_collar_trace_layer_name`

    Get selected collar :

    - :meth:`get_selected_collar_from_layer`

    Select collar :

    - :meth:`select_collar_by_id`

    By default, all functions are not implemented and raises :class:`InvalidInterface` exception.

    Unselect collar from current selection :

     - :meth:`unselect_collar_by_id`

    """

    class InvalidInterface(Exception):
        pass

    def __init__(self):
        # Store created collar layers, so we can access selected feature in widget only with OpenLogConnection interface
        self.collar_layer = None
        self.planned_collar_layer = None
        self.collar_trace_layer = None
        self.planned_trace_layer = None
        self.splitted_trace_layer = []

    def get_openlog_layer_names(self) -> List[str]:
        """
        Return list of all connection layer names (collar, traces, assays).
        """
        result = []
        # assays
        try:
            result += [
                layer.name()
                for layer in QgsProject.instance()
                .layerTreeRoot()
                .findGroup("Downhole data")
                .findLayers()
            ]
        except:
            pass

        # collars
        result.append(self.get_collar_layer_name())
        result.append(self.get_planned_collar_layer_name())

        # traces
        result.append(self.get_collar_trace_layer_name())
        result.append(self.get_planned_trace_layer_name())

        return result

    def get_assay_layers(self, assay_definitions: List) -> List[QgsVectorLayer]:
        """
        Return list of assays as QgsVectorLayers.
        """
        return []

    def get_collar_layer(self) -> QgsVectorLayer:
        """
        Return collar QgsVectorLayer

        Returns:
            QgsVectorLayer: raises :class:`InvalidInterface` if not implemented
        """
        raise LayersInterface.InvalidInterface()

    def get_planned_collar_layer(self) -> QgsVectorLayer:
        """
        Return planned collar QgsVectorLayer

        Returns:
            QgsVectorLayer: raises :class:`InvalidInterface` if not implemented
        """
        raise LayersInterface.InvalidInterface()

    def get_collar_trace_layer(self) -> QgsVectorLayer:
        """
        Return collar trace QgsVectorLayer

        Returns:
            QgsVectorLayer: raises :class:`InvalidInterface` if not implemented
        """
        raise LayersInterface.InvalidInterface()

    def get_planned_trace_layer(self) -> QgsVectorLayer:
        """
        Return planned trace QgsVectorLayer

        Returns:
            QgsVectorLayer: raises :class:`InvalidInterface` if not implemented
        """
        raise LayersInterface.InvalidInterface()

    def _set_clamping(self, layer: QgsVectorLayer) -> None:
        """
        Set elevation property to 'Absolute'
        """
        layer.elevationProperties().setClamping(Qgis.AltitudeClamping.Absolute)

    def is_planned_trace_geometry_exist(self) -> bool:
        """
        Check if planned trace geometries exist.
        It could not exist if there are no surveys or desurveying have not been called.

        """
        lyr = self.get_planned_trace_layer()
        if lyr is None:
            return False
        """
        Avoid RuntimeError: wrapped C/C++ object of type QgsVectorLayer has been deleted.
        Raised when closing QGIS
        """
        try:
            valid = 0
            for ftr in lyr.getFeatures():
                valid += not ftr.geometry().isEmpty()
        except RuntimeError:
            valid = 0

        return valid > 0

    def is_trace_geometry_exist(self) -> bool:
        """
        Check if trace geometries exist.
        It could not exist if there are no surveys or desurveying have not been called.

        """
        lyr = self.get_collar_trace_layer()
        if lyr is None:
            return False
        """
        Avoid RuntimeError: wrapped C/C++ object of type QgsVectorLayer has been deleted.
        Raised when closing QGIS
        """
        try:
            valid = 0
            for ftr in lyr.getFeatures():
                valid += not ftr.geometry().isEmpty()
        except RuntimeError:
            valid = 0

        return valid > 0

    def _is_layer_exist(self, layer: QgsVectorLayer):
        """
        Check if QGIS layer is not deleted.
        """

        try:
            layer.isValid()
            return True
        except RuntimeError:
            return False

    def splitted_trace_layer_available(self) -> bool:
        """Define if splitted trace layer can be used

        :return: True is splitted trace layer can be used, False otherwise
        :rtype: bool
        """
        return False

    def get_splitted_trace_layer_name(
        self, column_config, planned: bool, barcharts: bool = False
    ) -> str:
        """
        Returns display splitted layer name.
        """
        suffix = "[Planned trace]" if planned else "[Trace]"
        if barcharts:
            suffix = "[Planned barcharts]" if planned else "[Barcharts]"
        return f"{column_config.assay_display_name} - {column_config.column.display_name} {suffix}"

    def get_splitted_trace_layer(
        self, column_config, planned: bool = False, barcharts: bool = False
    ) -> QgsVectorLayer:
        """
        Return splitted trace QgsVectorLayer

        Returns:
            QgsVectorLayer: None if interface not implemented
        """
        return None

    def get_collar_layer_style_file(self) -> Path:
        """
        Get QGIS file style for collar layer

        Returns:
            Path: path to QGIS style file
        """
        return DIR_PLUGIN_ROOT / "resources" / "styles" / "collar.qml"

    def get_planned_collar_layer_style_file(self) -> Path:
        """
        Get QGIS file style for planned collar layer

        Returns:
            Path: path to QGIS style file
        """
        return DIR_PLUGIN_ROOT / "resources" / "styles" / "planned_collar.qml"

    def get_collar_trace_layer_style_file(self) -> Path:
        """
        Get QGIS file style for collar trace layer

        Returns:
            Path: path to QGIS style file
        """
        return DIR_PLUGIN_ROOT / "resources" / "styles" / "collar_trace.qml"

    def get_planned_trace_layer_style_file(self) -> Path:
        """
        Get QGIS file style for collar trace layer

        Returns:
            Path: path to QGIS style file
        """
        return DIR_PLUGIN_ROOT / "resources" / "styles" / "planned_trace.qml"

    def get_collar_layer_name(self) -> str:
        """
        Get collar layer name

        Returns:
            str: collar layer name
        """
        return "Collar"

    def get_planned_collar_layer_name(self) -> str:
        """
        Get planned collar layer name

        Returns:
            str: collar layer name
        """
        return "Planned collar"

    def get_collar_trace_layer_name(self) -> str:
        """
        Get collar trace layer name

        Returns:
            str: collar trace layer name
        """
        return "Trace"

    def get_planned_trace_layer_name(self) -> str:
        """
        Get planned trace layer name

        Returns:
            str: collar trace layer name
        """
        return "Planned trace"

    def get_selected_collar_from_layer(self) -> List[str]:
        """
        Get selected collar id from QGIS layer

        Returns:
            List[str]: selected collar id
        """
        collar_layer = self.get_collar_layer()
        if collar_layer is not None:
            res = [f["hole_id"] for f in collar_layer.selectedFeatures()]
        else:
            res = []
        return res

    def get_selected_planned_collar_from_layer(self) -> List[str]:
        """
        Get selected planned collar id from QGIS layer

        Returns:
            List[str]: selected collar id
        """
        planned_collar_layer = self.get_planned_collar_layer()
        if planned_collar_layer is not None:
            res = [f["hole_id"] for f in planned_collar_layer.selectedFeatures()]
        else:
            res = []
        return res

    def select_collar_by_id(self, hole_ids: List[str]) -> None:
        """
        Select collar by ID in collar layer

        Args:
            hole_ids: List[str]: selected collar id
        """
        collar_layer = self.get_collar_layer()
        if collar_layer is not None:
            in_ = ",".join(["'" + hole + "'" for hole in hole_ids])
            collar_layer.selectByExpression(f'"hole_id" IN ({in_})')

    def unselect_collar_by_id(self, hole_ids: List[str]) -> None:
        """
        Unselect collar by ID in collar layer

        Args:
            hole_ids: List[str]: selected collar id
        """
        collar_layer = self.get_collar_layer()
        if collar_layer is not None:
            in_ = ",".join(["'" + hole + "'" for hole in hole_ids])
            collar_layer.selectByExpression(
                f'"hole_id" IN ({in_})',
                behavior=Qgis.SelectBehavior.RemoveFromSelection,
            )

    def select_planned_collar_by_id(self, hole_ids: List[str]) -> None:
        """
        Select planned collar by ID in planned collar layer

        Args:
            hole_ids: List[str]: selected collar id
        """
        planned_collar_layer = self.get_planned_collar_layer()
        if planned_collar_layer is not None:
            in_ = ",".join(["'" + hole + "'" for hole in hole_ids])
            planned_collar_layer.selectByExpression(f'"hole_id" IN ({in_})')

    def unselect_planned_collar_by_id(self, hole_ids: List[str]) -> None:
        """
        Unselect planned collar by ID in planned collar layer

        Args:
            hole_ids: List[str]: selected collar id
        """
        planned_collar_layer = self.get_planned_collar_layer()
        if planned_collar_layer is not None:
            in_ = ",".join(["'" + hole + "'" for hole in hole_ids])
            planned_collar_layer.selectByExpression(
                f'"hole_id" IN ({in_})',
                behavior=Qgis.SelectBehavior.RemoveFromSelection,
            )
