import tempfile

import pyqtgraph as pg
from qgis.PyQt.QtCore import QRect, QRectF, QTimer
from qgis.PyQt.QtGui import QImage, QTransform

from openlog.datamodel.assay.generic_assay import GenericAssay
from openlog.gui.pyqtgraph.ItemSwitcher import ItemSwitcher


class ImageryItemSwitcher(ItemSwitcher):
    def __init__(self, item, altitude, planned_altitude, planned_eoh=None) -> None:
        super().__init__(item, planned_eoh)
        self.altitude = altitude
        self.planned_altitude = planned_altitude
        self.image = self.item.image
        # length
        self.x_start = self.item.x_start
        self.x_end = self.item.x_end
        # current
        self.current_x_start = self.item.x_start
        self.current_x_end = self.item.x_end

    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 altitude == "effective":
            x_end = self.altitude[1]
            x_start = self.altitude[0]
        elif altitude == "planned":
            x_end = self.planned_altitude[1]
            x_start = self.planned_altitude[0]
        else:
            x_end = self.x_end
            x_start = self.x_start

        # store current coordinates
        self.current_x_start = x_start
        self.current_x_end = x_end

        ratio = self.image.height() / self.image.width()
        new_height = x_end - x_start
        new_width = abs(new_height / ratio)
        self.item.image_width_m = new_width

        tr = QTransform()
        tr.translate(0, x_start)
        tr.scale(new_width / self.image.width(), new_height / self.image.height())
        self.item.setTransform(tr)
        self.item._update_image_transform()


class ImageryImageItem(pg.GraphicsObject):
    def __init__(
        self,
        image: QImage,
        x_start: float,
        x_end: float,
        altitude: list = None,
        planned_altitude: list = None,
        planned_eoh: float = None,
    ):
        """
        Implementation of pg.GraphicsObject to display image for fixed image ratio with :
        - minimum image width in pixel (l_min)
        - maximum stretch factor (l_max_stretch_factor)
        """
        super().__init__()
        self.image = image
        self.l_min = 150.0
        self.l_max_stretch_factor = 10.0

        self.x_start = x_start
        self.x_end = x_end
        # Define coordinates to respect image ratio
        ratio = self.image.height() / self.image.width()
        new_height = x_end - x_start
        new_width = new_height / ratio
        self.image_width_m = new_width

        # Define transform with current x_start / x_end
        tr = QTransform()
        tr.translate(0, x_start)
        tr.scale(new_width / self.image.width(), new_height / self.image.height())
        self.setTransform(tr)

        # Add timer for image transform update on viewTransformChanged
        self._update_image_timer = QTimer()
        self._update_image_timer.setSingleShot(True)
        self._update_image_timer.timeout.connect(self._update_image_transform)

        self.switcher = ImageryItemSwitcher(
            self, altitude, planned_altitude, planned_eoh
        )

    def dataBounds(self, axis: int, frac=None, orthoRange=None):
        """
        Method called by AssayPlotWidget for limits setting.
        """

        try:
            res = [
                (0, self.l_min),
                (self.switcher.current_x_start, self.switcher.current_x_end),
            ]
        except:
            res = None, None

        return res[axis]

    def boundingRect(self):
        if self.image is None:
            return QRectF(0.0, 0.0, 0.0, 0.0)
        return QRectF(0.0, 0.0, float(self.image.width()), float(self.image.height()))

    def paint(self, p, *args):
        if self.image:
            p.drawImage(
                QRectF(0, 0, self.image.width(), self.image.height()), self.image
            )

    def set_min_width(self, min_width: int) -> None:
        """Define minimum width in pixel for image

        Args:
            min_width: (int) image minium width in pixel
        """
        self.l_min = min_width
        self._update_image_transform()

    def set_max_stretch_factor(self, max_factor: float) -> None:
        """Define maximum stretch factor for image

        Args:
            max_factor: (float) max. stretch factor
        """
        self.l_max_stretch_factor = max_factor
        self._update_image_transform()

    def viewTransformChanged(self) -> None:
        """
        Update transform for image

        """
        # Use timer to avoid multiple transform update
        self._update_image_timer.stop()
        self._update_image_timer.start(75)
        super().viewTransformChanged()

    def _update_image_transform(self) -> None:
        """
        Update image transform for correct image ratio

        """
        if not self.image:
            return

        # Get current image display ratio
        A = self.image.width() / self.image.height()

        # Get current view width and height in pixels
        widget = self.getViewWidget()
        screen_geom = widget.getViewBox().screenGeometry()
        l = screen_geom.width()
        h = screen_geom.height()

        # Get current view range in item coordinates
        y_range = widget.viewRange()[1]
        coordinate_range = y_range[1] - y_range[0]
        if coordinate_range == 0:
            return

        # Define item height in coordinates from transformation
        t = self.transform()
        horizontal_scaling = t.m22()
        item_height = horizontal_scaling * self.image.height()

        # Calculate height in pixel from item height
        h_image = h * item_height / coordinate_range

        # Define width in pixel with image display ratio
        l_image = abs(h_image * A)

        # Apply factor to use maximum available width
        l_image = min(l, l_image * self.l_max_stretch_factor)

        # Define image minimum width
        l_image = max(l_image, self.l_min)

        # Define range from screen geometry width and image width in meters
        image_range_m = (l / l_image) * self.image_width_m
        widget.setXRange(0, image_range_m)


class ImageryGraphItems:
    def __init__(self, assay: GenericAssay, column: str):
        """
        Store multiple plot items to display assay data as imagery series for extended data :
        - multiple pg.ImageItem to display imagery

        Args:
            assay: GenericAssay
            column: assay column name (should have an extended data extent and nominal or categorical series type)
        """
        self._assay = assay
        self._column = column
        self._image_items = []

        # For now display all values available
        (x, y) = assay.get_all_values(column, remove_none=True)
        x = x.astype(float)

        # Get image format from assay definition
        image_format_col = assay.assay_definition.columns[column].image_format_col
        (_, image_format) = assay.get_all_values(image_format_col)

        altitude = assay.get_altitude()
        planned_altitude = assay.get_altitude(planned=True)

        for i, image_binary in enumerate(y):
            x_start = x[i][0]
            x_end = x[i][1]

            # Create image from binary data
            temp_file = tempfile.NamedTemporaryFile(mode="wb", suffix=image_format[i])
            image_filename = temp_file.name
            temp_file.close()
            with open(image_filename, "wb") as fobj:
                fobj.write(image_binary)
            img = QImage(image_filename)

            # altitudes
            item_alt = None
            if altitude is not None:
                item_alt = [altitude[0][i], altitude[1][i]]

            item_planned_alt = None
            if planned_altitude is not None:
                item_planned_alt = [planned_altitude[0][i], planned_altitude[1][i]]

            # Add item to current images list
            image_item = ImageryImageItem(
                img,
                x_start,
                x_end,
                altitude=item_alt,
                planned_altitude=item_planned_alt,
                planned_eoh=assay.get_planned_eoh(),
            )
            self._image_items.append(image_item)

    def add_to_plot(self, plot: pg.PlotWidget) -> None:
        """
        Add stored items to plot

        Args:
            plot: pg.PlotWidget
        """
        for image_item in self._image_items:
            plot.addItem(image_item)

    def setVisible(self, visible: bool) -> None:
        """
        Define store object visibility

        Args:
            visible: True to display items, False otherwise
        """
        for image_item in self._image_items:
            image_item.setVisible(visible)

    def set_min_width(self, min_width: int) -> None:
        """Define minimum width in pixel for images

        Args:
            min_width: (int) image minium width in pixel
        """
        for image_item in self._image_items:
            image_item.set_min_width(min_width)

    def set_max_stretch_factor(self, max_factor: float) -> None:
        """Define maximum stretch factor for images

        Args:
            max_factor: (float) max. stretch factor
        """
        for image_item in self._image_items:
            image_item.set_max_stretch_factor(max_factor)


def create_extended_imagery_plot_items(
    plot: pg.PlotWidget,
    assay: GenericAssay,
    column: str,
    config,
) -> None:
    item = ImageryGraphItems(assay, column)
    item.add_to_plot(plot)
    config.set_plot_item(item)
