from math import cos, pi, sin

import numpy as np
import pyqtgraph as pg
from qgis.PyQt.QtCore import QPointF, QRectF, Qt
from qgis.PyQt.QtGui import (
    QBrush,
    QColor,
    QPainter,
    QPicture,
    QPixmap,
    QPolygonF,
    QTransform,
)

from openlog.__about__ import DIR_PLUGIN_ROOT
from openlog.gui.pyqtgraph.ItemSwitcher import ItemSwitcher

# Value for aliases
SYMB_ROOT = f"{DIR_PLUGIN_ROOT}/plugins/visualization/log_techniques/symbology"


class SchemaLogsTechniquesSwitcher(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)
        # TODO : for now item is considered empty to avoid any error with switch
        self.is_empty = True


class SchemaLogsTechniquesItem(pg.GraphicsObject):
    def __init__(self, data):
        pg.GraphicsObject.__init__(self)

        # TODO : for now no information about coordinated and planned coordinates are used
        self.switcher = SchemaLogsTechniquesSwitcher(
            self,
            coordinates=None,
            planned_coordinates=None,
            planned_eoh=None,
        )

        self.data = data
        self.intubation_brush = {
            "FENTES LASER": QBrush(QPixmap(f"{SYMB_ROOT}/fentes_laser.png")),
            "NERVURES REPOUSSEES": QBrush(
                QPixmap(f"{SYMB_ROOT}/nervures_repoussees.png")
            ),
            "TROUS OBLONGS": QBrush(QPixmap(f"{SYMB_ROOT}/trous_oblongs.png")),
            "FIL ENROULE": QBrush(
                QColor("#000000"), Qt.HorPattern
            ),  # QBrush(QPixmap(f"{SYMB_ROOT}/fil_enroule.png")),
            "FENTES ARTISANALES": QBrush(
                QPixmap(f"{SYMB_ROOT}/fentes_artisanales.png")
            ),
            "TROUS ARTISANAUX": QBrush(QPixmap(f"{SYMB_ROOT}/trous_artisanaux.png")),
            "INDETERMINE": pg.mkBrush("w"),
        }
        self.filling_brush = {
            "CIMENT": QBrush(QColor("#AFABAB")),
            "GRAVIER": QBrush(QColor("#BF9000"), Qt.BDiagPattern),
            "ARGILE": QBrush(QColor("#C55A11"), Qt.HorPattern),
            "PACKER": QBrush(QColor("#404040")),
        }
        self.accessory_pen = {
            "BOUCHON DE FOND": pg.mkPen(0.0, width=2),
            "PARAPLUIE": pg.mkPen(0.0, width=2, style=Qt.DashDotLine),
        }
        self.deviation = 0
        self.generatePicture()

    def dataBounds(self, axis: int, frac=None, orthoRange=None):
        """
        Get min and max depth.
        Args:
            - axis : 0 for x, 1 for y
        """
        try:
            # boundingRect return (origin_x, origin_y, width, height)
            rect = self.boundingRect()
            res = [rect.x(), rect.x() + rect.width()], [
                rect.y(),
                rect.y() + rect.height(),
            ]
        except:
            res = None, None

        return res[axis]

    def apply_deviation(self, p, item):
        p.translate(0, float(item["depth_from"]) * 1000 / cos(self.deviation))
        p.rotate(-float(item["deviation"]) * 10)
        self.deviation += (-float(item["deviation"]) * 10) * pi / 180
        p.translate(0, -float(item["depth_from"]) * 1000 / cos(self.deviation))

    def scale_brush(self, brush):
        if self.getViewBox():
            sc = self.getViewBox().getAspectRatio()
            transform = QTransform()
            transform = transform.scale(1, sc)
            brush.setTransform(transform)

    def compute_polygon(self, rect, deviation):
        dev_from = float(rect.top()) * 1000 / cos(self.deviation)
        dev_to = float(rect.bottom()) * 1000 / cos(self.deviation)
        hg = QPointF(
            cos(deviation) * rect.right(), dev_from + sin(deviation) * rect.right()
        )
        hd = QPointF(
            cos(deviation) * rect.left(), dev_from + sin(deviation) * rect.left()
        )
        bg = QPointF(rect.right(), dev_to)
        bd = QPointF(rect.left(), dev_to)
        return QPolygonF([hg, hd, bd, bg])

    def draw_hole(self, p):
        p.save()
        for drilling in sorted(
            self.data["drilling"],
            key=lambda d: (float(d["diameter"]), -float(d["depth_from"])),
            reverse=True,
        ):
            if float(drilling["deviation"]) != 0:
                self.apply_deviation(p, drilling)
            p.drawPolygon(
                self.compute_polygon(
                    QRectF(
                        -float(drilling["diameter"]) / 2,
                        float(drilling["depth_from"]),
                        float(drilling["diameter"]),
                        float(drilling["depth_to"]) - float(drilling["depth_from"]),
                    ),
                    float(drilling["deviation"]) * 10 * pi / 180,
                )
            )
        p.restore()
        self.deviation = 0

    def draw_drilling(self, p):
        p.setPen(pg.mkPen(0.0, width=2))
        self.draw_hole(p)
        p.setPen(pg.mkPen(None))
        p.setBrush(pg.mkBrush("w"))
        self.draw_hole(p)

    def fill_drilling(self, p):
        p.save()
        for drilling in sorted(
            self.data["drilling"],
            key=lambda d: (float(d["diameter"]), -float(d["depth_from"])),
            reverse=True,
        ):
            if float(drilling["deviation"]) != 0:
                self.apply_deviation(p, drilling)

            fill_drilling = [
                fill
                for fill in self.data["filling"]
                if (
                    float(fill["depth_from"]) < float(drilling["depth_to"])
                    and float(fill["depth_to"]) > float(drilling["depth_from"])
                    and float(fill["diameter"]) == float(drilling["diameter"])
                )
            ]
            for fill in fill_drilling:
                if (
                    fill["material"] in self.filling_brush
                    and self.filling_brush[fill["material"]]
                ):
                    brush = self.filling_brush[fill["material"]]
                else:
                    brush = pg.mkBrush("w")
                self.scale_brush(brush)

                p.setPen(pg.mkPen(None))
                p.setBrush(brush)
                p.drawPolygon(
                    self.compute_polygon(
                        QRectF(
                            -float(drilling["diameter"]) / 2,
                            max(
                                float(drilling["depth_from"]), float(fill["depth_from"])
                            ),
                            float(drilling["diameter"]),
                            min(float(drilling["depth_to"]), float(fill["depth_to"]))
                            - max(
                                float(drilling["depth_from"]), float(fill["depth_from"])
                            ),
                        ),
                        float(drilling["deviation"]) * 10 * pi / 180,
                    )
                )
        p.restore()
        self.deviation = 0

    def get_drilling_tube_sections(self, drilling):
        tube_sections = []
        tube_drilling = [
            tube
            for tube in self.data["intubation"]
            if (
                float(tube["depth_from"]) < float(drilling["depth_to"])
                and float(tube["depth_to"]) > float(drilling["depth_from"])
                and float(tube["diameter"]) < float(drilling["diameter"])
            )
        ]
        for tube in sorted(
            tube_drilling,
            key=lambda d: (float(d["diameter"]), -float(d["depth_from"])),
            reverse=True,
        ):
            tube_section = tube.copy()
            if float(drilling["depth_from"]) > float(tube["depth_from"]):
                tube_section["depth_from"] = drilling["depth_from"]
            if float(drilling["depth_to"]) < float(tube["depth_to"]):
                tube_section["depth_to"] = drilling["depth_to"]
            tube_sections.append(tube_section)
        return tube_sections

    def draw_tube(self, p, tube, deviation):
        p.setPen(pg.mkPen(None))
        if (
            tube["filter"] in self.intubation_brush
            and self.intubation_brush[tube["filter"]]
        ):
            brush = self.intubation_brush[tube["filter"]]
        else:
            brush = pg.mkBrush("w")
        self.scale_brush(brush)

        tube_polygone = self.compute_polygon(
            QRectF(
                -float(tube["diameter"]) / 2,
                float(tube["depth_from"]),
                float(tube["diameter"]),
                float(tube["depth_to"]) - float(tube["depth_from"]),
            ),
            deviation,
        )

        p.setBrush(brush)
        p.drawPolygon(tube_polygone)

        p.setPen(pg.mkPen(0.0))
        p.drawLine(tube_polygone[0], tube_polygone[3])
        p.drawLine(tube_polygone[1], tube_polygone[2])

    def fill_tube(self, p, tube, deviation):
        fill_intubation = [
            fill
            for fill in self.data["filling"]
            if (
                float(fill["depth_from"]) < float(tube["depth_to"])
                and float(fill["depth_to"]) > float(tube["depth_from"])
                and float(fill["diameter"]) == float(tube["diameter"])
            )
        ]
        for fill in fill_intubation:
            if (
                fill["material"] in self.filling_brush
                and self.filling_brush[fill["material"]]
            ):
                brush = self.filling_brush[fill["material"]]
            else:
                brush = pg.mkBrush("w")
            self.scale_brush(brush)

            p.setPen(pg.mkPen(None))
            p.setBrush(brush)
            p.drawPolygon(
                self.compute_polygon(
                    QRectF(
                        -float(tube["diameter"]) / 2,
                        max(float(tube["depth_from"]), float(fill["depth_from"])),
                        float(tube["diameter"]),
                        min(float(tube["depth_to"]), float(fill["depth_to"]))
                        - max(float(tube["depth_from"]), float(fill["depth_from"])),
                    ),
                    deviation,
                )
            )

    def add_accessory(self, p, tube, deviation):
        intubation_accessory = [
            accessory
            for accessory in self.data["accessory"]
            if (
                float(accessory["depth_from"]) <= float(tube["depth_to"])
                and float(accessory["depth_to"]) >= float(tube["depth_from"])
                and float(accessory["diameter"]) == float(tube["diameter"])
                and float(accessory["depth_from"]) == float(accessory["depth_to"])
            )
        ]
        for accessory in intubation_accessory:
            p.setPen(self.accessory_pen[accessory["type"]])
            if accessory["inside_tube"]:
                depth = float(accessory["depth_from"]) * 1000 / cos(self.deviation)
                p.drawLine(
                    QPointF(-float(accessory["diameter"]) / 2, depth),
                    QPointF(float(accessory["diameter"]) / 2, depth),
                )
            else:
                # On récupère le diametre externe correspondant
                upper_diameter = [
                    drilling["diameter"]
                    for drilling in self.data["drilling"]
                    if (
                        float(accessory["depth_from"]) <= float(drilling["depth_to"])
                        and float(accessory["depth_from"])
                        >= float(drilling["depth_from"])
                        and float(accessory["diameter"]) < float(drilling["diameter"])
                    )
                ]
                upper_diameter.extend(
                    [
                        intubation["diameter"]
                        for intubation in self.data["intubation"]
                        if (
                            float(accessory["depth_from"])
                            <= float(intubation["depth_to"])
                            and float(accessory["depth_from"])
                            >= float(intubation["depth_from"])
                            and float(accessory["diameter"])
                            < float(intubation["diameter"])
                        )
                    ]
                )
                diameter = min(upper_diameter)
                depth = float(accessory["depth_from"]) * 1000 / cos(self.deviation)
                p.drawLine(
                    QPointF(-diameter / 2, depth),
                    QPointF(-float(accessory["diameter"]) / 2, depth),
                )
                p.drawLine(
                    QPointF(float(accessory["diameter"]) / 2, depth),
                    QPointF(diameter / 2, depth),
                )

    def draw_center_line(self, p):
        # On récupère les break points
        p.save()
        br = self.boundingRect()
        break_points = [
            {"depth_from": drilling["depth_from"], "deviation": drilling["deviation"]}
            for drilling in self.data["drilling"]
            if (float(drilling["deviation"]) != 0)
        ]
        p.setPen(pg.mkPen(0.0, style=Qt.DashLine))
        if len(break_points) > 0:
            y_top = br.top()
            for break_point in sorted(
                break_points, key=lambda d: float(d["depth_from"])
            ):
                y_bottom = float(break_point["depth_from"]) * 1000 / cos(self.deviation)
                p.drawLine(QPointF(0, y_top), QPointF(0, y_bottom))
                self.apply_deviation(p, break_point)
                y_top = float(break_point["depth_from"]) * 1000 / cos(self.deviation)
            p.drawLine(QPointF(0, y_top), QPointF(0, br.bottom() / cos(self.deviation)))
        else:
            p.drawLine(QPointF(0, br.top()), QPointF(0, br.bottom()))
        p.restore()
        self.deviation = 0

    def generatePicture(self):
        self.picture = QPicture()
        p = QPainter(self.picture)
        p.setRenderHint(QPainter.Antialiasing, True)
        p.setRenderHint(QPainter.SmoothPixmapTransform, True)

        # Forage du trou
        self.draw_drilling(p)
        # Remplissage du drilling
        self.fill_drilling(p)

        # On tube les drillings
        p.save()
        for drilling in sorted(
            self.data["drilling"],
            key=lambda d: (float(d["diameter"]), -float(d["depth_from"])),
            reverse=True,
        ):
            if float(drilling["deviation"]) != 0:
                self.apply_deviation(p, drilling)

            deviation = float(drilling["deviation"]) * 10 * pi / 180
            for tube_section in sorted(
                self.get_drilling_tube_sections(drilling),
                key=lambda d: (float(d["diameter"]), -float(d["depth_from"])),
                reverse=True,
            ):
                # On place le tube
                self.draw_tube(p, tube_section, deviation)
                # On remplit le tube
                self.fill_tube(p, tube_section, deviation)
                # On place les accessoires
                self.add_accessory(p, tube_section, deviation)
        p.restore()
        self.deviation = 0

        # On ajoute la line centrale
        self.draw_center_line(p)
        p.end()

    def paint(self, p, *args):
        self.generatePicture()
        p.drawPicture(0, 0, self.picture)

    def boundingRect(self):
        return QRectF(self.picture.boundingRect())
