# -*- coding: utf-8 -*-
#
#   Copyright (C) 2019 Oslandia <infos@oslandia.com>
#
#   This file is a piece of free software; you can redistribute it and/or
#   modify it under the terms of the GNU Library General Public
#   License as published by the Free Software Foundation; either
#   version 2 of the License, or (at your option) any later version.
#
#   This library is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#   Library General Public License for more details.
#   You should have received a copy of the GNU Library General Public
#   License along with this library; if not, see <http://www.gnu.org/licenses/>
#

import os

from PyQt5.QtCore import Qt
from qgis.PyQt.QtWidgets import QAction, QVBoxLayout, QWidget, QApplication
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtCore import QSize, pyqtSignal
from qgis.core import QgsProject, QgsFeatureRequest

from .config_create_dialog import ConfigCreateDialog
from .qgeologis.plot_view import PlotView
from .qgeologis.depth_plot_view import DepthPlotView
from .qgeologis.stratigraphy import StratigraphyItem
from .qgeologis.imagery_data import ImageryDataItem
from .qgeologis.data_interface import LayerData, IntervalData, FeatureData
from .data_selector import DataSelector
from .data_stacker import DataStacker
from .config import LayerConfig
from .qgeologis.common import (
    ORIENTATION_HORIZONTAL,
    ORIENTATION_UPWARD,
    ORIENTATION_DOWNWARD,
    ORIENTATION_LEFT_TO_RIGHT,
)


def load_plots(obj, feature, config, config_list):
    """ Display plots

    :param obj: WellLogViewWrapper or TimeSeriesWrapper
    :type obj: WellLogViewWrapper or TimeSeriesWrapper
    :param feature: feature to display
    :type feature: QgsFeature
    :param config: configuration layer
    :type config: LayerConfig
    :param config_list: list of plot to display
    :type config_list: list of PlotItem
    :return: (x_min, x_max)
    :rtype: tuple
    """
    feature_id = feature[config["id_column"]]
    feature_name = feature[config["name_column"]]
    feature_elevation = (
        feature[config["ground_elevation"]]
        if isinstance(obj, WellLogViewWrapper) and config["ground_elevation"]
        else False
    )

    min_x = []
    max_x = []
    for cfg in config_list:
        layerid = cfg.get_layerid()
        data_l = QgsProject.instance().mapLayers()[layerid]
        filter_expr = "{}='{}'".format(cfg["feature_ref_column"], feature_id)

        nodata = cfg.get("nodata", 0.0)
        if nodata == "remove":
            nodata = None

        # Handle stacked data
        stacked_datas = {}
        stacked_data_names = []
        if len(cfg["stacked"]) > 0:
            for lyr_id in cfg["stacked"]:
                if "#" in lyr_id:
                    cat = lyr_id.split("#")[1]
                    lyr_id = lyr_id.split("#")[0]

                stacked_data_l = QgsProject.instance().mapLayers()[lyr_id]
                _, stacked_config = config.get_layer_config_from_layer_id(lyr_id)
                symbology = stacked_config["symbology"] if "symbology" in stacked_config else None
                symbology_type = (
                    stacked_config["symbology_type"] if "symbology" in stacked_config else None
                )
                title = stacked_config["name"]

                filter_stacked = filter_expr
                if stacked_config["feature_filter_type"] == "unique_data_from_values":
                    filter_stacked = filter_stacked + "and {} = '{}'".format(
                        stacked_config["feature_filter_column"], cat
                    )
                    uom = cfg.get_uom()
                    lyr_id = lyr_id + "#" + cat
                    title = cat
                else:
                    uom = stacked_config["uom"]
                if stacked_config["type"] == "instantaneous":
                    stacked_data = LayerData(
                        stacked_data_l,
                        feature_elevation,
                        (stacked_config["event_column"]),
                        stacked_config["value_column"],
                        stacked_config["uncertainty_column"],
                        filter_expression=filter_stacked,
                        uom=uom,
                        nodata_value=nodata,
                        symbology=symbology,
                        symbology_type=symbology_type,
                    )
                elif stacked_config["type"] == "cumulative":
                    stacked_data = IntervalData(
                        stacked_data_l,
                        feature_elevation,
                        [stacked_config["min_event_column"], stacked_config["max_event_column"]],
                        stacked_config["value_column"],
                        stacked_config["uncertainty_column"],
                        filter_expression=filter_stacked,
                        uom=uom,
                        nodata_value=nodata,
                        symbology=symbology,
                        symbology_type=symbology_type,
                    )
                elif stacked_config["type"] == "continuous":
                    req = QgsFeatureRequest()
                    req.setFilterExpression(filter_stacked)
                    fids = [f.id() for f in data_l.getFeatures(req)]
                    stacked_data = FeatureData(
                        stacked_data_l,
                        feature_elevation,
                        stacked_config["values_column"],
                        feature_ids=fids,
                        uncertainty_fieldname=stacked_config["uncertainty_column"]
                        if stacked_config["uncertainty_column"] != ""
                        else None,
                        uom=uom,
                        x_start_fieldname=stacked_config["start_measure_column"],
                        x_delta_fieldname=stacked_config["interval_column"],
                        symbology=symbology,
                        symbology_type=symbology_type,
                    )
                stacked_datas[lyr_id] = stacked_data
                stacked_data_names.append(title)

        if cfg.get("feature_filter_type") == "unique_data_from_values":
            if len(cfg.get("displayed_cat")) > 0:
                for cat in cfg.get("displayed_cat"):
                    title = cat
                    filter_with_unique = filter_expr + "and {} = '{}'".format(
                        cfg.get("feature_filter_column"), cat
                    )
                    uom = cfg.get_uom()

                    if cfg["type"] == "instantaneous":
                        data = LayerData(
                            data_l,
                            feature_elevation,
                            (cfg["event_column"]),
                            cfg["value_column"],
                            cfg["uncertainty_column"],
                            filter_expression=filter_with_unique,
                            uom=uom,
                            nodata_value=nodata,
                        )
                        uom = data.get_uom()

                    elif cfg["type"] == "cumulative":
                        data = IntervalData(
                            data_l,
                            feature_elevation,
                            [cfg["min_event_column"], cfg["max_event_column"]],
                            cfg["value_column"],
                            cfg["uncertainty_column"],
                            filter_expression=filter_with_unique,
                            uom=uom,
                            nodata_value=nodata,
                        )
                        uom = data.get_uom()

                    if data.get_x_min() is not None:
                        min_x.append(data.get_x_min())
                        max_x.append(data.get_x_max())
                        obj.add_data_cell(
                            data,
                            title,
                            station_name=feature_name,
                            config=cfg,
                            stacked_data=stacked_datas,
                            stacked_data_names=stacked_data_names,
                        )
        else:
            title = cfg["name"]

            uom = cfg.get_uom()

            if cfg["type"] == "instantaneous":

                data = LayerData(
                    data_l,
                    feature_elevation,
                    cfg["event_column"],
                    cfg["value_column"],
                    cfg["uncertainty_column"],
                    filter_expression=filter_expr,
                    uom=uom,
                    nodata_value=nodata,
                )
                uom = data.get_uom()

            elif cfg["type"] == "cumulative":
                uom = cfg.get_uom()
                data = IntervalData(
                    data_l,
                    feature_elevation,
                    [cfg["min_event_column"], cfg["max_event_column"]],
                    cfg["value_column"],
                    cfg["uncertainty_column"],
                    filter_expression=filter_expr,
                    uom=uom,
                    nodata_value=nodata,
                )

            elif cfg["type"] == "continuous":
                req = QgsFeatureRequest()
                req.setFilterExpression(filter_expr)
                fids = [f.id() for f in data_l.getFeatures(req)]

                data = FeatureData(
                    data_l,
                    feature_elevation,
                    cfg["values_column"],
                    feature_ids=fids,
                    uncertainty_fieldname=cfg["uncertainty_column"]
                    if cfg["uncertainty_column"] != ""
                    else None,
                    uom=uom,
                    x_start_fieldname=cfg["start_measure_column"],
                    x_delta_fieldname=cfg["interval_column"],
                )

            if data.get_x_min() is not None:
                min_x.append(data.get_x_min())
                max_x.append(data.get_x_max())
                obj.add_data_cell(
                    data,
                    title,
                    station_name=feature_name,
                    config=cfg,
                    stacked_data=stacked_datas,
                    stacked_data_names=stacked_data_names,
                )

    if not min_x:
        return None, None
    return (min(min_x), max(max_x))


class WellLogViewWrapper(DepthPlotView):
    def __init__(self, config, iface):
        """ View wrapper for well log

        :param config: configuration layer
        :type config: LayerConfig
        :type iface: qgis interface
        :type iface: QgisInterface
        """
        self.__x_orientation = (
            ORIENTATION_UPWARD if config["ground_elevation"] else ORIENTATION_DOWNWARD
        )
        self.__y_orientation = ORIENTATION_LEFT_TO_RIGHT
        DepthPlotView.__init__(self, x_orientation=self.__x_orientation)
        self.__iface = iface
        self.__features = []
        self.__config = config

        image_dir = os.path.join(os.path.dirname(__file__), "qgeologis", "img")
        self.__action_add_configuration = QAction(
            QIcon(os.path.join(image_dir, "new_plot.svg")),
            self.tr("Add a new column configuration"),
            self._toolbar,
        )
        self.__action_add_configuration.triggered.connect(self.on_add_configuration)
        self._toolbar.addAction(self.__action_add_configuration)
        self.__action_remove_configuration = QAction(
            QIcon(os.path.join(image_dir, "remove_plot.svg")),
            self.tr("Remove a column from configuration"),
            self._toolbar,
        )
        self.__action_remove_configuration.triggered.connect(self.on_remove_configuration)
        self._toolbar.addAction(self.__action_remove_configuration)

        self._PlotView__action_move_cell_before.setText(self.tr("Move left"))
        self._PlotView__action_move_cell_after.setText(self.tr("Move right"))
        self._PlotView__action_edit_style.setText(self.tr("Edit style"))
        self._PlotView__action_add_cell.setText(self.tr("Add a data cell"))
        self._PlotView__action_remove_cell.setText(self.tr("Remove a data cell"))

    def on_add_configuration(self):
        """ Open a dialog to configure a new plot"""
        dialog = ConfigCreateDialog(self)
        if dialog.exec_():
            config_type, plot_config = dialog.config()
            self.__config.add_plot_config(config_type, plot_config)

        self.update_view()

    def on_remove_configuration(self):
        """ Open a dialog to remove a configured column """
        s = DataSelector(
            self,
            self.__features,
            self.__config.get_stratigraphy_plots()
            + self.__config.get_log_plots()
            + self.__config.get_imageries()
            + self.__config.get_stratigraphy_plots(False)
            + self.__config.get_log_plots(False)
            + self.__config.get_imageries(False),
            self.__config,
            removeMode=True,
        )
        if s.exec_():
            if s.get_config_removed():
                self.on_config_remove.emit()
                return
            for item in s.items():
                self.__config.remove_plot_config(item)

        self.update_view()

    def set_features(self, features):
        """ Set the features to display

        :param features: features to display
        :type features: list of QgsFeature
        """
        self.__features = features
        self.update_view()

    def update_view(self):
        """ Update view after clearing all cells """
        self.clear_data_cells()

        min_x = []
        max_x = []
        for feature in self.__features:
            fmin_x, fmax_x = self.__load_feature(feature)
            if fmin_x is not None:
                min_x.append(fmin_x)
                max_x.append(fmax_x)

        if min_x:
            if self.__config["ground_elevation"]:
                self.set_x_range(min(min_x), max(max_x), True)
            else:
                self.set_x_range(-200, 0, False)

    def __load_feature(self, feature):
        """ Load columns for a feature

        :param feature: feature
        :type feature: QgsFeature
        :return: (xmin, xmax)
        :rtype: tuple
        """
        feature_name = feature[self.__config["name_column"]]
        if self.__config["ground_elevation"]:
            feature_elevation = feature[self.__config["ground_elevation"]]
        else:
            feature_elevation = False

        min_x = []
        max_x = []
        # load stratigraphy
        for cfg in self.__config.get_stratigraphy_plots():
            layer = QgsProject.instance().mapLayers()[cfg.get_layerid()]
            f = "{}='{}'".format(cfg["feature_ref_column"], feature[self.__config["id_column"]])
            if len([ft for ft in layer.getFeatures(QgsFeatureRequest().setFilterExpression(f))]):
                xmin, xmax = self.add_stratigraphy(
                    layer,
                    feature_elevation,
                    f,
                    {
                        "depth_from_column": cfg["depth_from_column"],
                        "depth_to_column": cfg["depth_to_column"],
                        "formation_code_column": cfg["formation_code_column"],
                        "rock_code_column": cfg["rock_code_column"],
                        "formation_description_column": cfg.get("formation_description_column"),
                        "rock_description_column": cfg.get("rock_description_column"),
                    },
                    cfg.get("name", "Stratigraphy"),
                    cfg.get_style_file(),
                    config=cfg,
                    station_name=feature_name,
                )
                if xmin is not None:
                    min_x.append(xmin)
                    max_x.append(xmax)

        # load log measures
        xmin, xmax = load_plots(self, feature, self.__config, self.__config.get_log_plots())
        if xmin is not None:
            min_x.append(xmin)
            max_x.append(xmax)

        # load imagery
        feature_id = feature[self.__config["id_column"]]
        for cfg in self.__config.get_imageries():
            layer = QgsProject.instance().mapLayersByName(cfg["name"])[0]
            xmin, xmax = self.add_imagery_from_db(
                cfg, feature_id, layer, feature_elevation, feature_name
            )
            if xmin is not None:
                min_x.append(xmin)
                max_x.append(xmax)

        if not min_x:
            return None, None
        return min(min_x), max(max_x)

    def on_add_cell(self):
        """ Open a dialog to select a configured columns to display"""
        if not self.__features and self.__iface:
            self.__iface.messageBar().pushWarning(
                "QGeoloGIS", self.tr("Impossible to add plot without selecting a feature")
            )
            return

        if self.selected_cell() is None:
            s = DataSelector(
                self,
                self.__features,
                self.__config.get_stratigraphy_plots(False)
                + self.__config.get_log_plots(False)
                + self.__config.get_imageries(False),
                self.__config,
            )
            if s.exec_():
                for item in s.items():
                    self.__config.display_plot(item)
                self.update_view()
        else:
            item, legend, _ = self.selected_cell()
            s = DataStacker(
                self,
                self.__features,
                self.__config.get_layer_config_from_layer_id(item.layer().id()),
                self.__config,
            )
            if s.exec_():
                self.__config.get_layer_config_from_layer_id(item.layer().id())[1][
                    "stacked"
                ] = s.stacked_items()
                self.update_view()

    def add_imagery_from_db(self, cfg, feature_id, layer, feature_elevation, station_name=""):
        """ Append imagery

        :param cfg: imagery dict
        :type cfg: dict
        :param feature_id: feature id
        :type feature_id: int
        :param station_name: station name
        :type station_name: str
        :return: x_min xmax
        :rtype: tuple
        """
        if cfg.get("provider", "postgres_bytea") != "postgres_bytea":
            raise self.tr("Access method not implemented !")

        import psycopg2
        import tempfile

        conn = psycopg2.connect(cfg["conn"])
        cur = conn.cursor()
        cur.execute(
            "select {depth_from}, {depth_to}, {data}, {format} \
             from {schema}.{table} where {ref_column}=%s".format(
                depth_from=cfg.get("depth_from_column", "depth_from"),
                depth_to=cfg.get("depth_to_column", "depth_to"),
                data=cfg.get("image_data_column", "image_data"),
                format=cfg.get("image_data_column", "image_format"),
                schema=cfg["schema"],
                table=cfg["table"],
                ref_column=cfg["feature_ref_column"],
            ),
            (feature_id,),
        )
        r = cur.fetchone()
        if r is None:
            return None, None

        depth_from, depth_to = float(r[0]), float(r[1])
        image_data = r[2]
        image_format = r[3]
        f = tempfile.NamedTemporaryFile(mode="wb", suffix=image_format.lower())
        image_filename = f.name
        f.close()
        with open(image_filename, "wb") as fo:
            fo.write(image_data)
        return self.add_imagery(
            image_filename,
            layer,
            feature_elevation,
            cfg["name"],
            cfg["plot_size"],
            depth_from,
            depth_to,
            station_name,
        )

    def _post_remove(self, item, legend):
        """ Modify configuration when a column is hidden"""
        self.__config.hide_plot(item, legend)

    def _edit_plot(self, item, legend):
        self.__config.edit_plot(item, legend)
        self.update_view()

    def zoom(self, dt, relative_pos, item, legend):
        min_y, max_y = self.__config.zoom(dt, relative_pos, item)
        if min_y and max_y:
            legend.set_scale(min_y, max_y)
            legend.update()
            item.data_window().setY(min_y)
            item.data_window().setHeight(max_y - min_y)
            item.update()

    def pan(self, translation_y, item, legend):
        min_y, max_y = self.__config.pan(translation_y, item)
        if min_y and max_y:
            legend.set_scale(min_y, max_y)
            legend.update()
            item.data_window().setY(min_y)
            item.data_window().setHeight(max_y - min_y)
            item.update()


class TimeSeriesWrapper(PlotView):
    def __init__(self, config, iface):
        """ View wrapper for time series

        :param config: configuration layer
        :type config: LayerConfig
        :type iface: qgis interface
        :type iface: QgisInterface
        """
        PlotView.__init__(self, orientation=ORIENTATION_HORIZONTAL)
        self.__iface = iface
        self.__config = config
        self.__features = []

        image_dir = os.path.join(os.path.dirname(__file__), "qgeologis", "img")
        self.__action_add_configuration = QAction(
            QIcon(os.path.join(image_dir, "new_plot.svg")),
            self.tr("Add a new column configuration"),
            self._toolbar,
        )
        self.__action_add_configuration.triggered.connect(self.on_add_configuration)
        self._toolbar.addAction(self.__action_add_configuration)
        self.__action_remove_configuration = QAction(
            QIcon(os.path.join(image_dir, "remove_plot.svg")),
            self.tr("Remove a column from configuration"),
            self._toolbar,
        )
        self.__action_remove_configuration.triggered.connect(self.on_remove_configuration)
        self._toolbar.addAction(self.__action_remove_configuration)

        self._PlotView__action_move_cell_before.setText(self.tr("Move up"))
        self._PlotView__action_move_cell_after.setText(self.tr("Move down"))
        self._PlotView__action_edit_style.setText(self.tr("Edit style"))
        self._PlotView__action_add_cell.setText(self.tr("Add a data cell"))
        self._PlotView__action_remove_cell.setText(self.tr("Remove a data cell"))

    def on_add_configuration(self):
        """ Open a dialog to configure a new plot"""
        dialog = ConfigCreateDialog(self, timeseries=True)
        if dialog.exec_():
            config_type, plot_config = dialog.config()
            self.__config.add_plot_config(config_type, plot_config)

        self.update_view()

    def set_features(self, features):
        """ Set the features to display

        :param features: features to display
        :type features: list of QgsFeature
        """
        self.__features = features
        self.update_view()

    def update_view(self):
        """ Update view after clearing all cells """
        self.clear_data_cells()

        min_x = []
        max_x = []
        for feature in self.__features:
            fmin_x, fmax_x = load_plots(
                self, feature, self.__config, self.__config.get_timeseries()
            )
            if fmin_x is not None:
                min_x.append(fmin_x)
                max_x.append(fmax_x)

        if min_x:
            self.set_x_range(min(min_x), max(max_x))

    def on_add_cell(self):
        """ Open a dialog to select a configured columns to display"""
        if not self.__features and self.__iface:
            self.__iface.messageBar().pushWarning(
                "QGeoloGIS", self.tr("Impossible to add plot without selecting a feature")
            )
            return

        if self.selected_cell() is None:
            s = DataSelector(
                self, self.__features, self.__config.get_timeseries(False), self.__config
            )
            if s.exec_():
                for item in s.items():
                    self.__config.display_plot(item)
                self.update_view()
                # Use resizeEvent to center the scene on the scale
                self.centerSceneOnScale.emit()
        else:
            item, legend, _ = self.selected_cell()
            s = DataStacker(
                self,
                self.__features,
                self.__config.get_layer_config_from_layer_id(item.layer().id()),
                self.__config,
            )
            if s.exec_():
                self.update_view()

    def on_remove_configuration(self):
        """ Open a dialog to remove a configured column """
        s = DataSelector(
            self,
            self.__features,
            self.__config.get_timeseries() + self.__config.get_timeseries(False),
            self.__config,
            removeMode=True,
        )
        if s.exec_():
            if s.get_config_removed():
                self.on_config_remove.emit()
                return
            for item in s.items():
                self.__config.remove_plot_config(item)

        self.update_view()

    def _post_remove(self, item, legend):
        """ Modify configuration when a column is hidden"""
        self.__config.hide_plot(item, legend)

    def _edit_plot(self, item, legend):
        self.__config.edit_plot(item, legend)
        self.update_view()

    def zoom(self, dt, relative_pos, item, legend):
        min_y, max_y = self.__config.zoom(dt, relative_pos, item)
        if min_y and max_y:
            legend.set_scale(min_y, max_y)
            legend.update()
            item.data_window().setY(min_y)
            item.data_window().setHeight(max_y - min_y)
            item.update()

    def pan(self, translation_y, item, legend):
        min_y, max_y = self.__config.pan(translation_y, item)
        if min_y and max_y:
            legend.set_scale(min_y, max_y)
            legend.update()
            item.data_window().setY(min_y)
            item.data_window().setHeight(max_y - min_y)
            item.update()


class MainDialog(QWidget):

    config_removed = pyqtSignal(str)

    def __init__(self, parent, plot_type, config, layer, iface):
        """Create a plot dialog that updates when a layer selection updates.

        Parameters
        ----------
        parent: QObject
          Qt parent object
        plot_type: Literal["logs", "timeseries"]
          Type of plot, either "logs" or "timeseries"
        config: dict
          Layer configuration
        layer: QgsVectorLayer
          Main layer
        iface: QgisInterface
          QGIS interface class
        """

        super().__init__(parent)
        self.setWindowTitle("{} {}".format(layer.name(), plot_type))
        self.setMinimumSize(QSize(600, 400))

        self.__layer = layer
        self.__config = LayerConfig(config, layer.id())
        self.__iface = iface

        if plot_type == "logs":
            self.__view = WellLogViewWrapper(self.__config, self.__iface)
        elif plot_type == "timeseries":
            self.__view = TimeSeriesWrapper(self.__config, self.__iface)
        else:
            raise RuntimeError(self.tr("Invalid plot_type") + "{}".format(plot_type))
        self.__view.styles_updated.connect(self.on_styles_updated)
        self.__view.on_config_remove.connect(self.config_remove)

        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self.__view)
        self.setLayout(layout)

        self.__layer.selectionChanged.connect(self.__update_selected_features)

        self.__update_selected_features()

    def __update_selected_features(self):
        """ Update features in current view"""
        if not self.__layer.selectedFeatureCount():
            return

        QApplication.setOverrideCursor(Qt.WaitCursor)
        self.__view.set_features(self.__layer.selectedFeatures())
        QApplication.restoreOverrideCursor()

    def on_styles_updated(self):
        """ Update symbology"""
        styles = self.__view.styles()

        for cfg in self.__config.get_vertical_plots() + self.__config.get_timeseries():
            for layer_id, (style, renderer_type) in styles.items():
                if cfg.get("source") == layer_id:
                    cfg.set_symbology(style, renderer_type)
                    break

    def config_remove(self):
        """ Triggered on configuration removal"""
        self.config_removed.emit(self.__layer.id())
