import numpy as np
from qgis.PyQt.QtCore import QObject, pyqtSignal
from qgis.PyQt.QtGui import QImage, QTransform


class InspectorSwitcher:
    def __init__(self, item) -> None:
        self.item = item
        self.is_altitude = False

    def switch(self, altitude: bool) -> None:
        self.is_altitude = altitude
        bounds = self.item.widget.getViewBox().state["limits"]["yLimits"]
        self.item.setBounds(bounds)
        self.item.setPos(min(bounds) + ((max(bounds) - min(bounds)) / 2))


class ItemSwitcher(QObject):

    """
    Abstract class to switch between drilled length vs altitude.
    Subclasses must be instantiated during plotted items initialisation.
    Items must be instantiated in normal mode (length).
    This class basically store items altitude and length, and the ability to switch.
    Altitudes are generated at item construction, in PlotItemFactory.
    Args :
        - item : pyqtgraph item to switch
        - coordinates : a np.ndarray describing x,y,z coordinates on trace for each depth
        - planned_coordinates : a np.ndarray describing x,y,z coordinates on planned trace for each depth
        - planned_eoh : altitude of planned trace limit
    """

    switchSignal = pyqtSignal()

    def __init__(
        self, item, coordinates=None, planned_coordinates=None, planned_eoh=None
    ) -> None:
        QObject.__init__(self)
        self.item = item
        self.coordinates = coordinates
        self.planned_coordinates = planned_coordinates
        self.planned_eoh = planned_eoh
        # 3 modes : length, effective, planned
        self.mode = "length"
        self.is_empty = False
        self.min_planned_alt = None

    def switch(self, mode: str) -> None:
        self.mode = mode
        self.switchSignal.emit()

    def get_data(self, value: float) -> tuple:
        """
        For a given measure point (depth or altitude), return (depth, x, y, z)
        """
        if self.mode != "length":
            res = self.get_data_from_altitude(value)
        else:
            res = self.get_data_from_depth(value)
        return res

    def get_data_from_depth(self, value):
        """
        Return depth, x, y, z (effective altitude)
        """
        raise NotImplementedError

    def get_data_from_altitude(self, value):
        raise NotImplementedError

    def set_planned_eoh(self, value: float):
        self.planned_eoh = value

    def check_emptiness(self):
        raise NotImplementedError


class DiscreteItemSwitcher(ItemSwitcher):
    def __init__(
        self,
        item,
        coordinates: np.ndarray,
        planned_coordinates: np.ndarray,
        planned_eoh=None,
    ) -> None:
        """
        Coordinates is a 3 columns array (x,y,z).
        """
        super().__init__(item, coordinates, planned_coordinates, planned_eoh)
        x, y = self._get_item_data()
        self.x = x
        self.y = y
        self._set_altitude()
        self.check_emptiness()

    def check_emptiness(self):
        self.is_empty = self.x is None and self.y is None

    def _set_altitude(self):
        if self.coordinates is not None:
            self.altitude = self.coordinates[:, 2]
        else:
            self.altitude = None

        if self.planned_coordinates is not None:
            self.planned_altitude = self.planned_coordinates[:, 2]
            self.min_planned_alt = self.planned_altitude.min()
        else:
            self.planned_altitude = None

    def _get_item_data(self):
        if hasattr(self.item, "getData"):
            return self.item.getData()

        else:
            return (self.item.opts["x"], self.item.opts["y"])

    def switch(self, altitude: str) -> None:
        super().switch(altitude)

        if (self.altitude is None and self.mode == "effective") or (
            self.planned_altitude is None and self.mode == "planned"
        ):

            self.item.setVisible(False)

            return
        else:
            self.item.setVisible(True)

        if self.mode == "effective":
            y = self.altitude
        elif self.mode == "planned":
            y = self.planned_altitude
        else:
            y = self.y

        self.item.setData(x=self.x, y=y)
        # TODO : CategoricalScatterPlotItem.setData should be overriden
        if hasattr(self.item, "set_symbology"):
            self.item.set_symbology(self.item.symbology)

    def get_data_from_depth(self, value):
        coordinates = self.coordinates
        if coordinates is None:
            coordinates = np.full((len(self.y), 3), fill_value=None)

        index = np.argmin(np.abs(self.y - value))
        res = coordinates[index, :]
        res = [value] + res.tolist()
        return res

    def get_data_from_altitude(self, value):
        coordinates = (
            self.coordinates if self.mode == "effective" else self.planned_coordinates
        )

        index = np.argmin(np.abs(coordinates[:, 2] - value))
        depth = self.y[index]
        res = coordinates[index, :]
        res = [depth] + res.tolist()
        return res


class ExtendedItemSwitcher(ItemSwitcher):
    def __init__(
        self,
        item,
        coordinates: np.ndarray,
        planned_coordinates: np.ndarray,
        altitude: np.ndarray = None,
        planned_altitude: np.ndarray = None,
        planned_eoh=None,
    ) -> None:

        super().__init__(item, coordinates, planned_coordinates, planned_eoh)

        self._set_altitude(coordinates, altitude, planned_coordinates, planned_altitude)
        self.height = self.item.opts["height"]
        self.y0 = self.item.opts["y0"]
        self.y1 = self.item.opts["y1"]
        self.check_emptiness()

    def check_emptiness(self):
        self.is_empty = self.y0 is None and self.y1 is None and self.height is None

    def _set_altitude(
        self, coordinates, altitude, planned_coordinates, planned_altitude
    ):
        if coordinates is not None and altitude is None:
            self.altitude = [np.array(self.coordinates)[:, i] for i in [2, 5]]
        elif altitude is not None:
            self.altitude = altitude
        else:
            self.altitude = None

        if planned_coordinates is not None and planned_altitude is None:
            self.planned_altitude = [
                np.array(self.planned_coordinates)[:, i] for i in [2, 5]
            ]
            self.min_planned_alt = self.planned_altitude[1].min()

        elif planned_altitude is not None:
            self.planned_altitude = planned_altitude
            self.min_planned_alt = self.planned_altitude[1].min()
        else:
            self.planned_altitude = None

    def _switch_with_height(self, altitude: str):

        if altitude == "effective":
            new_y0 = self.altitude[0]
            new_height = self.altitude[1] - self.altitude[0]
            self.item.setOpts(height=new_height, y0=new_y0)

        elif altitude == "planned":
            new_y0 = self.planned_altitude[0]
            new_height = self.planned_altitude[1] - self.planned_altitude[0]
            self.item.setOpts(height=new_height, y0=new_y0)

        else:
            self.item.setOpts(height=self.height, y0=self.y0)

    def _switch_with_y1(self, altitude: str):
        if altitude == "effective":
            new_y0 = self.altitude[0]
            new_y1 = self.altitude[1]
            self.item.setOpts(y1=new_y1, y0=new_y0)

        elif altitude == "planned":
            new_y0 = self.planned_altitude[0]
            new_y1 = self.planned_altitude[1]
            self.item.setOpts(y1=new_y1, y0=new_y0)

        else:
            self.item.setOpts(y1=self.y1, y0=self.y0)

    def switch(self, altitude: str):
        super().switch(altitude)

        if (self.altitude is None and self.mode == "effective") or (
            self.planned_altitude is None and self.mode == "planned"
        ):

            self.item.setVisible(False)
            return
        else:
            self.item.setVisible(True)

        if self.height is not None:
            self._switch_with_height(altitude)
        else:
            self._switch_with_y1(altitude)

    def get_data_from_depth(self, value):
        coordinates = self.coordinates
        if coordinates is None:
            coordinates = np.full((len(self.y0), 6), fill_value=None)

        index = np.argmin(np.abs(np.array(self.y0) - value))
        res = coordinates[index, :3]
        res = [value] + res.tolist()
        return res

    def get_data_from_altitude(self, value):
        coordinates = (
            self.coordinates if self.mode == "effective" else self.planned_coordinates
        )

        index = np.argmin(np.abs(coordinates[:, 2] - value))
        depth = self.y0[index]
        res = coordinates[index, :3]
        res = [depth] + res.tolist()
        return res


class TextItemSwitcher(ItemSwitcher):
    def __init__(
        self,
        item,
        altitude: tuple[np.ndarray, np.ndarray],
        planned_altitude: tuple[np.ndarray, np.ndarray],
    ) -> None:

        super().__init__(item)
        self.altitude = altitude
        self.planned_altitude = planned_altitude
        x, y = self.item.pos()
        self.x = x
        self.y = y

    def switch(self, altitude: str):
        super().switch(altitude)

        if (self.altitude == (None, None) and self.mode == "effective") or (
            self.planned_altitude == (None, None) and self.mode == "planned"
        ):

            self.item.setVisible(False)

            return
        else:
            self.item.setVisible(True)

        if altitude == "effective":
            new_y0 = self.altitude[0]
            new_height = self.altitude[1] - self.altitude[0]
            self.item.setPos(self.x, new_y0 + new_height / 2)

        elif altitude == "planned":
            new_y0 = self.planned_altitude[0]
            new_height = self.planned_altitude[1] - self.planned_altitude[0]
            self.item.setPos(self.x, new_y0 + new_height / 2)

        else:
            self.item.setPos(self.x, self.y)
