"""
/***************************************************************************
 SeilaplanPlugin
                                 A QGIS plugin
 Seillinien-Layoutplaner
                              -------------------
        begin                : 2013
        copyright            : (C) 2015 by ETH Zürich
        email                : seilaplanplugin@gmail.com
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
"""

from math import degrees
from typing import List

import numpy as np
from qgis.PyQt.QtCore import QCoreApplication, QSize, Qt, QT_VERSION_STR
from qgis.PyQt.QtWidgets import QSizePolicy

if QT_VERSION_STR.startswith("5"):
    from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
else:
    from matplotlib.backends.backend_qtagg import (
        FigureCanvasQTAgg as FigureCanvas,
    )

from matplotlib.figure import Figure
from matplotlib.patches import Rectangle
import matplotlib.patheffects as pe
from matplotlib.pyplot import imread

from SEILAPLAN.gui.plotting_tools import zoom_with_wheel
from SEILAPLAN.tools.bird_view_symbol import BirdViewSymbol, BirdViewSymbolLoader
from SEILAPLAN.utils.qgis_utils import isQgisInDarkMode as inDarkMode

# Page dimensions in inch
PLOT_OUTPUT_DIMENSIONS = {"A4": (11.69, 8.27), "A3": (16.53, 11.69)}


class PlotMarker(object):

    def __init__(self, label, x, z, color, alignment):
        self.label = label
        self.x = x
        self.z = z
        self.color = color
        self.alignment = alignment


class AdjustmentPlot(FigureCanvas):

    ZOOM_TO_DISTANCE = 20
    SCREEN_DPI = 72

    def __init__(
        self,
        parent=None,
        width=5.0,
        height=4.0,
        dpi=SCREEN_DPI,
        withBirdView=False,
        profilePlotRatio=3,
        asPdf=False,
    ):
        self.win = parent
        self.dpi = dpi
        self.COLOR_MARKER = {
            0: "#403e3d" if inDarkMode() and not asPdf else "#696969",
            # grey = neutral
            1: "#1d6532" if inDarkMode() and not asPdf else "#4a6b55",
            # dark green = ok
            2: "#b95f00" if inDarkMode() and not asPdf else "#e38400",
            # orange = attention
            3: "#c04040" if inDarkMode() and not asPdf else "#e06767",
            # red = error
        }
        self.COLOR_PLOT_BG = "#7a7a7a" if inDarkMode() and not asPdf else "#efefef"
        self.COLOR_AXES_BG = "#7a7a7a" if inDarkMode() and not asPdf else "#fbfbfb"
        self.COLOR_GRID = "#636363" if inDarkMode() and not asPdf else "#c7c7c7"
        self.COLOR_MARKER_BG = "#a3a3a3" if inDarkMode() and not asPdf else "#fbfbfb"
        self.COLOR_SURVEY_PNTS = "#00400d" if inDarkMode() and not asPdf else "green"
        self.asPdf = asPdf

        self.fig = Figure(
            figsize=(width, height), dpi=self.dpi, facecolor=self.COLOR_PLOT_BG
        )

        FigureCanvas.__init__(self, self.fig)
        self.setParent(parent)

        self.axesBirdView = None
        self.birdViewMarkers = None

        if withBirdView:
            axes = self.fig.subplots(
                nrows=2,
                ncols=1,
                gridspec_kw={"height_ratios": [profilePlotRatio, 1]},
                sharex=True,
            )
            self.axes = axes[0]
            self.axesBirdView = axes[1]
            # Load markers
            loader = BirdViewSymbolLoader()
            self.birdViewMarkers = loader.loadSymbolFromArray()
            self.axesBirdView.set_aspect("equal")
            self.fig.subplots_adjust(hspace=0)
        else:
            self.axes = self.fig.add_subplot(111)
            self.fig.tight_layout(pad=0, w_pad=0.1, h_pad=0.1)

        self.axes.set_facecolor(self.COLOR_AXES_BG)

        # Profile plot should always be drawn with equal aspect
        self.axes.set_aspect("equal", "datalim", share=True)

        self.xdata = None
        self.terrain = None
        self.tPoints = None
        self.peakLoc_x = None
        self.peakLoc_y = None
        self.data_xlow = 0
        self.data_xhi = 0
        self.data_ylow = 0
        self.data_yhi = 0
        self.labelBuffer = 1
        self.arrowMarker = []
        self.arrowLabel = []
        # Reference to navigation toolbar
        self.tbar = None

        self.currentPole = None
        self.isZoomed = False

        # Enable zoom with scroll wheel
        zoomFunc = zoom_with_wheel(self, self.axes, zoomScale=1.3)  # noqa: F841

        self.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
        FigureCanvas.setSizePolicy(
            self, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
        )
        FigureCanvas.updateGeometry(self)

    def __setupAxes(self):
        direction = "in"
        if self.axesBirdView:
            direction = "inout"
        self.axes.ticklabel_format(style="plain", useOffset=False)
        self.axes.tick_params(
            axis="both",
            which="major",
            direction=direction,
            length=5,
            width=1,
            bottom=True,
            top=False,
            left=True,
            right=False,
        )
        self.axes.minorticks_on()
        self.axes.grid(which="major", color=self.COLOR_GRID, lw=1)
        self.axes.grid(which="minor", color=self.COLOR_GRID, lw=1, linestyle=":")
        self.axes.tick_params(which="minor", direction=direction)

    def initData(self, xdata, terrain, peakLocation_x, peakLocation_y, surveyPoints):
        self.xdata = xdata
        self.terrain = terrain
        self.peakLoc_x = peakLocation_x
        self.peakLoc_y = peakLocation_y
        self.data_xlow = np.min(self.xdata)
        self.data_xhi = np.max(self.xdata)
        self.data_ylow = np.min(self.terrain)
        # Add 40m to have space for poles
        self.data_yhi = np.max(self.terrain) + 40
        self.tPoints = surveyPoints

        rangeX = self.data_xhi - self.data_xlow
        rangeY = self.data_yhi - self.data_ylow
        ratio = rangeX / rangeY
        # Update figure size to fit data, height is a minimum of 330 px
        if not self.axesBirdView:
            minHeight = 330
            minLength = int(round(min(minHeight * ratio, 600)))
            self.setMinimumSize(QSize(minLength, minHeight))
        # Set label positioning by taking height of figure into account
        height_m2px = rangeY / self.height()
        self.labelBuffer = 5 * height_m2px

        self.data_ylow -= 8 * self.labelBuffer
        self.data_yhi += 4 * self.labelBuffer

    def setPlotLimits(self):
        """Only used when activating zoomTo functionality."""
        if self.isZoomed:
            d = self.currentPole["d"]
            z = self.currentPole["z"]
            h = self.currentPole["h"]
            self.axes.set_xlim(
                d - AdjustmentPlot.ZOOM_TO_DISTANCE,
                d + AdjustmentPlot.ZOOM_TO_DISTANCE,
            )
            self.axes.set_ylim(
                (z + 0.5 * h) - AdjustmentPlot.ZOOM_TO_DISTANCE,
                (z + 0.5 * h) + AdjustmentPlot.ZOOM_TO_DISTANCE,
            )
            self.labelBuffer = 0.2

        else:
            self.axes.set_xlim(self.data_xlow, self.data_xhi)
            self.axes.set_ylim(self.data_ylow, self.data_yhi)

    def updatePlot(self, poles, cable):
        # Remember current view port of plot
        currentView = (self.axes.get_xlim(), self.axes.get_ylim())

        scale = 1
        legendCol = 4
        fontSize = 12
        if self.asPdf:
            legendCol = 3
            scale = 0.5
            fontSize = 8

        self.axes.clear()
        self.__setupAxes()
        # Terrain
        self.axes.plot(
            self.xdata,
            self.terrain,
            color="#a1d1ab",
            linewidth=3.5 * scale,
            zorder=1,
        )
        # Mark survey points when working with CSV height data
        if self.tPoints is not None:
            # Add markers for survey points
            for pointX, pointY, idx, notes in zip(
                self.tPoints["d"],
                self.tPoints["z"],
                self.tPoints["nr"],
                self.tPoints["notes"],
            ):
                self.axes.plot(
                    [pointX, pointX],
                    [pointY, pointY - 6 * self.labelBuffer * scale],
                    color=self.COLOR_SURVEY_PNTS,
                    linewidth=1.5 * scale,
                    zorder=2,
                )
                labelText = f"{idx}"
                rot = 0
                ha = "center"
                va = "top"
                if notes:
                    labelText = f"{idx}{':' if notes else ''} {notes if len(notes) <= 25 else notes[:22] + '...'}"
                    # For less placement issues, labels are rotated 45°
                    rot = 55
                    ha = "right"
                    va = "center"
                    # Label rotation is switched if profile line inclines
                    if self.terrain[0] < self.terrain[-1]:
                        rot = 305
                        ha = "left"
                self.axes.text(
                    pointX,
                    pointY - 7 * self.labelBuffer * scale,
                    labelText,
                    ha=ha,
                    va=va,
                    fontsize=fontSize,
                    color=self.COLOR_SURVEY_PNTS,
                    rotation=rot,
                    rotation_mode="anchor",
                )

        if not self.asPdf:
            # Well suited locations (peaks) for poles
            self.axes.scatter(
                self.peakLoc_x,
                self.peakLoc_y,
                marker="^",
                color="#ffaa00",
                edgecolors="#496b48",
                label=self.tr("Guenstige Gelaendeform"),
                zorder=3,
            )
        # Cable lines
        self.axes.plot(
            cable["xaxis"],
            cable["empty"],
            color="#299bff",
            linewidth=1.5 * scale,
            label=self.tr("Leerseil"),
        )
        self.axes.plot(
            cable["xaxis"],
            cable["load"],
            color="#FF4D44",
            linewidth=1.5 * scale,
            label=self.tr("Lastwegkurve nach Zweifel"),
        )
        # Anchors
        if cable["anchorA"]:
            self.axes.plot(
                cable["anchorA"]["d"],
                cable["anchorA"]["z"],
                color="#a6a6a6",
                linewidth=1.5 * scale,
            )
        if cable["anchorE"]:
            self.axes.plot(
                cable["anchorE"]["d"],
                cable["anchorE"]["z"],
                color="#a6a6a6",
                linewidth=1.5 * scale,
            )
        # Ground clearance
        self.axes.plot(
            cable["groundclear_di"],
            cable["groundclear"],
            color="#910000",
            linewidth=1 * scale,
            linestyle=":",
            label=self.tr("Min. Bodenabstand"),
        )
        self.axes.plot(
            cable["groundclear_di"],
            cable["groundclear_under"],
            color="#910000",
            linewidth=1 * scale,
        )

        # Poles
        [
            pole_d,
            pole_z,
            pole_h,
            pole_dtop,
            pole_ztop,
            pole_nr,
            poleType,
            category,
            position,
            abspann,
        ] = poles
        for i, d in enumerate(pole_d):
            self.axes.plot(
                [pole_d[i], pole_dtop[i]],
                [pole_z[i], pole_ztop[i]],
                color="#363432",
                linewidth=3.0 * scale,
            )
            if poleType[i] == "crane":
                h = 3.5
                w = h * 1.8
                rect = Rectangle(
                    (pole_d[i] - w / 2, pole_z[i]),
                    w,
                    h,
                    ls="-",
                    lw=3.0 * scale,
                    facecolor="black",
                    ec="black",
                    zorder=5,
                )
                self.axes.add_patch(rect)
        # Vertical guide lines
        if self.isZoomed:
            d = self.currentPole["d"]
            z = self.currentPole["z"]
            ztop = self.currentPole["ztop"]
            self.axes.axvline(lw=1, ls="dotted", color="black", x=d)
            self.axes.axhline(lw=1, ls="dotted", color="black", y=z)
            self.axes.axhline(lw=1, ls="dotted", color="black", y=ztop)

        if self.asPdf:
            # Make sure plot has enough buffer at start and end of cable line
            #  to fit bird view symbols.
            symbolBuffer = 40
            cableStart = cable["xaxis"][0]
            if cable["anchorA"]:
                cableStart = cable["anchorA"]["d"][0]
            cableEnd = cable["xaxis"][-1]
            if cable["anchorE"]:
                cableEnd = cable["anchorE"]["d"][-1]
            if abs(cableStart - self.axes.get_xlim()[0]) < symbolBuffer:
                self.axes.set_xlim(cableStart - symbolBuffer, self.axes.get_xlim()[1])
            if abs(cableEnd - self.axes.get_xlim()[1]) < symbolBuffer:
                self.axes.set_xlim(self.axes.get_xlim()[0], cableEnd + symbolBuffer)
        else:
            # Add labels
            self.placeLabels(pole_dtop, pole_ztop, pole_nr)
        # Legend
        self.axes.legend(
            loc="lower center",
            fontsize=fontSize,
            bbox_to_anchor=(0.5, 0),
            ncol=legendCol,
            facecolor=self.COLOR_AXES_BG,
        )
        # Redraw the plot
        self.draw()

        if not self.asPdf:
            # Set new plot extent as home extent (for home button)
            self.tbar.update()
            self.tbar.push_current()
            # Recreate earlier view port of plot
            self.axes.set_xlim(currentView[0])
            self.axes.set_ylim(currentView[1])
            self.draw()

    def showMarkers(self, plotMarkers: List[PlotMarker]):
        # Displays marker for thresholds
        self.removeMarkers()
        spacing = (
            3 * abs(self.axes.get_ylim()[0] - self.axes.get_ylim()[1]) / self.height()
        )

        for marker in plotMarkers:
            textVa = "top"
            symbol = "^"
            markerZ = marker.z - spacing * 3
            labelZ = marker.z - spacing * 5
            if marker.alignment == "top":
                textVa = "bottom"
                symbol = "v"
                markerZ = marker.z + spacing * 7
                labelZ = marker.z + spacing * 9

            self.arrowMarker.append(
                self.axes.scatter(
                    marker.x,
                    markerZ,
                    marker=symbol,
                    zorder=20,
                    c=self.COLOR_MARKER[marker.color],
                    s=100,
                )
            )
            # Adds a label underneath marker with threshold value
            self.arrowLabel.append(
                self.axes.text(
                    marker.x,
                    labelZ,
                    marker.label,
                    zorder=30,
                    ha="center",
                    va=textVa,
                    backgroundcolor=self.COLOR_MARKER_BG,
                    color=self.COLOR_MARKER[marker.color],
                    fontweight="semibold",
                )
            )
        self.draw()

    def removeMarkers(self):
        for arrow in self.arrowMarker:
            if arrow in self.axes.collections:
                arrow.remove()
        for label in self.arrowLabel:
            if label in self.axes.texts:
                label.remove()
        self.arrowMarker.clear()
        self.arrowLabel.clear()
        self.draw()

    def zoomTo(self, pole):
        self.isZoomed = True
        self.currentPole = pole
        self.setPlotLimits()

    def zoomOut(self):
        self.isZoomed = False
        self.currentPole = {}
        self.setPlotLimits()
        # Set new plot extent as home extent (for home button)
        self.tbar.update()
        self.tbar.push_current()

    def placeLabels(self, xdata, ydata, label):
        if self.isZoomed:
            d = self.currentPole["d"]
            z = self.currentPole["z"]
            h = self.currentPole["h"]
            ztop = self.currentPole["ztop"]
            pos_t_d = self.axes.get_xlim()[0]
            pos_t_z = z + self.labelBuffer
            pox_yp_z = ztop + self.labelBuffer
            pos_h_d = d
            pos_h_z = ztop + self.labelBuffer * 3

            self.axes.text(pos_t_d, pos_t_z, f"{round(z, 1)} m", ha="left")
            self.axes.text(pos_t_d, pox_yp_z, f"{round(ztop, 1)} m", ha="left")
            self.axes.text(pos_h_d, pos_h_z, f"{round(h, 1)} m", ha="center")
        else:
            for i in range(len(xdata)):
                self.axes.text(
                    xdata[i],
                    ydata[i] + self.labelBuffer,
                    label[i],
                    fontsize=12,
                    ha="center",
                    fontweight="semibold",
                    color=self.COLOR_MARKER[0],
                )

    def layoutDiagrammForPrint(self, title, poles, direction):
        self.axes.set_title(
            self.tr("Seilaplan Plot  -  {}").format(title),
            fontsize=10,
            multialignment="center",
        )
        if not self.axesBirdView:
            self.axes.set_xlabel(self.tr("Horizontaldistanz [m]"), fontsize=9)
        self.axes.set_ylabel(self.tr("Hoehe [m.ue.M]"), fontsize=9)
        self.axes.tick_params(labelsize=8)
        self.axes.tick_params(labelsize=8, labeltop=False, labelbottom=True)
        self.axes.grid(which="major", lw=0.5)
        self.axes.grid(which="minor", lw=0.5, linestyle=":")
        # Label poles
        # Halo effect around text for better readability
        haloEffect = [pe.withStroke(linewidth=2, foreground="white")]
        for idx, pole in enumerate(poles):
            # Don't label anchors
            if pole["poleType"] == "anchor":
                continue
            poleNr = f"{pole['nr']}: " if pole["nr"] else ""
            poleName = (
                pole["name"][:28].strip() + "…"
                if len(pole["name"]) > 30
                else pole["name"]
            )
            rotation = 30 if direction == "downhill" else -30
            ha = "left" if direction == "downhill" else "right"

            self.axes.text(
                pole["dtop"],
                pole["ztop"] + self.labelBuffer * 4,
                f"{poleNr}{poleName}\nH = {pole['h']:.1f} m",
                ha=ha,
                fontsize=7,
                rotation=rotation,
                rotation_mode="anchor",
                path_effects=haloEffect,
            )

    def createBirdView(self, poles, azimut):
        # Height of y-axis (+/- from x-axis) in meter
        yLim = 30
        # Set fixed limits of the vertical axis
        self.axesBirdView.set_ylim(-yLim, yLim)
        # Horizontal line symbolizing cable line from first to last pole
        self.axesBirdView.plot(
            [poles[0]["d"], poles[-1]["d"]], [0, 0], color="red", linewidth=2.5
        )

        # Calculate marker size by take the height of the drawn plot into account
        # First, draw it once to get the figures size
        self.fig.canvas.draw()
        figExtent = self.axesBirdView.get_window_extent()
        # Diameter of the marker in the plot (in meter). This is not really a
        #  meaningful number any more because the whole scaling has become a
        #  bit chaotic and empirical. There is just too much transformation
        #  between svg, matplotlib plot units, pixels and millimeters going on.
        markerDiameter = 35
        # Transform to marker size (in point) ... If we're lucky
        #  1 point = self.fig.dpi / 72 pixels
        markerSize = (
            figExtent.height / (2 * yLim / markerDiameter) * 72.0 / self.fig.dpi
        )
        # Vertical shift of poles that are not in the center (in meter)
        #  The 0.05 is also empirical...
        posShiftVertical = 0.05 * self.axesBirdView.axes.get_ylim()[1] * 2
        # Halo effect around markers and north arrow for better readability
        haloEffect = [pe.withStroke(linewidth=3, foreground="white")]

        # Draw bird view markers
        defaultCircle: BirdViewSymbol = self.birdViewMarkers["default"]
        for pole in poles:
            # Special symbol for option 'flach'
            if pole["abspann"] == "flach":
                symbol: BirdViewSymbol = self.birdViewMarkers["dreizackiger_stern"]
            elif pole["category"]:
                symbol: BirdViewSymbol = self.birdViewMarkers[pole["category"]]
            else:
                symbol: BirdViewSymbol = defaultCircle

            # Mirror symbol depending on whether pole is facing start or end
            #  and if it's left or right of cable
            if pole["abspann"] == "anfang" and pole["position"] == "rechts":
                # Mirror symbol on x and y-axis
                marker = symbol.mirror("xy")
            elif pole["abspann"] == "anfang":
                # Mirror symbol on y-axis
                marker = symbol.mirror("y")
            elif pole["position"] == "rechts":
                # Mirror symbol on x-axis
                marker = symbol.mirror("x")
            else:
                marker = symbol.mplPath

            # Move marker a bit up/or down depending on pole position
            yPos = 0
            if pole["position"] == "links":
                yPos += posShiftVertical
            elif pole["position"] == "rechts":
                yPos -= posShiftVertical

            # Plot the marker
            self.axesBirdView.plot(
                pole["d"],
                yPos,
                marker=marker,
                markersize=symbol.scale * markerSize,
                color=symbol.color,
                path_effects=haloEffect,
            )
            # Add a brown center point where needed
            if symbol.centerPoint:
                self.axesBirdView.plot(
                    pole["d"],
                    yPos,
                    marker=defaultCircle.mplPath,
                    markersize=defaultCircle.scale * markerSize,
                    color=defaultCircle.color,
                    path_effects=haloEffect,
                )

        # Add north arrow
        yMin, yMax = self.axesBirdView.get_ylim()
        xMin, xMax = self.axesBirdView.get_xlim()
        # Label font size for north arrow is fixed to 30
        labelSize = 25
        # Place arrow in upper right corner, with distance in points
        baseShiftInPointsY = 45
        baseShiftInPointsX = 40
        # Calculate shift in plot units
        labelXShift = (baseShiftInPointsX / figExtent.width) * (xMax - xMin)
        labelYShift = (baseShiftInPointsY / figExtent.height) * (yMax - yMin)
        self.axesBirdView.text(
            xMax - labelXShift,
            yMax - labelYShift,
            s="➸",
            fontsize=labelSize,
            color="black",
            ha="center",
            va="center",
            rotation=degrees(azimut),
            rotation_mode="anchor",
            path_effects=haloEffect,
        )
        # Add title and axis labels
        self.layoutBirdViewForPrint()

        return self.axesBirdView.get_xlim(), self.axesBirdView.get_ylim()

    def layoutBirdViewForPrint(self):
        # Show ticks
        self.axesBirdView.tick_params(labelsize=8, labeltop=False, labelbottom=False)
        self.axesBirdView.ticklabel_format(style="plain", useOffset=False)
        self.axesBirdView.tick_params(
            axis="both",
            which="major",
            direction="inout",
            length=5,
            width=1,
            bottom=False,
            top=True,
            left=True,
            right=False,
        )
        self.axesBirdView.minorticks_on()
        self.axesBirdView.tick_params(
            which="minor",
            direction="inout",
            bottom=False,
            top=True,
            left=False,
            right=False,
        )
        # Add grid
        self.axesBirdView.grid(which="major", axis="x", lw=0.5)
        self.axesBirdView.grid(which="minor", axis="x", lw=0.5, linestyle=":")

    def addBackgroundMap(self, imgPath):
        xMin, xMax = self.axesBirdView.get_xlim()
        yMin, yMax = self.axesBirdView.get_ylim()
        img = imread(imgPath)
        self.axesBirdView.imshow(img, aspect="equal", extent=[xMin, xMax, yMin, yMax])

    def exportPdf(self, fileLocation):
        self.fig.tight_layout(pad=2)
        self.print_figure(fileLocation, self.dpi, facecolor="white")

    def setToolbar(self, tbar):
        self.tbar = tbar

    def tr(self, message, **kwargs):
        """Get the translation for a string using Qt translation API.
        We implement this ourselves since we do not inherit QObject.

        :param message: String for translation.
        :type message: str, QString

        :returns: Translated version of message.
        :rtype: QString

        Parameters
        ----------
        **kwargs
        """
        return QCoreApplication.translate(type(self).__name__, message)


def calculatePlotDimensions(xData, yData, output_size: str):
    # Dimensions in inch
    width, height = PLOT_OUTPUT_DIMENSIONS[output_size]
    # Define height to width ratio and reduce it by 4% to make up for empty
    #  space between subplots
    pageHeightRatio = (height / width) * 0.96

    # Data ranges
    data_xlow = np.min(xData)
    data_xhi = np.max(xData)
    data_ylow = np.min(yData)
    # Add another 40 m because there will be poles and labels
    data_yhi = np.max(yData) + 40
    ratio = (data_yhi - data_ylow) / 60

    # Height to width ration of data
    dataHeightRatio = (data_yhi - data_ylow) / (data_xhi - data_xlow)

    # If the data does not fit on the landscape page, we turn the page to portrait
    if output_size == "A4" and dataHeightRatio > pageHeightRatio:
        width, height = reversed(list(PLOT_OUTPUT_DIMENSIONS[output_size]))
        ratio = (data_yhi - data_ylow) / 60

    return width, height, ratio


def saveImgAsPdfWithMpl(imgPath, savePath, dpi=300):
    """Loads an image into a plot and saves it as a pdf. This is a workaround
    to be able to save PDF without an additional library."""
    height, width = PLOT_OUTPUT_DIMENSIONS["A4"]
    fig = Figure(figsize=(width, height), dpi=dpi)
    axis = fig.add_subplot(111)
    img = imread(imgPath)
    axis.imshow(img, aspect="equal")
    # Remove axis ticks
    axis.tick_params(
        axis="both",
        which="both",
        bottom=False,
        top=False,
        left=False,
        right=False,
    )
    # Remove axis labels
    axis.xaxis.set_ticklabels([])
    axis.yaxis.set_ticklabels([])
    # Save to pdf
    canvas = FigureCanvas(fig)
    canvas.print_figure(savePath, dpi, facecolor="white")
