"""
/***************************************************************************
 qgSurf - plugin for Quantum GIS

 geologic stereoplots
-------------------

    Begin                : 2015.04.18
    Copyright            : (C) 2015-2026 by Mauro Alberti
    Email                : alberti dot m65 at gmail dot com

 ***************************************************************************/

# licensed under the terms of GNU GPL 3

/***************************************************************************
 *                                                                         *
 *   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 3 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
"""

import numbers
from typing import Tuple, List, Dict

from builtins import map

from ..external.apsg import StereoNet, Lin as aLin, Fol as aFol, Fault as aFault

from geogst.dependencies.apsg.faults import *
from geogst.core.geology.utils.faults import *
from geogst.core.geology.utils.parsing import *
from geogst.core.inspections.errors import *
from geogst.core.orientations.orientations import *
from geogst.plots.parameters import *

from ..apps.plots.export import *
from ..apps.qgis.messages import *
from ..apps.qgis.points import *
from ..apps.qt.colors import *

from ..ui.auxiliary_windows.stereonet import *

from ..configurations.general_params import *


class InputLayerDataCharacteristics:

    def __init__(
        self,
        has_plane_data: bool,
        pln_az_type: str,
        pln_dip_type: str,
        has_line_trpl_data: bool,
        ln_az_type: str,
        ln_dip_type: str,
        has_ln_ms_data: bool,
        ln_ms_type: str,
        has_rake_data: bool,
        ln_rake_type: str
    ):

        self.has_plane_data = has_plane_data
        self.pln_az_type = pln_az_type
        self.pln_dip_type = pln_dip_type
        self.has_line_trpl_data = has_line_trpl_data
        self.ln_az_type = ln_az_type
        self.ln_dip_type = ln_dip_type
        self.has_ln_ms_data = has_ln_ms_data
        self.ln_ms_type = ln_ms_type
        self.has_rake_data = has_rake_data
        self.ln_rake_type = ln_rake_type


class SourceLayerParams:

    def __init__(
        self,
        layer_src_data=False,
        src_layer=None,
        attitude_fld_names=None,
        input_data_types=None
    ):

        self.layer_src_data = layer_src_data
        self.src_layer = src_layer
        self.attitude_fld_names = [] if attitude_fld_names is None else attitude_fld_names
        self.input_data_types = [] if input_data_types is None else input_data_types


class SourceTextParams:

    def __init__(
        self,
        text_src_data=False,
        geostructural_data=None
    ):

        self.text_src_data = text_src_data
        self.geostructural_data = [] if geostructural_data is None else geostructural_data


def parse_plane_dirdir(
    azimuth_type: str,
    az_raw: numbers.Real
) -> numbers.Real:

    if azimuth_type == "strike rhr":
        dip_dir = az_raw + 90.0
    else:
        dip_dir = az_raw

    return dip_dir % 360.0


def parse_fault_mov_sense(
    raw_values: List
) -> Tuple[numbers.Real, numbers.Real, numbers.Real, numbers.Real, str]:

    azim, dip_ang, lin_trend, lin_plunge = list(map(float, raw_values[:4]))
    mov_sense = str(raw_values[4])

    return azim, dip_ang, lin_trend, lin_plunge, mov_sense


def extract_record_values_from_text_row(
    data_type: str,
    azimuth_type: str,
    row: str,
    sep: str,
) -> Tuple[Union[None, dict], Error]:
    """
    Extract record values from text row.
    """

    try:

        record_dict = dict()

        raw_values = row.split(sep)

        if data_type == "planes":

            azim, dip_ang = list(map(float, raw_values))

            record_dict["pln_dipdir"] = parse_plane_dirdir(azimuth_type, azim)
            record_dict["pln_dipang"] = dip_ang

        elif data_type == "axes":

            trend, plunge = list(map(float, raw_values))

            record_dict["ln_tr"] = trend
            record_dict["ln_pl"] = plunge

        elif data_type == "planes & axes":

            azim, dip_ang, trend, plunge = list(map(float, raw_values))

            record_dict["pln_dipdir"] = parse_plane_dirdir(azimuth_type, azim)
            record_dict["pln_dipang"] = dip_ang
            record_dict["ln_tr"] = trend
            record_dict["ln_pl"] = plunge

        elif data_type == "fault planes with slickenline trend, plunge and movement sense":

            azim, dip_ang, slick_tr, slick_pl, mov_sense = parse_fault_mov_sense(raw_values)

            record_dict["pln_dipdir"] = parse_plane_dirdir(azimuth_type, azim)
            record_dict["pln_dipang"] = dip_ang
            record_dict["ln_tr"] = slick_tr
            record_dict["ln_pl"] = slick_pl
            record_dict["ln_ms"] = mov_sense

        elif data_type == "fault planes with rake":

            azim, dip_ang, rake = list(map(float, raw_values))

            record_dict["pln_dipdir"] = parse_plane_dirdir(azimuth_type, azim)
            record_dict["pln_dipang"] = dip_ang
            record_dict["ln_rk"] = rake

        else:

            return None, Error(
                    True,
                    caller_name(),
                    Exception(f"Unimplemented input data type: {data_type}")
                )

        return record_dict, Error()

    except Exception as e:

        return None, Error(
                True,
                caller_name(),
                e,
                traceback.format_exc()
            )


def extract_orientations_from_text(
        data_type,
        azimuth_type,
        text,
        sep=','
) -> Tuple[Union[None, List[Dict]], Error]:
    """
    Extracts orientations from a òist of text rows.
    """

    try:

        geostructural_data = []

        if text is None or text == '':
            return None, Error(
                True,
                caller_name(),
                Exception("No value available")
            )

        rows = text.split("\n")

        if len(rows) == 0:

            return None, Error(
                True,
                caller_name(),
                Exception("No value available")
            )

        for row_ndx, row in enumerate(rows):

            if row:

                data_dict, err = extract_record_values_from_text_row(
                    data_type,
                    azimuth_type,
                    row,
                    sep,
                )

                if err:

                    return None, err

                if not data_dict:

                    return None, Error(
                        True,
                        caller_name(),
                        Exception("Error with input")
                    )

                geostructural_data.append(data_dict)

        return geostructural_data, Error()

    except Exception as e:

        return None, Error(
                True,
                caller_name(),
                e,
                traceback.format_exc()
            )


def parse_field_choice(val, choose_message):

    if val == choose_message:
        return ''
    else:
        return val


def extract_input_layer_params_from_dialog(
    dialog,
) -> Tuple[Union[None, dict], Error]:

    try:

        dInputLayerParams = dict()

        dInputLayerParams["pln_azimuth_type"] = dialog.cmbInputLyrPlaneOrAzimType.currentText()
        dInputLayerParams["pln_azimuth_name_field"] = parse_field_choice(
            dialog.cmbInputPlaneAzimSrcFld.currentText(),
            UNDEFINED_TEXT)

        dInputLayerParams["pln_dip_type"] = dialog.cmbInputPlaneOrientDipType.currentText()
        dInputLayerParams["pln_dip_name_field"] = parse_field_choice(
            dialog.cmbInputPlaneDipSrcFld.currentText(),
            UNDEFINED_TEXT)

        dInputLayerParams["ln_azimuth_type"] = dialog.cmbInputAxisAzimType.currentText()
        dInputLayerParams["ln_azimuth_name_field"] = parse_field_choice(
            dialog.cmbInputAxisAzimSrcFld.currentText(),
            UNDEFINED_TEXT)

        dInputLayerParams["ln_dip_type"] = dialog.cmbInputAxisDipType.currentText()
        dInputLayerParams["ln_dip_name_field"] = parse_field_choice(
            dialog.cmbInputAxisDipSrcFld.currentText(),
            UNDEFINED_TEXT)

        dInputLayerParams["ln_movsen_type"] = dialog.cmbInputAxisMovSenseType.currentText()
        dInputLayerParams["ln_movsen_name_field"] = parse_field_choice(
            dialog.cmbInputAxisMovSenseSrcFld.currentText(),
            UNDEFINED_TEXT)

        dInputLayerParams["ln_rake_type"] = dialog.cmbInputAxisRakeType.currentText()
        dInputLayerParams["ln_rake_name_field"] = parse_field_choice(
            dialog.cmbInputAxisRakeSrcFld.currentText(),
            UNDEFINED_TEXT)

        return dInputLayerParams, Error()

    except Exception as e:

        return None, Error(
                True,
                caller_name(),
                e,
                traceback.format_exc()
            )


def valid_input_layer_params(
    dInputLayerParams: dict,
) -> bool:

    if dInputLayerParams["pln_azimuth_name_field"] is not None and \
                    dInputLayerParams["pln_dip_name_field"] is not None:
        return True
    elif dInputLayerParams["ln_azimuth_name_field"] is not None and \
                    dInputLayerParams["ln_dip_name_field"] is not None:
        return True
    else:
        return False


def input_layer_data_characteristics(
    input_layer_params: dict,
) -> Tuple[Union[None, InputLayerDataCharacteristics], Error]:

    try:

        # define type for planar data

        bIsPlaneDefined = True if input_layer_params["pln_azimuth_name_field"] and input_layer_params[
            "pln_dip_name_field"] else False

        if bIsPlaneDefined:
            tPlaneAzimType = "dip_dir" if input_layer_params["pln_azimuth_type"] == "dip direction" else "strike_rhr"
            tPlaneDipType = "dip"
        else:
            tPlaneAzimType = None
            tPlaneDipType = None

        # define type for linear data

        bIsLnTrndPlngDefined = True if input_layer_params["ln_azimuth_name_field"] and input_layer_params[
            "ln_dip_name_field"] else False
        bIsLnMovSnsDefined = True if bIsLnTrndPlngDefined and input_layer_params["ln_movsen_name_field"] else False
        bIsLnRkDefined = True if bIsPlaneDefined and input_layer_params["ln_rake_name_field"] else False

        if bIsLnTrndPlngDefined:
            tLineAzimType = "trend"
            tLineDipType = "plunge"
        else:
            tLineAzimType = None
            tLineDipType = None

        if bIsLnMovSnsDefined:
            tLineMovSenseType = "movement_sense"
        else:
            tLineMovSenseType = None

        if bIsLnRkDefined:
            tLineRakeType = "rake"
        else:
            tLineRakeType = None

        return InputLayerDataCharacteristics(
            has_plane_data=bIsPlaneDefined,
            pln_az_type=tPlaneAzimType,
            pln_dip_type=tPlaneDipType,
            has_line_trpl_data=bIsLnTrndPlngDefined,
            ln_az_type=tLineAzimType,
            ln_dip_type=tLineDipType,
            has_ln_ms_data=bIsLnMovSnsDefined,
            ln_ms_type=tLineMovSenseType,
            has_rake_data=bIsLnRkDefined,
            ln_rake_type=tLineRakeType
        ), Error()

    except Exception as e:

        return None, Error(
                True,
                caller_name(),
                e,
                traceback.format_exc()
            )


def extract_user_defined_stereoplot_style(
        dialog
) -> Tuple[Union[None, StereonetStyleParams], Error]:

    try:

        line_color = dialog.btnLineColor.color().name()
        line_style = dialog.cmbLineStyle.currentText()
        line_width = dialog.cmbLineWidth.currentText()
        line_transp = dialog.cmbLineTransp.currentText()

        marker_color = dialog.btnPointColor.color().name()
        marker_style = dialog.cmbPointStyle.currentText()
        marker_size = dialog.cmbPointSize.currentText()
        marker_transp = dialog.cmbPointTransp.currentText()

        return StereonetStyleParams(
            line_color,
            line_style,
            line_width,
            line_transp,
            marker_color,
            marker_style,
            marker_size,
            marker_transp,
        ), Error()

    except Exception as e:

        return None, Error(
                True,
                caller_name(),
                e,
                traceback.format_exc()
            )


def parse_layer_data(
    input_data_types: InputLayerDataCharacteristics,
    structural_data
):
    # create list of text holders for data to extract

    data_types_to_extract = [("x", float), ("y", float)]

    if input_data_types.has_plane_data:
        data_types_to_extract += [
            (
                "pln_dipdir",
                dipdir_str_to_float if input_data_types.pln_az_type == "dip_dir" else strikerhr_str_to_dipdir_float
            ),
            (
                "pln_dipang",
                float
            )
        ]

    if input_data_types.has_line_trpl_data:
        data_types_to_extract += [("ln_tr", float), ("ln_pl", float)]

    if input_data_types.has_ln_ms_data:
        data_types_to_extract.append(("ln_ms", str))

    if input_data_types.has_rake_data:
        data_types_to_extract.append(("ln_rk", float))

    # extract and parse raw data

    source_structural_values = []

    for rec in structural_data:

        try:

            dRecord = dict()

            for ndx, (key, func) in enumerate(data_types_to_extract):
                dRecord[key] = func(rec[ndx])

            source_structural_values.append(dRecord)

        except:

            pass

    return source_structural_values


def parse_marker_size(
    marker_size: str
) -> numbers.Real:

    return float(marker_size.split()[0])


def parse_transparency(
    transparency: str
) -> numbers.Real:

    return 1.0 - (float(transparency[:-1]) / 100.0)  # removes final '%' from input value


def filter_plane_data(
    struct_vals: List[dict]
) -> List[Tuple[numbers.Real, numbers.Real]]:

    plane_data = []

    for row in struct_vals:

        if not ("pln_dipdir" in row and "pln_dipang" in row):

            continue

        else:

            plane_data.append((row["pln_dipdir"], row["pln_dipang"]))

    return plane_data


def filter_line_data(
    struct_vals: List[dict]
) -> List[Tuple[numbers.Real, numbers.Real]]:

    line_data = []

    for row in struct_vals:

        if not (("ln_tr" in row and "ln_pl" in row) or
                ("pln_dipdir" in row and "pln_dipang" in row and "ln_rk" in row)):

            continue

        else:

            if "pln_dipdir" in row and "pln_dipang" in row and "ln_rk" in row:

                result = downaxis_from_rake(row["pln_dipdir"], row["pln_dipang"], row["ln_rk"])

                if isinstance(result, Error):
                    print(f"{repr(result)}")
                    continue

                lin_tr, lin_pl = result

            elif "ln_tr" in row and "ln_pl" in row:

                lin_tr, lin_pl = row["ln_tr"], row["ln_pl"]

            else:

                continue

            line_data.append((lin_tr, lin_pl))

    return line_data


def filter_fault_slikenline_data(
    struct_vals: List[dict]
) -> List[Tuple[numbers.Real, numbers.Real, numbers.Real, numbers.Real, numbers.Integral]]:

    fault_slickenline_data = []

    for row in struct_vals:

        if "pln_dipdir" in row and "pln_dipang" in row:

            dip_dir = row["pln_dipdir"]
            dip_ang = row["pln_dipang"]

            if "ln_rk" in row:

                rake = row["ln_rk"]

                # rake must be in the -180 / +180 interval

                if rake < -180:
                    rake = -180
                elif rake > 180:
                    rake = 180

                # apsg do not handle rake = 0° or +/- 180°

                if rake == 0.0:
                    rake = 0.1
                elif rake == 180:
                    rake = 179.9
                elif rake == -180:
                    rake = -179.9

                result = downaxis_from_rake(dip_dir, dip_ang, rake)

                if isinstance(result, Error):
                    print(f"{result}")
                    continue

                lin_tr, lin_pl = result

                sense = rake_to_apsg_movsense(rake)

            elif "ln_tr" in row and "ln_pl" in row and "ln_ms" in row:

                lin_tr, lin_pl = row["ln_tr"], row["ln_pl"]

                mov_sense = row["ln_ms"].upper()

                if mov_sense == "":

                    continue

                else:

                    sense = movsense_to_apsg_movsense(mov_sense)

            else:

                continue

            fault_slickenline_data.append((dip_dir, dip_ang, lin_tr, lin_pl, sense))

        else:

            continue

    return fault_slickenline_data


class StereoplotWidget(QWidget):

    window_closed = pyqtSignal()

    def __init__(self,
                 module_name,
                 canvas,
                 settings_name,
                 plugin_folder):

        super(StereoplotWidget, self).__init__()
        self.setAttribute(Qt.WA_DeleteOnClose, False)
        self.canvas = canvas
        self.module_name = module_name
        self.settings_name = settings_name
        self.plugin_folder = plugin_folder

        # settings stored for plugin

        settings = QSettings(self.settings_name, plugin_name)

        # stored setting values for plot style

        line_color = settings.value("StereoplotWidget/line_color", None)
        line_style = settings.value("StereoplotWidget/line_style", None)
        line_width = settings.value("StereoplotWidget/line_width", None)
        line_transp = settings.value("StereoplotWidget/line_transp", None)
        marker_color = settings.value("StereoplotWidget/marker_color", None)
        marker_style = settings.value("StereoplotWidget/marker_style", None)
        marker_size = settings.value("StereoplotWidget/marker_size", None)
        marker_transp = settings.value("StereoplotWidget/point_transp", None)

        self.stereonet_params = StereonetStyleParams(
            line_color,
            line_style,
            line_width,
            line_transp,
            marker_color,
            marker_style,
            marker_size,
            marker_transp
        )

        # stored setting values for figure export

        self.dExportParams = dict()
        self.dExportParams["expfig_width_inch"] = settings.value("StereoplotWidget/expfig_width_inch", "10")
        self.dExportParams["expfig_res_dpi"] = settings.value("StereoplotWidget/expfig_res_dpi", "200")
        self.dExportParams["expfig_font_size_pts"] = settings.value("StereoplotWidget/expfig_font_size_pts", "12")

        # reset all data definitions

        self.reset_layer_src_data()
        self.reset_text_src_data()

        # create gui window

        self.setup_gui()


    def reset_layer_src_data(self):

        self.source_layer_params = SourceLayerParams()

    def reset_text_src_data(self):

        self.source_text_params = SourceTextParams()

    def setup_gui(self):

        dialog_layout = QVBoxLayout()
        main_widget = QTabWidget()

        main_widget.addTab(self.setup_stereoplot_tab(), "Stereoplot")

        main_widget.addTab(self.setup_help_tab(), "Help")

        dialog_layout.addWidget(main_widget)
        self.setLayout(dialog_layout)

        self.adjustSize()
        self.setWindowTitle('{} - {}'.format(plugin_name, self.module_name))

    def setup_stereoplot_tab(self):

        widget = QWidget()

        layout = QVBoxLayout()

        self.stereonet = StereoNet()

        layout.addWidget(self.stereonet.fig.canvas)

        self.pshDefineInput = QPushButton(self.tr("Input data"))
        self.pshDefineInput.clicked.connect(self.define_input)
        layout.addWidget(self.pshDefineInput)

        self.pshDefineStyle = QPushButton(self.tr("Plot style"))
        self.pshDefineStyle.clicked.connect(self.define_style)
        layout.addWidget(self.pshDefineStyle)

        self.pshDefineStereoplot = QPushButton(self.tr("Plot data"))
        self.pshDefineStereoplot.clicked.connect(self.define_stereoplot)
        layout.addWidget(self.pshDefineStereoplot)

        self.pshClearStereoplot = QPushButton(self.tr("Clear stereonet"))
        self.pshClearStereoplot.clicked.connect(self.clear_stereoplot)
        layout.addWidget(self.pshClearStereoplot)

        self.pshSaveFigure = QPushButton(self.tr("Save figure"))
        self.pshSaveFigure.clicked.connect(self.save_figure)
        layout.addWidget(self.pshSaveFigure)

        widget.setLayout(layout)

        return widget

    def define_input(self):

        llyrLoadedPointLayers = loaded_point_layers()

        dialog = StereoplotInputDlg(llyrLoadedPointLayers)

        if dialog.exec_():

            if dialog.tabWdgt.currentIndex() == 0:  # layer as input

                self.reset_text_src_data()  # discard text-derived data

                # check that input layer is defined
                try:

                    lyrInputLayer = llyrLoadedPointLayers[dialog.cmbInputLayers.currentIndex() - 1]

                except:

                    warn_qgis(
                        self.module_name,
                        "Incorrect point layer choice"
                    )
                    return

                # extract input type definitions

                input_layer_params, err = extract_input_layer_params_from_dialog(dialog)

                if err:
                    warn_qgis(
                        self.module_name,
                        f"{str(err)}",
                    )
                    return

                if not valid_input_layer_params(input_layer_params):
                    warn_qgis(
                        self.module_name,
                        "Invalid/incomplete parameters"
                    )
                    return

                # get used field names in the point attribute table

                ltAttitudeFldNms = [input_layer_params["pln_azimuth_name_field"],
                                    input_layer_params["pln_dip_name_field"],
                                    input_layer_params["ln_azimuth_name_field"],
                                    input_layer_params["ln_dip_name_field"],
                                    input_layer_params["ln_movsen_name_field"],
                                    input_layer_params["ln_rake_name_field"],]

                # set input data presence and type

                layer_src_data = True
                src_layer = lyrInputLayer
                attitude_fld_names = ltAttitudeFldNms

                input_data_types, err = input_layer_data_characteristics(input_layer_params)

                if err:
                    warn_qgis(
                        self.module_name,
                        f"{str(err)}"
                    )
                    return

                self.source_layer_params = SourceLayerParams(
                    layer_src_data,
                    src_layer,
                    attitude_fld_names,
                    input_data_types
                )

            elif dialog.tabWdgt.currentIndex() == 1:  # text as input

                self.reset_layer_src_data()  # discard layer-derived data

                try:

                    data_type = dialog.cmbInputDataType.currentText()
                    plane_azim_type = dialog.cmbInputPlaneOrAzimType.currentText()
                    raw_values = dialog.plntxtedInputValues.toPlainText()

                except:

                    warn_qgis(
                        self.module_name,
                        "Incorrect text input"
                    )
                    return

                results, err = extract_orientations_from_text(
                    data_type,
                    plane_azim_type,
                    raw_values
                )

                if err:
                    warn_qgis(
                        self.module_name,
                        str(err)
                    )
                    return

                self.source_text_params = SourceTextParams(
                    True,
                    results
                )

            else:  # unknown choice

                self.reset_text_src_data()  # discard text-derived data
                self.reset_layer_src_data()  # discard layer-derived data

                warn_qgis(
                    self.module_name,
                    "Error with input data choice"
                )
                return

            info_qgis(
                self.module_name,
                f"Input data defined"
            )

    def define_style(self):

        dialog = StereonetStyleDlg(self.stereonet_params)

        if dialog.exec_():

            result, err = extract_user_defined_stereoplot_style(dialog)
            if err:
                warn_qgis(
                    self.module_name,
                    f"{e!r} "
                )
                return

            self.stereonet_params = result
            self.update_style_settings()

    def plot_dataset(
            self,
            structural_values,
            plot_setts):

        line_style = self.stereonet_params.line_style
        line_width = parse_marker_size(self.stereonet_params.line_width)
        line_color = fract_rgb_from_qcolor_name(self.stereonet_params.line_color)
        line_alpha = parse_transparency(self.stereonet_params.line_transp)

        marker_style = marker_styles[self.stereonet_params.marker_style]
        marker_size = parse_marker_size(self.stereonet_params.marker_size)
        marker_color = fract_rgb_from_qcolor_name(self.stereonet_params.marker_color)
        marker_alpha = parse_transparency(self.stereonet_params.marker_transp)

        if plot_setts["bPlotPlanes"]:

            assert plot_setts["tPlotPlanesFormat"] in ["great circles", "normal axes"]

            plane_data = filter_plane_data(structural_values)

            if not plane_data:

                warn_qgis(
                    self.module_name,
                    "No plane data"
                )

            else:

                for plane_dda in plane_data:

                    if plot_setts["tPlotPlanesFormat"] == "great circles":

                        p = aFol(*plane_dda)
                        self.stereonet.plane(
                            p,
                            linestyle=line_style,
                            linewidth=line_width,
                            color=line_color,
                            alpha=line_alpha
                        )

                    elif plot_setts["tPlotPlanesFormat"] == "normal axes":

                        line_rec = Plane(*plane_dda).normal_direction().d
                        l = aLin(*line_rec)
                        self.stereonet.line(
                            l,
                            marker=marker_style,
                            markersize=marker_size,
                            color=marker_color,
                            alpha=marker_alpha
                        )

                    else:

                        raise Exception("Not yet implemented")

        if plot_setts["bPlotPlaneswithRake"]:

            flt_slik_data = filter_fault_slikenline_data(structural_values)

            if not flt_slik_data:

                warn_qgis(
                    self.module_name,
                    "No fault-slickenline data"
                )

            else:

                for flt_slick in flt_slik_data:

                    dip_dir, dip_ang, lin_tr, lin_pl, sense = flt_slick

                    flt = aFault(dip_dir, dip_ang, lin_tr, lin_pl, sense)

                    if plot_setts["tPlotPlaneswithRakeFormat"] == "faults with skickenlines":

                        self.stereonet.fault(
                            flt,
                            linestyle=line_style,
                            linewidth=line_width,
                            color=line_color,
                            alpha=line_alpha,
                            marker_size=marker_size,
                            marker_color=marker_color,
                            marker_alpha=marker_alpha
                        )

                    elif plot_setts["tPlotPlaneswithRakeFormat"] == "T-L diagrams":

                        self.stereonet.hoeppner(
                            flt,
                            marker_size=marker_size,
                            marker_color=marker_color,
                            marker_alpha=marker_alpha,
                        )

                    else:

                        raise Exception("Not yet implemented")

        if plot_setts["bPlotAxes"]:

            line_data = filter_line_data(structural_values)

            if not line_data:

                warn_qgis(
                    self.module_name,
                    "No line data"
                )

            else:

                for line_rec in line_data:

                    if plot_setts["tPlotAxesFormat"] == "poles":

                        l = aLin(*line_rec)
                        self.stereonet.line(
                            l,
                            marker=marker_style,
                            markersize=marker_size,
                            color=marker_color,
                            alpha=marker_alpha
                        )

                    elif plot_setts["tPlotAxesFormat"] == "perpendicular planes":

                        az, pl = line_rec
                        plane_dda = Axis(az, pl).normal_plane().vals
                        p = aFol(*plane_dda)

                        self.stereonet.plane(
                            p,
                            linestyle=line_style,
                            linewidth=line_width,
                            color=line_color,
                            alpha=line_alpha
                        )

                    else:

                        raise Exception("Not yet implemented")

    def define_stereoplot(self):

        if not (self.source_layer_params.layer_src_data or self.source_text_params.text_src_data):

            warn_qgis(
                self.module_name,
                "No data to plot"
            )
            return

        if self.source_layer_params.layer_src_data and self.source_text_params.text_src_data:

            warn_qgis(
                self.module_name,
                "Both layer and text sources defined"
            )
            return

        if self.source_layer_params.layer_src_data:

            lGeoStructurData = read_qgis_pt_layer(
                self.source_layer_params.src_layer,
                self.source_layer_params.attitude_fld_names
            )

            lStructuralValues = parse_layer_data(
                self.source_layer_params.input_data_types,
                lGeoStructurData
            )

        elif self.source_text_params.text_src_data:

            lStructuralValues = self.source_text_params.geostructural_data

        else:

            warn_qgis(
                self.module_name,
                "Unknown data type source"
            )
            return

        dialog = PlotStereonetDialog()

        if dialog.exec_():

            dPlotSettings = dict(bPlotPlanes=dialog.chkPlanes.isChecked(),
                                 tPlotPlanesFormat=dialog.cmbPlanesType.currentText(),
                                 bPlotPlaneswithRake=dialog.chkPlaneswithRake.isChecked(),
                                 tPlotPlaneswithRakeFormat=dialog.cmbPlaneswithRakeType.currentText(),
                                 bPlotAxes=dialog.chkAxes.isChecked(),
                                 tPlotAxesFormat=dialog.cmbAxesType.currentText())

            self.plot_dataset(lStructuralValues, dPlotSettings)

    def clear_stereoplot(self):

        self.stereonet.cla()

    def save_figure(self):

        dialog = FigureExportDialog(
            plugin_name,
            self.dExportParams
        )

        if dialog.exec_():

            try:

                self.dExportParams["expfig_res_dpi"] = dialog.qleFigResolutionDpi.text()
                fig_resolution_dpi = int(self.dExportParams["expfig_res_dpi"])

            except:

                warn_qgis(
                    self.module_name,
                    "Error in figure resolution value"
                )
                return

            try:

                fig_outpath = str(dialog.qleFigureOutPath.text())

            except:

                warn_qgis(
                    self.module_name,
                    "Error in figure output path"
                )
                return

        else:

            warn_qgis(
                self.module_name,
                "No export figure defined"
            )
            return

        try:

            self.stereonet.fig.savefig(str(fig_outpath), dpi=fig_resolution_dpi)
            success, msg = True, "Image saved"

        except Exception as e:

            success, msg = False, f"Exception with image saving: {e!r}"

        if success:

            ok_qgis(
                self.module_name,
                msg
            )

        else:

            warn_qgis(
                self.module_name,
                msg
            )

    def update_style_settings(self):

        settings = QSettings(self.settings_name, plugin_name)
        settings.setValue("StereoplotWidget/line_color", self.stereonet_params.line_color)
        settings.setValue("StereoplotWidget/line_style", self.stereonet_params.line_style)
        settings.setValue("StereoplotWidget/line_width", self.stereonet_params.line_width)
        settings.setValue("StereoplotWidget/line_transp", self.stereonet_params.line_transp)

        settings.setValue("StereoplotWidget/marker_color", self.stereonet_params.marker_color)
        settings.setValue("StereoplotWidget/marker_style", self.stereonet_params.marker_style)
        settings.setValue("StereoplotWidget/marker_size", self.stereonet_params.marker_size)
        settings.setValue("StereoplotWidget/point_transp", self.stereonet_params.marker_transp)

    def closeEvent(self, event):

        # todo: define if this function it's reached or not, and how to change in negative case

        settings = QSettings(self.settings_name, plugin_name)
        settings.setValue("StereoplotWidget/size", self.size())
        settings.setValue("StereoplotWidget/position", self.pos())

        self.window_closed.emit()

    def setup_help_tab(self):

        qwdtHelp = QWidget()
        qlytHelp = QVBoxLayout()

        # Help section

        qtbrHelp = QTextBrowser(qwdtHelp)
        url_path = f"file:///{self.plugin_folder}/help/help_stereonet.html"
        qtbrHelp.setSource(QUrl(url_path))
        qtbrHelp.setSearchPaths([f'{self.plugin_folder}/help/images/stereonet'])
        qlytHelp.addWidget(qtbrHelp)

        qwdtHelp.setLayout(qlytHelp)

        return qwdtHelp





