try:    # need to TRY importing numba, only to see if it is available
    haveNumba = True
    import numba  # pylint: disable=W0611
except ImportError:
    haveNumba = False

try:    # need to TRY importing debugpy, only to see if it is available
    haveDebugpy = True
    import debugpy  # pylint: disable=W0611
except ImportError:
    haveDebugpy = False

try:
    haveMatplotlib = True
    import matplotlib
except ImportError:
    haveMatplotlib = False


import configparser
import importlib
import math
import os
# Used only for local serializability checks via pickle.dumps.
import pickle  # nosec B403
import re
import sys
from pathlib import Path

import numpy as np
import pyqtgraph as pg
import rasterio as rio
import wellpathpy as wp
from qgis.core import QgsGeometry, QgsPointXY
from qgis.PyQt.QtCore import (PYQT_VERSION_STR, QT_VERSION_STR, QLineF,
                              QPointF, QRectF, Qt)
from qgis.PyQt.QtGui import QColor, QPen, QPolygonF, QTransform, QVector3D

from .app_settings import isDebugLoggingEnabled


def silentPrint(*_, **__):
    pass


def myPrint(*args, **kwargs):                                                   # print function that can be suppressed
    if isDebugLoggingEnabled():
        print(*args, **kwargs)


def whoamI():
    # See: https://www.oreilly.com/library/view/python-cookbook/0596001673/ch14s08.html for introspective functions
    return sys._getframe(1).f_code.co_name                                      # pylint: disable=W0212 # unfortunately need access to protected member


def lineNo():
    return sys._getframe(1).f_lineno                                            # pylint: disable=W0212 # unfortunately need access to protected member


def callerName():
    try:
        caller = sys._getframe(2)                                               # pylint: disable=W0212 # unfortunately need access to protected member
        return caller.f_code.co_name
    except ValueError:
        return 'unknown_caller'


def toFloat(value: any, default: float = 0.0) -> float:
    if value is None:                                                         # if you expect None to be passed
        return default
    try:
        return float(value)
    except ValueError:
        return default


def toInt(value: any, default: int = 0) -> int:
    if value is None:                                                         # if you expect None to be passed
        return default
    try:
        return int(value)
    except ValueError:
        return default


def toBool(value: any, default=False) -> bool:
    if value is None:                                                         # if you expect None to be passed
        return default
    try:
        if value.lower() in ['true', '1', 'yes']:
            return True

        if value.lower() in ['false', '0', 'no']:
            return False

        return default
    except AttributeError:
        return default


def knotToMeterperSec(speed: float) -> float:
    return speed * 0.514444444


def meterPerSecToKnot(speed: float) -> float:
    return speed * 1.94384449412


def newtonToTonForce(force: float) -> float:
    return force * 0.0001019716


def tonForceToNewton(force: float) -> float:
    return force * 9806.652


def stringToIntList(string: str):

    string = string.replace(',', ' ').replace(';', ' ')                         # first convert all delimiters back to spaces

    stringList = string.split()                                                 # split the string into a list of strings
    intList = []

    for s in stringList:
        try:
            intList.append(int(s))                                              # append the integer to the list
        except ValueError:
            return []                                                           # bummer; input error

    return intList


def intListToString(intList):

    delimiter = ' '                                                             # Define the delimiter to be used
    intListString = map(str, intList)                                           # Convert each element into a string
    joinNumberString = delimiter.join(intListString)                            # Join the strings using the delimiter
    return joinNumberString


def odd(number):
    if number % 2 == 0:
        return False                                                            # even

    return True                                                             # odd


def even(number):
    if number % 2 == 0:
        return True                                                             # even

    return False                                                            # odd


def wideturnDetour(turnRadius, lineInterval):
    return max(lineInterval - 2.0 * turnRadius, 0.0)


def teardropDetour(turnRadius, lineInterval):
    # no teardrop effects for a wide turn
    if turnRadius <= 0.5 * lineInterval:
        return 0.0

    # amount of x-line compensation for each side of the teardrop
    teardropMoveout = turnRadius - 0.5 * lineInterval

    # get the angle of the pie segment
    teardropAngle = math.degrees(math.acos(1.0 - 0.5 * teardropMoveout / turnRadius))

    # there are two pie segments for each side of the teardrop; hence four segments in total
    detour = 4.0 * (teardropAngle / 360.0) * 2.0 * math.pi * turnRadius
    return detour


def lineturnDetour(turnRadius, saillineInterval, linesPerTrack, final=False):

    halfCircle = math.pi * turnRadius

    forwardLines = math.ceil(0.5 * linesPerTrack)
    backwardLines = math.floor(0.5 * linesPerTrack)

    xlineForward = forwardLines * saillineInterval
    xlineBackward = backwardLines * saillineInterval

    crosslineForward = wideturnDetour(turnRadius, xlineForward)
    crosslineBackward = wideturnDetour(turnRadius, xlineBackward)

    teardropForward = teardropDetour(turnRadius, xlineForward)
    teardropBackward = teardropDetour(turnRadius, xlineBackward)

    if final:
        lineturnTotal = halfCircle * backwardLines * 2
        crosslineTotal = (crosslineForward + crosslineBackward) * backwardLines
        teardropTotal = (teardropForward + teardropBackward) * backwardLines
    else:
        lineturnTotal = halfCircle * (forwardLines + backwardLines)
        crosslineTotal = crosslineForward * forwardLines + crosslineBackward * backwardLines
        teardropTotal = teardropForward * forwardLines + teardropBackward * backwardLines

    return (lineturnTotal, crosslineTotal, teardropTotal)


def maxTurnSpeedVsCableLength(length):
    maxSpeed = 49.7365 * length**-0.26803
    return maxSpeed


def maxCableLengthVsTurnSpeed(speed):
    maxLength = 2_138_916 * speed**-3.73094
    return maxLength


def makePenFromParms(parms):
    # Internal UI contract for pen serialization/deserialization.
    assert len(parms) == 6, "need 6 parameters for: ('color', 'width', 'style', 'capStyle', 'joinStyle', 'cosmetic')"  # nosec B101

    ps = Qt.PenStyle
    cs = Qt.PenCapStyle
    js = Qt.PenJoinStyle

    color = parms[0]
    width = parms[1]
    pStyle = getattr(ps, parms[2])
    cStyle = getattr(cs, parms[3])
    jStyle = getattr(js, parms[4])
    cosmet = parms[5]
    # commented out; avoid using fn.mkPen() for proper capStyle and joinStyle
    # names = ('color', 'width', 'style', 'capStyle', 'joinStyle', 'cosmetic')
    # parms = (color, width, pStyle, cStyle, jStyle, cosmet)
    # opts = dict(zip(names, parms))
    # pen = fn.mkPen(**opts)

    r = color[0]
    g = color[1]
    b = color[2]
    a = color[3]
    c = QColor(r, g, b, a)

    pen = QPen(c, width, pStyle, cStyle, jStyle)
    pen.setCosmetic(cosmet)
    return pen


# def getNameFromQtEnum(enum, value):
#     # See: _getAllowedEnums(self, enum), defined in pyqtgraph; qtenum.py.
#     # References to PySide have been removed. QGIS uses PyQt5 only...
#     searchObj = Qt
#     vals = {}
#     for key in dir(searchObj):
#         val = getattr(searchObj, key)
#         if isinstance(val, enum):
#             vals[key] = val

#     result = [k for k, v in vals.items() if v == value][0]
#     return result


def getNameFromQtEnum(enum, value):
    """
    Returns the name of the enum member in Qt (PyQt5 or PyQt6) that matches the given value.
    Compatible with both PyQt5 and PyQt6.
    """
    # PyQt6 enums are subclasses of enum.Enum, PyQt5 are not
    import enum as stdEnum  # pylint: disable=C0415

    # Handle PyQt6 enums (subclass of enum.Enum)
    if isinstance(enum, type) and issubclass(enum, stdEnum.Enum):
        for member in enum:
            if member.value == value or member == value:
                return member.name
        # Try matching by int value if value is int and member.value is enum
        for member in enum:
            if hasattr(member.value, 'value') and member.value.value == value:
                return member.name
        raise ValueError(f'Value {value} not found in enum {enum}')

    # Handle PyQt5 enums (not subclass of enum.Enum)
    searchObj = Qt
    vals = {}
    for key in dir(searchObj):
        val = getattr(searchObj, key)
        if isinstance(val, enum):
            vals[key] = val
    # Try direct match
    for k, v in vals.items():
        if v == value:
            return k
    # Try int value match
    for k, v in vals.items():
        if hasattr(v, 'value') and v.value == value:
            return k
    raise ValueError(f'Value {value} not found in enum {enum}')


def getMethodFromModule(pmm: str):
    """getting a method from a package defined by a 'dot' separated string "package.module.method"
    note: within 'roll' the package name is 'roll'"""
    package, module, method = pmm.split('.')
    module = importlib.import_module(module, package=package)
    method = getattr(module, method)
    return method


# See: https://github.com/bensarthou/pynufft_benchmark/blob/master/NDFT.py # now accelerated in funtions_numba.py
def nDft1Da(x, f, kMax, dK):
    """non-equispaced discrete Fourier transform on x with weights (1/0) in f"""
    # n = x.shape[0]                                            # normalize by maximum nr of available races
    n = np.count_nonzero(f)                                     # normalize by actual  nr of available traces
    a = 1 / n if n > 0 else 0
    k = np.arange(0, kMax, dK)
    r = np.dot(f, np.exp(2j * np.pi * k * x[:, np.newaxis])) * a
    return r


def nDft1Db(x, kMax, dK):
    """non-equispaced discrete Fourier transform on x with equal weights defined by size of transform"""
    n = x.shape[0]
    a = 1 / n if n > 0 else 0
    k = np.arange(0, kMax, dK)
    r = np.exp(2j * np.pi * k * x[:, np.newaxis]) * a
    s = r.sum(axis=0)
    return s


def rotatePoint2D(x: float, y: float, angle: float) -> tuple:
    # rotate a point around the origin
    # x' = x * cos(angle) - y * sin(angle)
    # y' = x * sin(angle) + y * cos(angle)
    angle = math.radians(angle)
    x1 = x * math.cos(angle) - y * math.sin(angle)
    y1 = x * math.sin(angle) + y * math.cos(angle)
    return x1, y1


def dummyTextOnNDFT():
    # For the Kr stack response, we need to do a DFT (not an FFT)
    # With a stack response you don't have even intervals between subsequent points as in a 'nornal' DFT/FFT
    # The intervals are defined by the |offset| increments between subsequent traces
    # The keyword here is "NUFFT" (Non Uniform FFT) as well as "NFFT" (another package).
    # See: https://nl.mathworks.com/help/matlab/ref/double.nufft.html
    # See: https://pynufft.readthedocs.io/en/latest/index.html
    # And: https://pynufft.readthedocs.io/en/latest/tutor/example.html
    # See: https://github.com/bensarthou/pynufft_benchmark
    # Ref: https://stackoverflow.com/questions/62785140/coding-a-discrete-fourier-transform-on-python-without-using-built-in-functions
    # Ref: https://stackoverflow.com/questions/63534781/how-to-obtain-frequencies-in-non-uniform-dfft

    # See: https://en.wikipedia.org/wiki/Non-uniform_discrete_Fourier_transform
    # See: https://www-user.tu-chemnitz.de/~potts/nfft/
    # See: https://github.com/ghisvail/pyNFFT
    # See: https://pythonhosted.org/pyNFFT/tutorial.html
    # See: https://pythonhosted.org/pyNFFT/api/nfft.html
    # See: https://stackoverflow.com/questions/26014375/fourier-coefficients-for-nfft-non-uniform-fast-fourier-transform
    # See: https://stackoverflow.com/questions/67350588/example-python-nfft-fourier-transform-issues-with-signal-reconstruction-normal
    # See: https://indico.cern.ch/event/484296/contributions/2002827/attachments/1215869/1775559/OscarBLANCO_Commissioning2016.pdf
    # See: https://notebook.community/jakevdp/nfft/notebooks/ImplementationWalkthrough
    # See: https://rdrr.io/github/gzt/rNFFT/man/ndft_1d.html

    # def progress(count, total, status=''):
    # 	barLen = 60
    # 	filledLen = int(round(barLen * count / float(total)))
    # 	percents = round(100.0 * count / float(total), 1)
    # 	bar = '=' * filledLen + '-' * (barLen - filledLen)
    # 	sys.stdout.write('[%s] %s%s ...%s\r' % (bar, percents, '%', status))
    # 	sys.stdout.flush()
    #
    # def ndft_1D(x, f, N):
    # 	"""non-equispaced discrete Fourier transform"""
    # 	k = -(N // 2) + np.arange(N)
    # 	return np.dot(f, np.exp(2j * np.pi * k * x[:, np.newaxis]))
    #
    #
    # def ndft_2D(x, f, Nd):
    # 	M,N = Nd[0], Nd[1]
    # 	K = np.shape(x)[0]
    # 	ndft2d = [0.0 for i in range(K)]
    # 	for k in range(K):
    # 		# print('k',k ,'sur ', K)
    # 		progress(k, K)
    # 		summed = 0.0
    # 		for m in range(M):
    # 			for n in range(N):
    # 				# print(n,m)
    # 				value = f[m, n]
    # 				e = np.exp(- 1j * 2*np.pi * (x[k,0] + x[k,1]))
    # 				summed += value * e
    # 		ndft2d[k] = summed / M / N
    # 	return ndft2d
    #
    # https://stackoverflow.com/questions/11333454/2d-fft-using-1d-fft
    ...


def makeParmsFromPen(pen):
    ps = Qt.PenStyle
    cs = Qt.PenCapStyle
    js = Qt.PenJoinStyle

    color = pen.color()
    width = pen.width()
    pStyle = getNameFromQtEnum(ps, pen.style())
    cStyle = getNameFromQtEnum(cs, pen.capStyle())
    jStyle = getNameFromQtEnum(js, pen.joinStyle())
    cosmet = pen.isCosmetic()

    r = color.red()
    g = color.green()
    b = color.blue()
    a = color.alpha()
    c = (r, g, b, a)

    params = (c, width, pStyle, cStyle, jStyle, cosmet)
    return params


def naturalSort(lst):
    def convert(text):
        return int(text) if text.isdigit() else text.lower()

    def alphanumKey(key):
        return [convert(c) for c in re.split('([0-9]+)', key)]

    return sorted(lst, key=alphanumKey)


def containsPoint2D(border: QRectF, point: QPointF) -> bool:
    if border.isNull():
        return True                                                             # no need to clip with an empty rect
    return border.contains(point)


def containsPoint3D(border: QRectF, point: QVector3D) -> bool:
    if border.isNull():
        return True                                                             # no need to clip with an empty rect
    return border.contains(point.toPointF())


def clipRectF(rect: QRectF, border: QRectF) -> QRectF:
    if border.isNull():
        return QRectF(rect)                                                     # don't clip against an empty rect !
    return QRectF(rect) & border                                                # get the area inside the border


def clipLineF(line: QLineF, border: QRectF) -> QLineF:

    # Python routine to implement Cohen Sutherland algorithm for line clipping.
    # See: https://www.geeksforgeeks.org/line-clipping-set-1-cohen-sutherland-algorithm/
    # See: https://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland_algorithm
    # See: https://www.geeksforgeeks.org/line-clipping-set-2-cyrus-beck-algorithm/?ref=rp

    # Define region codes
    INSIDE = 0                                                                  # 0000
    LEFT = 1                                                                    # 0001
    RIGHT = 2                                                                   # 0010
    BOTTOM = 4                                                                  # 0100
    TOP = 8  	                                                                # 1000

    if border.isNull():
        return QLineF(line)                                                     # don't clip against an empty rect !
    if line.isNull():
        return QLineF()                                                         # null in ?  null out !

    # for sake of ease; copy the line & rect elements to individual real values
    x1 = line.x1()
    y1 = line.y1()
    x2 = line.x2()
    y2 = line.y2()
    yMin = border.top()
    xMin = border.left()
    yMax = border.bottom()
    xMax = border.right()

    # Inner function to compute the region code for a point(x, y) relative to the border of the rectangle
    def computeCode(x, y):
        code = INSIDE
        if x < xMin:                                                           # to the left of rectangle
            code |= LEFT
        elif x > xMax:                                                         # to the right of rectangle
            code |= RIGHT
        if y < yMin:                                                           # below the rectangle
            code |= BOTTOM
        elif y > yMax:                                                         # above the rectangle
            code |= TOP
        return code

    # Compute region codes for P1, P2
    code1 = computeCode(x1, y1)
    code2 = computeCode(x2, y2)
    accept = False

    while True:                                                                 # Keep doing this, till we can escape

        if code1 == 0 and code2 == 0:                                           # both endpoints lie within rectangle
            accept = True
            break

        if (code1 & code2) != 0:                                                # both endpoints are outside rectangle
            break

        # If we get here, the line needs clipping
        # At least one of the points is outside of rect, select it

        x = 1.0
        y = 1.0

        if code1 != 0:
            codeOut = code1
        else:
            codeOut = code2

        # Find intersection point using formulas
        #   y = y1 + slope * (x - x1),
        #   x = x1 + (1 / slope) * (y - y1)
        if codeOut & TOP:  		                                            # point is above the clip rectangle
            x = x1 + (x2 - x1) * (yMax - y1) / (y2 - y1)
            y = yMax

        elif codeOut & BOTTOM:  		                                        # point is below the clip rectangle
            x = x1 + (x2 - x1) * (yMin - y1) / (y2 - y1)
            y = yMin

        elif codeOut & RIGHT:  		                                        # point is to the right of the clip rectangle
            y = y1 + (y2 - y1) * (xMax - x1) / (x2 - x1)
            x = xMax

        elif codeOut & LEFT:  		                                            # point is to the left of the clip rectangle
            y = y1 + (y2 - y1) * (xMin - x1) / (x2 - x1)
            x = xMin

        # Now an intersection point (x, y) has been found,
        # we replace the point outside clipping rectangle
        # by the intersection point
        if codeOut == code1:
            x1 = x
            y1 = y
            code1 = computeCode(x1, y1)
        else:
            x2 = x
            y2 = y
            code2 = computeCode(x2, y2)

    if accept:
        return QLineF(x1, y1, x2, y2)                                           # return the clipped line

    return QLineF()                                                             # return a null line


# some functions to count number of lines in a text file.
# See: https://stackoverflow.com/questions/845058/how-to-get-line-count-of-a-large-file-cheaply-in-python/68385697#68385697
def rawCount(filename):
    with open(filename, 'rb') as f:
        lines = 0
        bufSize = 1024 * 1024
        readFile = f.raw.read

        buf = readFile(bufSize)
        while buf:
            lines += buf.count(b'\n')
            buf = readFile(bufSize)
    return lines


def bufCountNewlinesGen(fname):
    def makeGen(reader):
        while True:
            b = reader(2**16)
            if not b:
                break
            yield b

    with open(fname, 'rb') as f:
        count = sum(buf.count(b'\n') for buf in makeGen(f.raw.read))
    return count


def countHeaderLines(filename):
    # This function uses a for-loop directly on the file pointer
    count = 0

    with open(filename, encoding='utf-8') as fp:
        for line in fp:
            if line.startswith('H'):
                count += 1
            else:
                return count
    return count


def countHeaderLines2(filename):
    # See: https://pynative.com/python-count-number-of-lines-in-file/
    # This function uses readlines to first establish total number of lines
    count = 0
    with open(filename, encoding='utf-8') as fp:
        lines = fp.readlines()
        for line in lines:
            if line.startswith('H'):
                count += 1

            return count
    return count


def isFileInUse(filePath):
    path = Path(filePath)

    if not path.exists():
        return False

    # try to rename the file to itself;
    # if it is in use, this will raise a PermissionError
    try:
        path.rename(path)
    except PermissionError:
        return True

    return False


def getUnpicklable(instance, exception=None, string='', firstOnly=True):
    # See: https://stackoverflow.com/questions/30499341/establishing-why-an-object-cant-be-pickled
    """
    Recursively go through all attributes of instance and return a list of whatever can't be pickled.

    Set firstOnly to only print the first problematic element in a list, tuple or
    dict (otherwise there could be lots of duplication).
    """
    problems = []
    if isinstance(instance, tuple) or isinstance(instance, list):
        for k, v in enumerate(instance):
            try:
                pickle.dumps(v)
            except BaseException as e:
                problems.extend(getUnpicklable(v, e, string + f'[{k}]'))
                if firstOnly:
                    break
    elif isinstance(instance, dict):
        for k in instance:
            try:
                pickle.dumps(k)
            except BaseException as e:
                problems.extend(getUnpicklable(k, e, string + f'[key type={type(k).__name__}]'))
                if firstOnly:
                    break
        for v in instance.values():
            try:
                pickle.dumps(v)
            except BaseException as e:
                problems.extend(getUnpicklable(v, e, string + f'[val type={type(v).__name__}]'))
                if firstOnly:
                    break
    else:
        for k, v in instance.__dict__.items():
            try:
                pickle.dumps(v)
            except BaseException as e:
                problems.extend(getUnpicklable(v, e, string + '.' + k))

    # if we get here, it means pickling instance caused an exception (string is not
    # empty), yet no member was a problem (problems is empty), thus instance itself
    # is the problem.
    if string != '' and not problems:
        problems.append(string + f" (Type '{type(instance).__name__}' caused: {exception})")
    return problems


def computeLcm(x, y):
    # function to find the Least Common Multiple (LCM) of two input numbers
    # choose the greater number
    if x > y:
        greater = x
    else:
        greater = y

    while True:
        if (greater % x == 0) and (greater % y == 0):
            lcm = greater
            break
        greater += 1

    return lcm


def convexHull(x, y):
    # see: https://stackoverflow.com/questions/1500595/convex-hull-in-python
    # see: https://bitbucket.org/william_rusnack/minimumboundingbox/src/master/MinimumBoundingBox.py
    # see: https://github.com/dbworth/minimum-area-bounding-rectangle/tree/master
    # see: https://gist.github.com/kchr/77a0ee945e581df7ed25

    points = np.column_stack((x, y))

    # Three inner functions for the recursive dome algorithm;
    # see: https://en.wikipedia.org/wiki/Convex_hull_algorithms#Dome_algorithm
    def link(a, b):
        return np.concatenate((a, b[1:]))

    def edge(a, b):
        return np.concatenate(([a], [b]))

    def dome(points, base):
        h, t = base
        dists = np.dot(points - h, np.dot(((0, -1), (1, 0)), (t - h)))
        outer = np.repeat(points, dists > 0, 0)
        if len(outer):
            pivot = points[np.argmax(dists)]
            return link(dome(outer, edge(h, pivot)), dome(outer, edge(pivot, t)))
        return base

    if len(points) > 2:
        axis = points[:, 0]
        base = np.take(points, [np.argmin(axis), np.argmax(axis)], 0)
        return link(dome(points, base), dome(points, base[::-1]))

    return points


def transformConvexHull(hullPoints, transform):
    """
    Transforms the output of convexHull(x, y) using QTransform.map().

    :param hullPoints: A NumPy array of points (x, y) representing the convex hull.
                        Expected shape: (n, 2), where n is the number of vertices.
    :param transform: A QTransform object to apply the transformation.
    :return: A transformed NumPy array of points (x, y).
    """
    if hullPoints is None or len(hullPoints) < 1:
        raise ValueError('Invalid convex hull points. At least 1 point is required.')

    if not isinstance(transform, QTransform):
        raise TypeError('The transform parameter must be a QTransform object.')

    # Apply the transformation to each point
    transformedPoints = np.array([transform.map(x, y) for x, y in hullPoints])

    return transformedPoints


def convexHullToQgisPolygon(hullPoints):
    """
    Converts the output of convexHull(x, y) to a QGIS polygon.

    :param hullPoints: A NumPy array of points (x, y) representing the convex hull.
                        Expected shape: (n, 2), where n is the number of vertices.
    :return: A QGIS polygon geometry (QgsGeometry).
    """
    if hullPoints is None or len(hullPoints) < 3:
        raise ValueError('Invalid convex hull points. At least 3 points are required to form a polygon.')

    # Convert the NumPy array of points to a list of QgsPointXY
    vertices = [QgsPointXY(x, y) for x, y in hullPoints]

    # Ensure the polygon is closed by appending the first point to the end
    if vertices[0] != vertices[-1]:
        vertices.append(vertices[0])

    # Create a QGIS polygon geometry
    polygon = QgsGeometry.fromPolygonXY([vertices])

    return polygon


def numpyToQpolygonF(xdata, ydata):
    # See: https://github.com/PlotPyStack/PythonQwt/blob/master/qwt/plot_curve.py#L63
    """
    Utility function to convert two 1D-NumPy arrays representing curve data
    (X-axis, Y-axis data) into a single polyline (QtGui.PolygonF object).
    License/copyright: MIT License © Pierre Raybaut 2020-2021.

    :param numpy.ndarray xdata: 1D-NumPy array
    :param numpy.ndarray ydata: 1D-NumPy array
    :return: Polyline
    :rtype: QtGui.QPolygonF
    """
    if not xdata.size == ydata.size == xdata.shape[0] == ydata.shape[0]:
        raise ValueError('Arguments must be 1D NumPy arrays with same size')
    size = xdata.size
    polyline = QPolygonF(size)
    buffer = polyline.data()
    buffer.setsize(16 * size)  # 16 bytes per point: 8 bytes per X,Y value (float64)
    memory = np.frombuffer(buffer, np.float64)
    memory[: (size - 1) * 2 + 1: 2] = np.asarray(xdata, dtype=np.float64)
    memory[1: (size - 1) * 2 + 2: 2] = np.asarray(ydata, dtype=np.float64)
    return polyline


def aboutText() -> str:
    # See : https://github.com/qgis/QGIS/blob/master/python/utils.py#L291 for version info

    currentDir = os.path.dirname(os.path.abspath(__file__))
    filePath = os.path.join(currentDir, 'metadata.txt')

    parser = configparser.ConfigParser()
    parser.optionxform = str
    parser.read(filePath)

    metadata = []
    metadata.extend(parser.items('general'))
    metaDict = dict(metadata)                                               # See: https://stackoverflow.com/questions/2191699/find-an-element-in-a-list-of-tuples
    rollVersion = metaDict['version']

    pythonVersionList = sys.version.split(' ')
    pythonVersion = pythonVersionList[0]

    numbaVersion = numba.__version__ if haveNumba else 'not installed'
    dbgPyVersion = debugpy.__version__ if haveDebugpy else 'not installed'
    matplotlibVersion = matplotlib.__version__ if haveMatplotlib else 'not installed'

    sourceUrl = "<a href='https://github.com/MrBeee/roll'>here</a>"
    sampleUrl = "<a href='https://github.com/MrBeee/roll_samples'>here</a>"

    text = (
        f'Roll can be used to generate seismic survey geometries. <br>'
        f'Both for Land/OBN as well as marine seismic surveys. <ul>'
        f'<li>Roll version: {rollVersion}. </li></ul>'
        f'The following Python & framework versions are used: <ul>'
        f'<li>Python version: {pythonVersion} </li>'
        f'<li>Qt version: {QT_VERSION_STR} </li>'
        f'<li>PyQt version: {PYQT_VERSION_STR} </li></ul>'
        f'The following libraries are used: <ul>'
        f'<li>Numpy version: {np.__version__} </li>'
        f'<li>Numba version: {numbaVersion} </li>'
        f'<li>Debugpy version: {dbgPyVersion} </li>'
        f'<li>Rasterio version: {rio.__version__} </li>'
        f'<li>PyQtGraph version: {pg.__version__} </li>'
        f'<li>Matplotlib version: {matplotlibVersion} </li>'
        f'<li>Wellpathpy version: {wp.__version__} </li></ul>'
        f'Source code is available on GitHub {sourceUrl} <br> '
        f'Sample projects are available on GitHub {sampleUrl} <br><br> '
        f'Copyright © 2022 - present by Duijndam.Dev'
    )

    return text


def highDpiText() -> str:

    dpiUrl = "<a href='https://github.com/qgis/QGIS/issues/53898'>here</a>"

    dpiText = (
        f'As of V3.32 High DPI UI scaling issues have arisen in QGIS. <br>'
        f'See the following discussion on GitHub {dpiUrl} <br> '
        f'The work around that has been proposed is as follows: <ol>'
        f"<li>Right click 'qgis-bin.exe' in folder 'C:\\Program Files\\QGIS 3.36.3\\bin' </li>"
        f"<li>Select 'Properties' </li>"
        f'<li>Select the Compatibility tab </li>'
        f"<li>Select 'change high DPI settings' </li>"
        f"<li>Set the tickmark before 'Override high DPI ...' </li>"
        f"<li>Have scaling performed by 'Application' </li>"
        f"<li>In the same folder edit the file 'qgis-bin.env' </li>"
        f'<li>Add one line at the very end: </li>'
        f'<li>QT_SCALE_FACTOR_ROUNDING_POLICY=Floor </li>'
        f"<li>Save the file in a different folder as 'C:\\Program Files' is write-protected </li>"
        f"<li>Copy the edited file back to the 'C:\\Program Files\\QGIS 3.36.3\\bin' folder </li>"
        f"<li>You'll be asked to confirm you want to overwrite the existing file. Go ahead. </li></ol>"
        f'This addresses the font and button scaling problems ! Finally: <br>'
        f'It is recommended to use font size 8.0 and Icon size 24, in Settings -> Options... -> General'
    )

    return dpiText


def licenseText() -> str:
    licenseTxt = (
        "\n"
        "    Copyright  © 2022 - present by Duijndam.Dev. All rights reserved.\n"
        "\n"
        "    Redistribution and use in source and binary forms, with or without modification,"
        " are permitted provided that the following conditions are met:\n"
        "\n"
        "    Redistributions of source code must retain the above copyright notice,"
        " this list of conditions and the following disclaimer.\n"
        "\n"
        "    Redistributions in binary form must reproduce the above copyright notice,"
        " this list of conditions and the following disclaimer in the documentation"
        " and/or other materials provided with the distribution.\n"
        "\n"
        "    Neither the name of Mapbox nor the names of its contributors may be used"
        " to endorse or promote products derived from this software"
        " without specific prior written permission.\n"
        "\n"
        "    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS"
        " “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,"
        " THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE"
        " ARE DISCLAIMED. IN NO EVENT SHALL DUIJNDAM.DEV BE LIABLE FOR ANY DIRECT, INDIRECT,"
        " INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,"
        " PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;"
        " OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,"
        " WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)"
        " ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,"
        " EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
    )
    return licenseTxt


def exampleSurveyXmlText() -> str:
    wktText = (
        'PROJCRS["Amersfoort / RD New",BASEGEOGCRS["Amersfoort",'
        'DATUM["Amersfoort",ELLIPSOID["Bessel 1841",6377397.155,299.1528128,'
        'LENGTHUNIT["metre",1]]],PRIMEM["Greenwich",0,'
        'ANGLEUNIT["degree",0.0174532925199433]],ID["EPSG",4289]],'
        'CONVERSION["RD New",METHOD["Oblique Stereographic",ID["EPSG",9809]],'
        'PARAMETER["Latitude of natural origin",52.1561605555556,'
        'ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8801]],'
        'PARAMETER["Longitude of natural origin",5.38763888888889,'
        'ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8802]],'
        'PARAMETER["Scale factor at natural origin",0.9999079,'
        'SCALEUNIT["unity",1],ID["EPSG",8805]],'
        'PARAMETER["False easting",155000,LENGTHUNIT["metre",1],ID["EPSG",8806]],'
        'PARAMETER["False northing",463000,LENGTHUNIT["metre",1],ID["EPSG",8807]]],'
        'CS[Cartesian,2],AXIS["easting (X)",east,ORDER[1],LENGTHUNIT["metre",1]],'
        'AXIS["northing (Y)",north,ORDER[2],LENGTHUNIT["metre",1]],'
        'USAGE[SCOPE["Engineering survey, topographic mapping."],'
        'AREA["Netherlands - onshore, including Waddenzee, Dutch Wadden Islands '
        'and 12-mile offshore coastal zone."],'
        'BBOX[50.75,3.2,53.7,7.22]],ID["EPSG",28992]]'
    )

    xmlText = f"""<?xml version="1.0" encoding="UTF-8"?>
    <survey version="1.0">
        <type>Orthogonal</type>
        <name>New survey</name>
        <surveyCrs>
            <spatialrefsys nativeFormat="Wkt">
                <wkt>{wktText}</wkt>
                <proj4>+proj=sterea +lat_0=52.1561605555556 +lon_0=5.38763888888889 +k=0.9999079 +x_0=155000 +y_0=463000 +ellps=bessel +units=m +no_defs</proj4>
                <srsid>2517</srsid>
                <srid>28992</srid>
                <authid>EPSG:28992</authid>
                <description>Amersfoort / RD New</description>
                <projectionacronym>sterea</projectionacronym>
                <ellipsoidacronym>EPSG:7004</ellipsoidacronym>
                <geographicflag>false</geographicflag>
            </spatialrefsys>
        </surveyCrs>
        <limits>
            <output xmax="7225.0" ymax="1800.0" xmin="6975.0" ymin="1400.0"/>
            <angles refmin="0.0" azimin="0.0" azimax="360.0" refmax="45.0"/>
            <offset rmin="0.0" xmax="5976.0" rmax="6027.0" ymax="776.0" xmin="-5976.0" ymin="-776.0"/>
            <unique deltaoff="200.0" azislots="1" apply="False" write="False"/>
            <binning vint="2000.0" method="cmp"/>
            <cfpana zfocus="2000.0" vrms="2000.0" useBinningAreaCenter="True" frequencies="8;16;20;24;32" maxAperture="45.0" xfocus="0.0" yfocus="0.0"/>
        </limits>
        <reflectors>
            <plane azi="45.0" z0="-2000.0" dip="4.0" y0="450000.0" x0="150000.0"/>
            <!--Plane equation: -0.049325·x + -0.049325·y + 0.997564·z + 31590.294922 = 0  -->
            <!--Plane is defined in global coordinates. Subsurface corresponds with negative z-values-->
            <sphere z0="-4000.0" radius="2000.0" y0="456100.0" x0="153900.0"/>
            <!--Sphere is defined in global coordinates. Subsurface corresponds with negative z-values-->
        </reflectors>
        <grid>
            <local l0="1000.0" s0="1000.0" fold="-1" dl="25.0" dy="25.0" y0="12.5" dx="25.0" ds="25.0" x0="12.5"/>
            <global azi="45.0" sy="1.0" sx="1.0" y0="450000.0" x0="150000.0"/>
            <!--Forward transform: A0=150000.000, B0=450000.000, A1=0.707107, B1=0.707107, A2=-0.707107, B2=0.707107-->
            <!--Inverse transform: A0=-424264.069, B0=-212132.034, A1=0.707107, B1=-0.707107, A2=0.707107, B2=0.707107-->
            <!--See EPSG:9624 (https://epsg.io/9624-method) for the affine parametric transform definition-->
        </grid>
        <block_list>
            <block>
                <name>Block-1</name>
                <borders>
                    <src_border xmax="20000.0" ymax="20000.0" ymin="-20000.0" xmin="-20000.0"/>
                    <rec_border xmax="0.0" ymax="0.0" ymin="0.0" xmin="0.0"/>
                </borders>
                <template_list>
                    <template>
                        <name>Template-1</name>
                        <roll_list>
                            <translate dz="0.0" dy="0.0" n="1" dx="0.0"/>
                            <translate dz="0.0" dy="200.0" n="10" dx="0.0"/>
                            <translate dz="0.0" dy="0.0" n="10" dx="250.0"/>
                        </roll_list>
                        <seed_list>
                            <seed azi="False" typno="0" z0="0.0" y0="625.0" argb="#77ff0000" x0="5975.0" src="True" patno="0">
                                <name>Src-1</name>
                                <grid roll="False" points="4">
                                    <translate dz="0.0" dy="0.0" n="1" dx="0.0"/>
                                    <translate dz="0.0" dy="0.0" n="1" dx="250.0"/>
                                    <translate dz="0.0" dy="50.0" n="4" dx="0.0"/>
                                </grid>
                            </seed>
                            <seed azi="False" typno="0" z0="0.0" y0="0.0" argb="#7700b0f0" x0="0.0" src="False" patno="1">
                                <name>Rec-1</name>
                                <grid roll="False" points="1920">
                                    <translate dz="0.0" dy="0.0" n="1" dx="0.0"/>
                                    <translate dz="0.0" dy="200.0" n="8" dx="0.0"/>
                                    <translate dz="0.0" dy="0.0" n="240" dx="50.0"/>
                                </grid>
                            </seed>
                        </seed_list>
                    </template>
                </template_list>
            </block>
        </block_list>
        <pattern_list>
            <pattern>
                <name>src-array</name>
                <seed_list>
                    <seed azi="False" z0="0.0" y0="-12.5" argb="#ffff0000" x0="-0.0">
                        <name>seed-1</name>
                        <grid roll="True" points="3">
                            <translate dz="0.0" dy="0.0" n="1" dx="0.0"/>
                            <translate dz="0.0" dy="0.0" n="1" dx="0.0"/>
                            <translate dz="0.0" dy="12.5" n="3" dx="0.0"/>
                        </grid>
                    </seed>
                </seed_list>
            </pattern>
            <pattern>
                <name>rec-array</name>
                <seed_list>
                    <seed azi="False" z0="0.0" y0="-20.83" argb="#ff0000ff" x0="-20.83">
                        <name>seed-1</name>
                        <grid roll="True" points="18">
                            <translate dz="0.0" dy="0.0" n="3" dx="16.66670036315918"/>
                            <translate dz="0.0" dy="8.333000183105469" n="2" dx="8.333000183105469"/>
                            <translate dz="0.0" dy="16.66699981689453" n="3" dx="0.0"/>
                        </grid>
                    </seed>
                </seed_list>
            </pattern>
        </pattern_list>
    </survey>"""
    return xmlText


def qgisCheatSheetText() -> str:
    qgisText = """
    <h2>QGIS Cheat Sheet</h2>
    <ul>
        <li><b>Ctrl + Shift + P</b>: Open Python console</li>
        <li><b>Ctrl + Shift + C</b>: Copy selected feature(s)</li>
        <li><b>Ctrl + Shift + V</b>: Paste copied feature(s)</li>
        <li><b>Ctrl + Shift + R</b>: Refresh the map canvas</li>
        <li><b>Ctrl + Shift + T</b>: Toggle the visibility of the attribute table</li>
        <li><b>Ctrl + Shift + E</b>: Open the expression dialog</li>
        <li><b>Ctrl + Shift + L</b>: Toggle the visibility of the layer list</li>
        <li><b>Ctrl + Shift + I</b>: Open the identify features tool</li>
    </ul>
    """
    return qgisText


eventLookup = {
    # See: https://stackoverflow.com/questions/62196835/how-to-get-string-name-for-qevent-in-pyqt5
    # usage: print(eventLookup[str(event.type())])
    '216': 'QEvent::UNKNOWN EVENT !',
    '0': 'QEvent::None',
    '114': 'QEvent::ActionAdded',
    '113': 'QEvent::ActionChanged',
    '115': 'QEvent::ActionRemoved',
    '99': 'QEvent::ActivationChange',
    '121': 'QEvent::ApplicationActivate',
    '122': 'QEvent::ApplicationDeactivate',
    '36': 'QEvent::ApplicationFontChange',
    '37': 'QEvent::ApplicationLayoutDirectionChange',
    '38': 'QEvent::ApplicationPaletteChange',
    '214': 'QEvent::ApplicationStateChange',
    '35': 'QEvent::ApplicationWindowIconChange',
    '68': 'QEvent::ChildAdded',
    '69': 'QEvent::ChildPolished',
    '71': 'QEvent::ChildRemoved',
    '40': 'QEvent::Clipboard',
    '19': 'QEvent::Close',
    '200': 'QEvent::CloseSoftwareInputPanel',
    '178': 'QEvent::ContentsRectChange',
    '82': 'QEvent::ContextMenu',
    '183': 'QEvent::CursorChange',
    '52': 'QEvent::DeferredDelete',
    '60': 'QEvent::DragEnter',
    '62': 'QEvent::DragLeave',
    '61': 'QEvent::DragMove',
    '63': 'QEvent::Drop',
    '170': 'QEvent::DynamicPropertyChange',
    '98': 'QEvent::EnabledChange',
    '10': 'QEvent::Enter',
    '150': 'QEvent::EnterEditFocus',
    '124': 'QEvent::EnterWhatsThisMode',
    '206': 'QEvent::Expose',
    '116': 'QEvent::FileOpen',
    '8': 'QEvent::FocusIn',
    '9': 'QEvent::FocusOut',
    '23': 'QEvent::FocusAboutToChange',
    '97': 'QEvent::FontChange',
    '198': 'QEvent::Gesture',
    '202': 'QEvent::GestureOverride',
    '188': 'QEvent::GrabKeyboard',
    '186': 'QEvent::GrabMouse',
    '159': 'QEvent::GraphicsSceneContextMenu',
    '164': 'QEvent::GraphicsSceneDragEnter',
    '166': 'QEvent::GraphicsSceneDragLeave',
    '165': 'QEvent::GraphicsSceneDragMove',
    '167': 'QEvent::GraphicsSceneDrop',
    '163': 'QEvent::GraphicsSceneHelp',
    '160': 'QEvent::GraphicsSceneHoverEnter',
    '162': 'QEvent::GraphicsSceneHoverLeave',
    '161': 'QEvent::GraphicsSceneHoverMove',
    '158': 'QEvent::GraphicsSceneMouseDoubleClick',
    '155': 'QEvent::GraphicsSceneMouseMove',
    '156': 'QEvent::GraphicsSceneMousePress',
    '157': 'QEvent::GraphicsSceneMouseRelease',
    '182': 'QEvent::GraphicsSceneMove',
    '181': 'QEvent::GraphicsSceneResize',
    '168': 'QEvent::GraphicsSceneWheel',
    '18': 'QEvent::Hide',
    '27': 'QEvent::HideToParent',
    '127': 'QEvent::HoverEnter',
    '128': 'QEvent::HoverLeave',
    '129': 'QEvent::HoverMove',
    '96': 'QEvent::IconDrag',
    '101': 'QEvent::IconTextChange',
    '83': 'QEvent::InputMethod',
    '207': 'QEvent::InputMethodQuery',
    '169': 'QEvent::KeyboardLayoutChange',
    '6': 'QEvent::KeyPress',
    '7': 'QEvent::KeyRelease',
    '89': 'QEvent::LanguageChange',
    '90': 'QEvent::LayoutDirectionChange',
    '76': 'QEvent::LayoutRequest',
    '11': 'QEvent::Leave',
    '151': 'QEvent::LeaveEditFocus',
    '125': 'QEvent::LeaveWhatsThisMode',
    '88': 'QEvent::LocaleChange',
    '176': 'QEvent::NonClientAreaMouseButtonDblClick',
    '174': 'QEvent::NonClientAreaMouseButtonPress',
    '175': 'QEvent::NonClientAreaMouseButtonRelease',
    '173': 'QEvent::NonClientAreaMouseMove',
    '177': 'QEvent::MacSizeChange',
    '43': 'QEvent::MetaCall',
    '102': 'QEvent::ModifiedChange',
    '4': 'QEvent::MouseButtonDblClick',
    '2': 'QEvent::MouseButtonPress',
    '3': 'QEvent::MouseButtonRelease',
    '5': 'QEvent::MouseMove',
    '109': 'QEvent::MouseTrackingChange',
    '13': 'QEvent::Move',
    '197': 'QEvent::NativeGesture',
    '208': 'QEvent::OrientationChange',
    '12': 'QEvent::Paint',
    '39': 'QEvent::PaletteChange',
    '131': 'QEvent::ParentAboutToChange',
    '21': 'QEvent::ParentChange',
    '212': 'QEvent::PlatformPanel',
    '217': 'QEvent::PlatformSurface',
    '75': 'QEvent::Polish',
    '74': 'QEvent::PolishRequest',
    '123': 'QEvent::QueryWhatsThis',
    '106': 'QEvent::ReadOnlyChange',
    '199': 'QEvent::RequestSoftwareInputPanel',
    '14': 'QEvent::Resize',
    '204': 'QEvent::ScrollPrepare',
    '205': 'QEvent::Scroll',
    '117': 'QEvent::Shortcut',
    '51': 'QEvent::ShortcutOverride',
    '17': 'QEvent::Show',
    '26': 'QEvent::ShowToParent',
    '50': 'QEvent::SockAct',
    '192': 'QEvent::StateMachineSignal',
    '193': 'QEvent::StateMachineWrapped',
    '112': 'QEvent::StatusTip',
    '100': 'QEvent::StyleChange',
    '87': 'QEvent::TabletMove',
    '92': 'QEvent::TabletPress',
    '93': 'QEvent::TabletRelease',
    '171': 'QEvent::TabletEnterProximity',
    '172': 'QEvent::TabletLeaveProximity',
    '219': 'QEvent::TabletTrackingChange',
    '22': 'QEvent::ThreadChange',
    '1': 'QEvent::Timer',
    '120': 'QEvent::ToolBarChange',
    '110': 'QEvent::ToolTip',
    '184': 'QEvent::ToolTipChange',
    '194': 'QEvent::TouchBegin',
    '209': 'QEvent::TouchCancel',
    '196': 'QEvent::TouchEnd',
    '195': 'QEvent::TouchUpdate',
    '189': 'QEvent::UngrabKeyboard',
    '187': 'QEvent::UngrabMouse',
    '78': 'QEvent::UpdateLater',
    '77': 'QEvent::UpdateRequest',
    '111': 'QEvent::WhatsThis',
    '118': 'QEvent::WhatsThisClicked',
    '31': 'QEvent::Wheel',
    '132': 'QEvent::WinEventAct',
    '24': 'QEvent::WindowActivate',
    '103': 'QEvent::WindowBlocked',
    '25': 'QEvent::WindowDeactivate',
    '34': 'QEvent::WindowIconChange',
    '105': 'QEvent::WindowStateChange',
    '33': 'QEvent::WindowTitleChange',
    '104': 'QEvent::WindowUnblocked',
    '203': 'QEvent::WinIdChange',
    '126': 'QEvent::ZOrderChange',
}
