# -*- coding: utf-8 -*-
#
#   Copyright (C) 2018 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/>.
#

"""
Set of classes used to draw both "vertical" and "horizontal" plots.

The "X" axis in the code below refers to the dimension against which
the "Y" dimension is observed: e.g. time for timeseries or depth for well logs.

The X axis may also be named "domain axis".
The Y axis may also be named "value axis".
"""

from PyQt5.QtCore import Qt, pyqtSignal, QSizeF, QRectF

from PyQt5.QtWidgets import (
    QGraphicsView,
    QGraphicsScene,
    QWidget,
    QAction,
    QToolBar,
    QLabel,
    QStatusBar,
    QHBoxLayout,
    QVBoxLayout,
)

from PyQt5.QtGui import QIcon

from .common import (
    POINT_RENDERER,
    POLYGON_RENDERER,
    ORIENTATION_UPWARD,
    ORIENTATION_DOWNWARD,
    ORIENTATION_LEFT_TO_RIGHT,
    ORIENTATION_HORIZONTAL,
    ORIENTATION_VERTICAL,
)
from .plot_item import PlotItem
from .time_scale import TimeScaleItem
from .z_scale import ZScaleItem
from .legend_item import LegendItem
from .units import unitConversionFactor
from .data_interface import IntervalData
from .symbol_legend import SymbolLegend

import os


class Axis:
    """Class designed to abstract X/Y axis calculations

    The X axis is refered to as the "domain" axis.
    The Y axis is refered to as the "value" axis.
    """

    def __init__(self, orientation):
        self._orientation = orientation

    def domain_pos(self, obj):
        """Returns the position on the domain axis of an object"""
        if self._orientation == ORIENTATION_HORIZONTAL:
            return obj.x()
        return obj.y()

    def value_pos(self, obj):
        """Returns the position on the value axis of an object"""
        if self._orientation == ORIENTATION_HORIZONTAL:
            return obj.y()
        return obj.x()

    def domain_size(self, obj):
        """Returns the size of the domain axis of a QSize/QRect/..."""
        if self._orientation == ORIENTATION_HORIZONTAL:
            return obj.width()
        return obj.height()

    def value_size(self, obj):
        """Returns the size of the value axis of a QSize/QRect/..."""
        if self._orientation == ORIENTATION_HORIZONTAL:
            return obj.height()
        return obj.width()

    def domain_set_size(self, obj, size):
        """Sets the size of the domain axis of a QSize/QRect/..."""
        if self._orientation == ORIENTATION_HORIZONTAL:
            return obj.setWidth(size)
        return obj.setHeight(size)

    def value_set_size(self, obj, size):
        """Sets the size of the value axis of a QSize/QRect/..."""
        if self._orientation == ORIENTATION_HORIZONTAL:
            return obj.setHeight(size)
        return obj.setWidth(size)


class PlotGraphicsView(QGraphicsView):
    def __init__(self, scene, orientation, parent=None):
        """ View handles user interaction, like resize, mouse etc

        :param scene: MyScene, heritated from QGraphicScene
        :type scene: MyScene
        :param orientation: orientation of the plot windows
        :type orientation: bool
        :param parent: parent
        :type parent: QObjet
        """
        QGraphicsView.__init__(self, scene, parent)

        self.__allow_mouse_translation = True
        self.__pos = None
        self.__translation_orig = None
        self.__translation_min_x = None
        self.__translation_max_x = None
        self.__translation_y = 0
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.__orientation = orientation
        if self.__orientation == ORIENTATION_VERTICAL:
            self.setAlignment(Qt.AlignLeft | Qt.AlignTop)
        if self.__orientation == ORIENTATION_HORIZONTAL:
            self.setAlignment(Qt.AlignLeft | Qt.AlignBottom)

        self.__altered_function = False

        self.setMouseTracking(True)

        self.__axis = Axis(self.__orientation)
        # if self.__orientation  == ORIENTATION_HORIZONTAL and self.height() < self.scene().sceneRect().height() :
        #     self.centerOn(self.scene().sceneRect().width(), self.scene().sceneRect().height() - self.height()/2 -1)

    def resizeEvent(self, event):
        """ Triggered on windows resize, recenter view

        :param event: QEvent
        :type event: QEvent
        """
        QGraphicsView.resizeEvent(self, event)
        # by default, the rect is centered on 0,0,
        # we prefer to have 0,0 in the upper left corner
        rect = self.scene().sceneRect()
        self.__axis.domain_set_size(rect, self.__axis.domain_size(event.size()))
        self.scene().setSceneRect(rect)
        # for time series, if logs scene is too high for windows, focus on the bottom to see the scale
        if self.__orientation == ORIENTATION_HORIZONTAL and self.height() < rect.height():
            self.centerOn(rect.width(), rect.height() - self.height() / 2 - 1)

        # handle column when the windows is resized
        if (
            self.__orientation == ORIENTATION_HORIZONTAL
            and self.height()
            >= self.scene().sceneRect().height() - self.parentWidget().y_axis_pos()
        ):
            self.parentWidget()._move_y = (
                self.height()
                - (self.scene().sceneRect().height() - self.parentWidget().y_axis_pos())
                - 5
            )
            self.parentWidget()._update_cell_sizes()
        elif (
            self.__orientation == ORIENTATION_VERTICAL
            and self.width() >= self.scene().sceneRect().width() + self.parentWidget().y_axis_pos()
        ):
            self.parentWidget()._move_y = -(
                self.width()
                - (self.scene().sceneRect().width() + self.parentWidget().y_axis_pos())
                - 5
            )
            self.parentWidget()._update_cell_sizes()

    def keyPressEvent(self, event):
        if event.modifiers() & Qt.ControlModifier:
            self.__altered_function = True

    def keyReleaseEvent(self, event):
        self.__altered_function = False

    def wheelEvent(self, event):
        """ Triggered on mouse wheel use, zoom

        :param event: QEvent
        :type event: QEvent
        """
        delta = -event.angleDelta().y() / 100.0
        if delta >= 0:
            dt = 1.1
        else:
            dt = 0.9

        if self.__altered_function:
            self.parentWidget().on_wheel(dt, self.__pos)
        else:
            min_x = self.parentWidget()._min_x
            max_x = self.parentWidget()._max_x
            w = max_x - min_x
            nw = w * dt
            dx = (
                self.__axis.domain_pos(event)
                / self.__axis.domain_size(self.scene().sceneRect())
                * (w - nw)
            )
            self.parentWidget()._min_x += dx
            self.parentWidget()._max_x = self.parentWidget()._min_x + nw
            self.parentWidget()._update_cell_sizes()

    def mouseMoveEvent(self, event):
        """ Triggered on mouse move, actualize translation attributes

        :param event: QEvent
        :type event: QEvent
        :return: QEvent
        :rtype: QEvent
        """
        self.__pos = self.mapToScene(event.pos())
        if not self.__allow_mouse_translation:
            return QGraphicsView.mouseMoveEvent(self, event)

        if self.__translation_orig is not None:
            delta = self.__translation_orig - self.__pos
            delta_x = (
                self.__axis.domain_pos(delta)
                / self.__axis.domain_size(self.scene().sceneRect())
                * (self.parentWidget()._max_x - self.parentWidget()._min_x)
            )
            item = self.parentWidget().item_type_on_cursor(self.__translation_orig)
            if item is not None and not isinstance(item, LegendItem):
                if (
                    self.parentWidget().x_orientation() == ORIENTATION_LEFT_TO_RIGHT
                    and self.parentWidget().y_orientation() == ORIENTATION_UPWARD
                ):
                    delta_x = -delta_x
                self.parentWidget()._min_x = self.__translation_min_x - delta_x
                self.parentWidget()._max_x = self.__translation_max_x - delta_x
                if isinstance(item, PlotItem):
                    self.yMove(delta, plot_trans=True)
                    self.parentWidget().on_pan(self.__translation_orig)
                self.parentWidget()._update_cell_sizes()
            elif isinstance(item, LegendItem):
                self.yMove(delta)
                self.parentWidget()._update_cell_sizes()

        return QGraphicsView.mouseMoveEvent(self, event)

    def yMove(self, delta, plot_trans=False):
        """ Process y translation through PlotView._move_y attribute

        :param delta: x,y delta
        :type delta: tuple
        """
        delta_y = self.__axis.value_pos(delta)

        # Time series axis
        if self.__orientation == ORIENTATION_HORIZONTAL:
            height = self.height()
            rect_height = self.scene().sceneRect().height()
            y_axis_pos = self.parentWidget().y_axis_pos()
            # Y axis moves if the logs height exceed the windows
            if (
                height < rect_height
                and (height <= rect_height - y_axis_pos or delta_y - self.__translation_y > 0)
                or plot_trans
            ):
                # Watch if translation doesn't exceed logs height
                if (
                    height - rect_height + y_axis_pos - 5 > delta_y - self.__translation_y
                    and delta_y - self.__translation_y < 0
                ) and not plot_trans:
                    self.parentWidget()._move_y = height - rect_height + y_axis_pos - 5
                    self.__translation_y = (
                        height - rect_height + y_axis_pos - 5 + self.__translation_y
                    )
                else:
                    self.parentWidget()._move_y = delta_y - self.__translation_y
                    self.__translation_y = delta_y

        elif self.__orientation == ORIENTATION_VERTICAL:
            width = self.width()
            rect_width = self.scene().sceneRect().width()
            y_axis_pos = self.parentWidget().y_axis_pos()
            # Y axis moves if the logs width exceed the windows
            if (
                width < rect_width
                and (width <= rect_width + y_axis_pos or delta_y < 0)
                or plot_trans
            ):
                # Watch if translation doesn't exceed logs width
                if (
                    -width + rect_width + y_axis_pos + 5 < delta_y - self.__translation_y
                ) and not plot_trans:
                    self.parentWidget()._move_y = -width + rect_width + y_axis_pos + 5
                else:
                    self.parentWidget()._move_y = delta_y - self.__translation_y
                self.__translation_y = delta_y

    def mousePressEvent(self, event):
        """ Triggered on click press, init translation attributes

        :param event: QEvent
        :type event: QEvent
        :return: QEvent
        :rtype: QEvent
        """
        pos = self.mapToScene(event.pos())
        self.__translation_orig = None
        if event.buttons() == Qt.LeftButton:
            self.__translation_orig = pos
            self.__translation_y = 0
            self.__translation_min_x = self.parentWidget()._min_x
            self.__translation_max_x = self.parentWidget()._max_x
        return QGraphicsView.mousePressEvent(self, event)

    def mouseReleaseEvent(self, event):
        """ Triggered on click release, do select or reset translation attributes

        :param event: QEvent
        :type event: QEvent
        :return: QEvent
        :rtype: QEvent
        """
        pos = self.mapToScene(event.pos())
        if pos == self.__translation_orig:
            self.parentWidget().select_cell_at(pos)
        self.__translation_orig = None
        self.__translation_y = 0

        return QGraphicsView.mouseReleaseEvent(self, event)


class MyScene(QGraphicsScene):

    plot_edited = pyqtSignal()

    def __init__(self, x, y, w, h):
        """MyScene is the general scene in which items are displayed

        :param x: x origin
        :type x: int
        :param y: y origin
        :type y: int
        :param w: width
        :type w: int
        :param h: height
        :type h: int
        """
        QGraphicsScene.__init__(self, x, y, w, h)

    def mouseMoveEvent(self, event):
        """Pass the event to the underlying item

        :param event: event to pass
        :type event: QEvent
        """
        for item in list(self.items()):
            r = item.boundingRect()
            r.translate(item.pos())
            if r.contains(event.scenePos()):
                return item.mouseMoveEvent(event)
        return QGraphicsScene.mouseMoveEvent(self, event)

    def mouseDoubleClickEvent(self, event):
        for item in list(self.items()):
            r = item.boundingRect()
            r.translate(item.pos())
            if r.contains(event.scenePos()) and isinstance(item, LegendItem):
                return self.plot_edited.emit()
        return QGraphicsScene.mouseDoubleClickEvent(self, event)


class PlotView(QWidget):

    DEFAULT_ROW_HEIGHT = 150
    DEFAULT_COLUMN_WIDTH = 150

    # Emitted when some styles have been updated
    styles_updated = pyqtSignal()
    centerSceneOnScale = pyqtSignal()
    on_config_remove = pyqtSignal()

    def __init__(
        self,
        title=None,
        image_dir=None,
        orientation=ORIENTATION_HORIZONTAL,
        x_orientation=ORIENTATION_LEFT_TO_RIGHT,
        y_orientation=ORIENTATION_UPWARD,
        parent=None,
    ):
        """Plot Widget inserted into main dialog.
        Subclass: DepthPlotView, TimeSeriesWrapper

        :param title: widget title
        :type title: string
        :param image_dir: images directory to import images
        :type image_dir: string
        :param orientation: plotview orientation for well log (vertical, 1) and time series (horizontal, 0)
        :type orientation: bool
        :param parent: parent
        :type parent: QObject
        """
        QWidget.__init__(self, parent)

        self.__orientation = orientation
        self.__x_orientation = x_orientation
        self.__y_orientation = y_orientation
        self.__axis = Axis(orientation)
        self._toolbar = QToolBar()
        self._scene = MyScene(0, 0, 600, 400)
        self._scene.plot_edited.connect(self.edit_plot)
        self.__view = PlotGraphicsView(self._scene, orientation)
        self.centerSceneOnScale.connect(self.center_scene_on_scale)

        self._scene.sceneRectChanged.connect(self.on_rect_changed)

        if image_dir is None:
            image_dir = os.path.join(os.path.dirname(__file__), "img")

        if orientation == ORIENTATION_HORIZONTAL:
            self.__action_move_cell_before = QAction(
                QIcon(os.path.join(image_dir, "up.svg")), self.tr("Move up"), self._toolbar
            )
            self.__action_move_cell_after = QAction(
                QIcon(os.path.join(image_dir, "down.svg")), self.tr("Move down"), self._toolbar
            )
        else:
            self.__action_move_cell_before = QAction(
                QIcon(os.path.join(image_dir, "left.svg")), self.tr("Move left"), self._toolbar
            )
            self.__action_move_cell_after = QAction(
                QIcon(os.path.join(image_dir, "right.svg")), self.tr("Move right"), self._toolbar
            )

        self.__action_move_cell_before.triggered.connect(self.on_move_cell_before)
        self.__action_move_cell_after.triggered.connect(self.on_move_cell_after)

        self.__action_edit_style = QAction(
            QIcon(os.path.join(image_dir, "symbology.svg")), self.tr("Edit style"), self._toolbar
        )
        self.__action_edit_style.triggered.connect(self.on_edit_style)

        self.__action_add_cell = QAction(
            QIcon(os.path.join(image_dir, "add.svg")), self.tr("Add a data cell"), self._toolbar
        )
        self.__action_add_cell.triggered.connect(self.on_add_cell)

        self.__action_remove_cell = QAction(
            QIcon(os.path.join(image_dir, "remove.svg")), self.tr("Remove the cell"), self._toolbar
        )
        self.__action_remove_cell.triggered.connect(self.on_remove_cell)

        self._toolbar.addAction(self.__action_move_cell_before)
        self._toolbar.addAction(self.__action_move_cell_after)
        self._toolbar.addAction(self.__action_edit_style)
        self._toolbar.addAction(self.__action_add_cell)
        self._toolbar.addAction(self.__action_remove_cell)

        self.__title_label = QLabel()
        if title is not None:
            self.set_title(title)

        self.__status_bar = QStatusBar()

        box = QVBoxLayout()
        box.addWidget(self.__title_label)
        box.addWidget(self._toolbar)
        box.addWidget(self.__view)
        box.addWidget(self.__status_bar)
        self.setLayout(box)

        # (plot_item, legend_item) for each cell
        self.__cells = []
        # { layer : (plot_item, legend_item) }
        self.__data2items = {}
        self.__cell_sizes = []

        # Minimum and maximum in the domain axis direction (a.k.a. X for horizontal)
        if orientation == ORIENTATION_HORIZONTAL:
            self._min_x, self._max_x = 0, 1200
        else:
            self._min_x, self._max_x = 0, 100

        # Parameter to move column in value axis direction (a.k.a. Y for horizontal)
        self._move_y = 0
        self._y_axis_pos = 0

        self.__allow_mouse_translation = True
        self.__translation_orig = None

        self.__style_dir = os.path.join(os.path.dirname(__file__), "styles")

        self.__has_scale = False

        # Unselect cell on creation
        self.select_cell(-1)

        self._update_cell_sizes()
        self.centerSceneOnScale.emit()

    def on_rect_changed(self, rect):
        """Triggered when the widget is resized

        :param rect: Rectangle for scene display
        :type rect: QRect
        """
        if self.__orientation == ORIENTATION_HORIZONTAL:
            for item, _, _ in self.__cells:
                item.set_width(rect.width())
        else:
            for item, _, _ in self.__cells:
                item.set_height(rect.height())
        self.centerSceneOnScale.emit()

    def set_title(self, title):
        """Set the title attribute

        :param title: Rectangle for scene display
        :type title: string
        """
        self.__title_label.setText(title)

    def _place_items(self):
        """Method to place plot item in right order"""
        xy = 0
        for i, r in enumerate(self.__cells):
            item, legend, symbol_legend = r
            size = self.__cell_sizes[i]
            if self.__orientation == ORIENTATION_HORIZONTAL:
                legend.setPos(0, xy)
                if symbol_legend is not None:
                    symbol_legend.setPos(legend.boundingRect().width(), xy)
                    item.setPos(legend.boundingRect().width(), xy)
                else:
                    item.setPos(legend.boundingRect().width(), xy)
            else:
                legend.setPos(xy, 0)
                if symbol_legend is not None:
                    symbol_legend.setPos(xy, legend.boundingRect().height())
                    item.setPos(xy, legend.boundingRect().height())
                else:
                    item.setPos(xy, legend.boundingRect().height())
            xy += size

        rect = self._scene.sceneRect()
        self.__axis.value_set_size(rect, xy)
        self._y_axis_pos = 0
        self._scene.setSceneRect(rect)

    def _add_cell(self, plot_item, legend_item, symbol_legend, reversed=False):
        """Add a new cell to the cell list according the plotview orientation

        :param plot_item: plot item to display (values)
        :type plot_item: PlotItem
        :param legend_item: legend item to display
        :type legend_item: LegendItem
        :param reversed: reversed plot (scale)
        :type reversed: bool
        """
        self._scene.addItem(plot_item)
        self._scene.addItem(legend_item)
        if symbol_legend is not None:
            self._scene.addItem(symbol_legend)

        plot_item.set_min_depth(self._min_x)
        plot_item.set_max_depth(self._max_x)

        if self.__orientation == ORIENTATION_VERTICAL:
            if not reversed:
                self.__cells.append((plot_item, legend_item, symbol_legend))
                self.__cell_sizes.append(plot_item.boundingRect().width())
            else:
                self.__cells.insert(0, (plot_item, legend_item, symbol_legend))
                self.__cell_sizes.insert(0, plot_item.boundingRect().width())
        else:
            if not reversed:
                self.__cells.insert(0, (plot_item, legend_item, symbol_legend))
                self.__cell_sizes.insert(0, plot_item.boundingRect().height())
            else:
                self.__cells.append((plot_item, legend_item, symbol_legend))
                self.__cell_sizes.append(plot_item.boundingRect().height())

        self._place_items()

    def set_x_range(self, min_x, max_x, elevation_scale=False):
        """Set scale range

        :param min_x: min x scale value
        :type min_x: float
        :param max_x: max x scale value
        :type max_x: float
        """
        self._min_x, self._max_x = min_x, max_x
        # Add a scale if none yet
        if self.__orientation == ORIENTATION_VERTICAL:
            if elevation_scale:
                title_scale = "Elevation"
            else:
                title_scale = "Depth"
        else:
            title_scale = "Time"
        self.add_scale(title=title_scale, elevation_scale=elevation_scale)
        self._update_cell_sizes()

    def _update_cell_sizes(self):
        """Method to resize cells if needed"""

        if self._min_x is None and self._move_y is None:
            return

        if self._move_y:
            # Watch if translation will not detach first column from scale
            if self._y_axis_pos - self._move_y > 0 and self.__orientation == ORIENTATION_VERTICAL:
                self._move_y = self._y_axis_pos
            if self._y_axis_pos - self._move_y < 0 and self.__orientation == ORIENTATION_HORIZONTAL:
                self._move_y = self._y_axis_pos
            self._y_axis_pos -= self._move_y

        # Update cells
        for i, cell in enumerate(self.__cells):
            item, legend, symbol = cell
            item.set_min_depth(self._min_x)
            item.set_max_depth(self._max_x)
            if self._move_y:
                # Don't scroll scale bar (the first for well log, the last for time series log)
                if i != 0 and self.__orientation == ORIENTATION_VERTICAL:
                    item.moveBy(-self._move_y, 0)
                    legend.moveBy(-self._move_y, 0)
                    if symbol is not None:
                        symbol.moveBy(-self._move_y, 0)
                if i != len(self.__cells) - 1 and self.__orientation == ORIENTATION_HORIZONTAL:
                    item.moveBy(0, -self._move_y)
                    legend.moveBy(0, -self._move_y)
                    if symbol is not None:
                        symbol.moveBy(0, -self._move_y)

            item.update()
        self._move_y = 0

    def center_scene_on_scale(self):
        """ For time series, if logs scene is too high for windows,
        focus on the bottom to see the scale
        """
        if (
            self.__orientation == ORIENTATION_HORIZONTAL
            and self.__view.height() < self._scene.sceneRect().height()
        ):
            self.__view.centerOn(
                self._scene.sceneRect().width(),
                self._scene.sceneRect().height() - self.__view.height() / 2 - 1,
            )

    def clear_data_cells(self):
        """Remove item from scene"""
        for (item, legend, symbol) in self.__cells:
            self._scene.removeItem(legend)
            self._scene.removeItem(item)
            if symbol is not None:
                self._scene.removeItem(symbol)

        # remove from internal lists
        self.__cells = []
        self.__cell_sizes = []
        self.__data2items = {}
        self._move_y = 0
        self._y_axis_pos = 0

        self.select_cell(-1)
        self._place_items()
        self._update_button_visibility()
        self.__has_scale = False

    def on_plot_tooltip(self, station_name, txt):
        """Display station name tooltip

        :param station_name: name of the station to display
        :type station_name: str
        :param txt: text to display as fallback
        :type txt: str
        """
        if station_name is not None:
            self.__status_bar.showMessage(u"Station: {} ".format(station_name) + txt)
        else:
            self.__status_bar.showMessage(txt)

    def add_data_cell(
        self,
        data,
        title,
        station_name=None,
        config=None,
        stacked_data=None,
        stacked_data_names=None,
    ):
        """
        Parameters
        ----------
        data: ??
        title: str
        uom: str
          Unit of measure
        station_name: str
          Station name
        config: PlotConfig
        """
        symbology, symbology_type = config.get_symbology()

        if self.__orientation == ORIENTATION_HORIZONTAL:
            default_size = QSizeF(self._scene.width(), config["plot_size"])
        else:
            default_size = QSizeF(config["plot_size"], self._scene.height())

        if isinstance(data, IntervalData):
            render_type = POLYGON_RENDERER if not symbology_type else symbology_type
        else:
            render_type = POINT_RENDERER if not symbology_type else symbology_type

        plot_item = PlotItem(
            default_size,
            render_type=render_type,
            symbology=symbology,
            x_orientation=self.__x_orientation,
            y_orientation=self.__y_orientation,
            uncertainty_column=config["uncertainty_column"],
            unit_factor=unitConversionFactor(config["uom"], config["displayed_uom"])
            if config.get("uom")
            else 1.0,
            scale_type=config["scale_type"],
        )

        plot_item.style_updated.connect(self.styles_updated)

        plot_item.set_layer(data.get_layer())
        plot_item.tooltipRequested.connect(lambda txt: self.on_plot_tooltip(station_name, txt))

        legend_item = LegendItem(
            self.__axis.value_size(default_size),
            title,
            station_name,
            unit_of_measure=data.get_uom(),
            displayed_unit=config.get("displayed_uom"),
            # legend is vertical if the plot is horizontal
            is_vertical=self.__orientation == ORIENTATION_HORIZONTAL,
        )
        data.data_modified.connect(lambda data=data: self._update_data_cell(data, config))

        # center on new data
        self._min_x, self._max_x = data.get_x_min(), data.get_x_max()
        if self._min_x and self._min_x == self._max_x:
            if self.__orientation == ORIENTATION_HORIZONTAL:
                # if we have only one value, center it on a 4 days range
                self._min_x -= 3600 * 24 * 2
                self._max_x += 3600 * 24 * 2
            else:
                # -/+ 1 meter in depth
                self._min_x -= 1
                self._max_x += 1

        self.__data2items[data] = (plot_item, legend_item)

        self._update_data_cell(data, config)
        symbol_legend = None
        if stacked_data != {} and stacked_data is not None:
            plot_item.set_stacked_data(stacked_data)
            names = [title] + stacked_data_names
            plots_renderer, plots_renderer_type = plot_item.get_renderer_info()
            symbol_legend = SymbolLegend(
                self.__axis.value_size(default_size),
                names,
                plots_renderer,
                plots_renderer_type,
                is_vertical=self.__orientation == ORIENTATION_HORIZONTAL,
            )
        self._add_cell(plot_item, legend_item, symbol_legend)
        self._update_cell_sizes()

    def add_scale(self, title="Time", elevation_scale=False):
        """"Add a scale plot

        :param title: title of the scale plot
        :type title: str
        """
        if self._min_x is None or self._max_x is None:
            return
        if self.__has_scale:
            return
        if self.__orientation == ORIENTATION_HORIZONTAL:
            scale_item = TimeScaleItem(
                self._scene.width(), self.DEFAULT_ROW_HEIGHT * 3 / 4, self._min_x, self._max_x
            )
            legend_item = LegendItem(self.DEFAULT_ROW_HEIGHT * 3 / 4, title, "", is_vertical=True)
        else:
            scale_item = ZScaleItem(
                self.DEFAULT_COLUMN_WIDTH / 2,
                self._scene.height(),
                self._min_x,
                self._max_x,
                elevation_scale,
            )
            legend_item = LegendItem(self.DEFAULT_COLUMN_WIDTH / 2, title, "", unit_of_measure="m")

        scale_item.setZValue(1)
        legend_item.setZValue(1)
        self._add_cell(scale_item, legend_item, None, reversed=True)
        self.__has_scale = True

    def _update_data_cell(self, data, config, stacked_data=None):
        """ Update plot  if data changed

        :param data: ??
        :type data: ??
        :param config: layer log configuration
        :type config: dict
        """
        plot_item, legend_item = self.__data2items[data]

        y_values = data.get_y_values()
        x_values = data.get_x_values()
        if y_values is None or x_values is None:
            plot_item.set_data_window(None)
            return
        plot_item.set_data(
            data.get_x_values(),
            data.get_y_values(),
            data.get_ids_values(),
            data.get_uncertainty_values(),
            data.get_uom(),
        )
        win = plot_item.data_window()
        min_x, min_y, max_x, max_y = (win.left(), win.top(), win.right(), win.bottom())

        if config and config.get("min") is not None:
            min_y = float(config["min"])
        if config and config.get("max") is not None:
            max_y = float(config["max"])
        # legend
        legend_item.set_scale(min_y, max_y)
        plot_item.set_data_window(QRectF(min_x, min_y, max_x - min_x, max_y - min_y))

        self._scene.update()

    def select_cell_at(self, pos):
        """ Select a cell at the cursor position

        :param pos: x,y cursor position
        :type pos: tuple
        """
        s = self.__axis.value_pos(pos)
        # handle y position axis
        r = self._y_axis_pos
        selected = -1
        for cell_idx, size in enumerate(self.__cell_sizes):
            if s >= r and s < r + size:
                selected = cell_idx
                break
            r += size
        # Select the cell only if no form has been asked for
        if not self.__cells[selected][0].form(pos):
            if selected != 0 and self.__orientation == ORIENTATION_VERTICAL:
                self.select_cell(selected)
            elif selected != len(self.__cells) - 1 and self.__orientation == ORIENTATION_HORIZONTAL:
                self.select_cell(selected)
            else:
                self.select_cell(-1)
        elif self.__selected_cell != selected:
            self.select_cell(-1)

    def select_cell(self, idx):
        """ Select cell from there index

        :param idx: cell index
        :type idx: int
        """
        self.__selected_cell = idx
        for i, p in enumerate(self.__cells):
            item, legend, symbol = p
            item.set_selected(idx == i)
            legend.set_selected(idx == i)
            if symbol is not None:
                symbol.set_selected(idx == i)
            item.update()
            legend.update()

        self._update_button_visibility()

    def selected_cell(self):
        """Retrun the selected cell

        :return: PlotItem and LegendItem
        :rtype: tuple
        """
        return self.__cells[self.__selected_cell] if self.__selected_cell != -1 else None

    def _update_button_visibility(self):
        """ Enable button """
        image_dir = os.path.join(os.path.dirname(__file__), "img")
        idx = self.__selected_cell
        if (
            idx in [0, -1]
            and self.__orientation == ORIENTATION_VERTICAL
            or (idx in [len(self.__cells) - 1, -1] and self.__orientation == ORIENTATION_HORIZONTAL)
        ):
            self.__action_move_cell_before.setEnabled(False)
            self.__action_move_cell_after.setEnabled(False)
            self.__action_edit_style.setEnabled(False)
            self.__action_remove_cell.setEnabled(False)
            self.__action_add_cell.setIcon(QIcon(os.path.join(image_dir, "add.svg")))
            return
        else:
            self.__action_move_cell_before.setEnabled(idx != -1 and idx > 0 + self.__orientation)
            self.__action_move_cell_after.setEnabled(
                idx != -1 and idx < len(self.__cells) - 1 - (not self.__orientation)
            )
            self.__action_edit_style.setEnabled(idx != -1)
            self.__action_remove_cell.setEnabled(idx != -1)
            plot, legend, symbol = self.__cells[idx]
            self.__action_add_cell.setIcon(
                QIcon(os.path.join(image_dir, "stack_plot.svg"))
                if isinstance(plot, PlotItem)
                else QIcon(os.path.join(image_dir, "add.svg"))
            )
            self.__action_add_cell.setText(
                self.tr("Stack a data to the selected data cell")
                if isinstance(plot, PlotItem)
                else self.tr("Add a data cell")
            )
            return

    def on_move_cell_before(self):
        """ Triggered on before arrow click """
        if self.__selected_cell < 1:
            return

        sel = self.__selected_cell
        self.__cells[sel - 1], self.__cells[sel] = (self.__cells[sel], self.__cells[sel - 1])
        self.__cell_sizes[sel - 1], self.__cell_sizes[sel] = (
            self.__cell_sizes[sel],
            self.__cell_sizes[sel - 1],
        )
        self.__selected_cell -= 1
        self._place_items()
        self._update_button_visibility()

    def on_move_cell_after(self):
        """ Triggered on after arrow click """
        if self.__selected_cell == -1 or self.__selected_cell >= len(self.__cells) - 1:
            return

        sel = self.__selected_cell
        self.__cells[sel + 1], self.__cells[sel] = (self.__cells[sel], self.__cells[sel + 1])
        self.__cell_sizes[sel + 1], self.__cell_sizes[sel] = (
            self.__cell_sizes[sel],
            self.__cell_sizes[sel + 1],
        )
        self.__selected_cell += 1
        self._place_items()
        self._update_button_visibility()

    def on_remove_cell(self):
        """ Triggered on remove action """
        if self.__selected_cell == -1:
            return

        sel = self.__selected_cell

        # remove item from scenes
        item, legend, symbol = self.__cells[sel]
        self._scene.removeItem(legend)
        self._scene.removeItem(item)
        if symbol is not None:
            self._scene.removeItem(symbol)
        self._post_remove(item, legend)

        # remove from internal list
        del self.__cells[sel]
        del self.__cell_sizes[sel]
        self.__selected_cell = -1
        self._place_items()
        self._update_button_visibility()

    def _post_remove(self):
        # to be overridden by subclasses
        pass

    def item_type_on_cursor(self, pos):
        for item in self._scene.items():
            r = item.boundingRect()
            r.translate(item.pos())
            if r.contains(pos):
                return item
        return None

    def on_wheel(self, dt, pos):
        for item, legend, _ in self.__cells:
            r = item.boundingRect()
            r.translate(item.pos())
            if r.contains(pos) and isinstance(item, PlotItem):
                if self.__orientation == ORIENTATION_HORIZONTAL:
                    relative_pos = 1 - (pos.y() - r.y()) / r.height()
                else:
                    relative_pos = (pos.x() - r.x()) / r.width()
                break
            item = None
        if item is not None:
            self.zoom(dt, relative_pos, item, legend)

    def zoom(self):
        # to be overridden by subclasses
        pass

    def on_pan(self, translation_orig):
        for item, legend, _ in self.__cells:
            r = item.boundingRect()
            r.translate(item.pos())
            if r.contains(translation_orig) and isinstance(item, PlotItem):
                if self.__orientation == ORIENTATION_HORIZONTAL:
                    y_translation = -self._move_y / r.height()
                else:
                    y_translation = self._move_y / r.width()
                break
            item = None
        if item is not None:
            self.pan(y_translation, item, legend)
        self._move_y = 0

    def pan(self):
        # to be overridden by subclasses
        pass

    def on_edit_style(self):
        """ Open style dialog"""
        if self.__selected_cell == -1:
            return

        item = self.__cells[self.__selected_cell][0]
        item.edit_style()

    def edit_plot(self):
        if self.__selected_cell == -1:
            return

        item, legend, _ = self.__cells[self.__selected_cell]
        self._edit_plot(item, legend)

    def on_add_cell(self):
        # to be overridden by subclasses
        pass

    def styles(self):
        """Return the current style of each item"""
        return dict(
            [
                (item.layer().id(), item.qgis_style())
                for item, _, _ in self.__cells
                if hasattr(item, "qgis_style")
            ]
        )

    def y_axis_pos(self):
        """ Return y axis position. 0 on init,
        the value changes when colum are moved

        :return: y axis position
        :rtype: float
        """
        return self._y_axis_pos

    def orientation(self):
        """ Return view orientation

        :return: horizontal or vertical
        :rtype: bool
        """
        return self.__orientation

    def x_orientation(self):
        """ Return x orientation

        :return: LEFT_TO_RIGHT, UPWARD or DOWNWARD
        :rtype: int
        """
        return self.__x_orientation

    def y_orientation(self):
        """ Return y orientation

        :return: LEFT_TO_RIGHT, UPWARD or DOWNWARD
        :rtype: int
        """
        return self.__y_orientation
