# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)

"""Spyder interactive tours"""

# pylint: disable=C0103
# pylint: disable=R0903
# pylint: disable=R0911
# pylint: disable=R0201

# Standard library imports
from math import ceil
import sys

# Third party imports
from qtpy.QtCore import (QEasingCurve, QPoint, QPropertyAnimation, QRectF, Qt,
                         Signal)
from qtpy.QtGui import (QBrush, QColor, QIcon, QPainter, QPainterPath, QPen,
                        QPixmap, QRegion)
from qtpy.QtWidgets import (QAction, QApplication, QComboBox, QDialog,
                            QGraphicsOpacityEffect, QHBoxLayout, QLabel,
                            QLayout, QMainWindow, QMenu, QMessageBox,
                            QPushButton, QSpacerItem, QToolButton, QVBoxLayout,
                            QWidget)

# Local imports
from spyder import __docs_url__
from spyder.api.panel import Panel
from spyder.api.translations import _
from spyder.config.base import _
from spyder.plugins.layout.layouts import DefaultLayouts
from spyder.py3compat import to_binary_string
from spyder.utils.icon_manager import ima
from spyder.utils.image_path_manager import get_image_path
from spyder.utils.palette import QStylePalette, SpyderPalette
from spyder.utils.qthelpers import add_actions, create_action
from spyder.utils.stylesheet import DialogStyle


MAIN_TOP_COLOR = MAIN_BG_COLOR = QColor(QStylePalette.COLOR_BACKGROUND_1)

MAC = sys.platform == 'darwin'


class FadingDialog(QDialog):
    """A general fade in/fade out QDialog with some builtin functions"""
    sig_key_pressed = Signal()

    def __init__(self, parent, opacity, duration, easing_curve):
        super(FadingDialog, self).__init__(parent)

        self.parent = parent
        self.opacity_min = min(opacity)
        self.opacity_max = max(opacity)
        self.duration_fadein = duration[0]
        self.duration_fadeout = duration[-1]
        self.easing_curve_in = easing_curve[0]
        self.easing_curve_out = easing_curve[-1]
        self.effect = None
        self.anim = None

        self._fade_running = False
        self._funcs_before_fade_in = []
        self._funcs_after_fade_in = []
        self._funcs_before_fade_out = []
        self._funcs_after_fade_out = []

        self.setModal(False)

    def _run(self, funcs):
        for func in funcs:
            func()

    def _run_before_fade_in(self):
        self._run(self._funcs_before_fade_in)

    def _run_after_fade_in(self):
        self._run(self._funcs_after_fade_in)

    def _run_before_fade_out(self):
        self._run(self._funcs_before_fade_out)

    def _run_after_fade_out(self):
        self._run(self._funcs_after_fade_out)

    def _set_fade_finished(self):
        self._fade_running = False

    def _fade_setup(self):
        self._fade_running = True
        self.effect = QGraphicsOpacityEffect(self)
        self.setGraphicsEffect(self.effect)
        self.anim = QPropertyAnimation(
                    self.effect, to_binary_string("opacity"))

    # --- public api
    def fade_in(self, on_finished_connect):
        self._run_before_fade_in()
        self._fade_setup()
        self.show()
        self.raise_()
        self.anim.setEasingCurve(self.easing_curve_in)
        self.anim.setStartValue(self.opacity_min)
        self.anim.setEndValue(self.opacity_max)
        self.anim.setDuration(self.duration_fadein)
        self.anim.finished.connect(on_finished_connect)
        self.anim.finished.connect(self._set_fade_finished)
        self.anim.finished.connect(self._run_after_fade_in)
        self.anim.start()

    def fade_out(self, on_finished_connect):
        self._run_before_fade_out()
        self._fade_setup()
        self.anim.setEasingCurve(self.easing_curve_out)
        self.anim.setStartValue(self.opacity_max)
        self.anim.setEndValue(self.opacity_min)
        self.anim.setDuration(self.duration_fadeout)
        self.anim.finished.connect(on_finished_connect)
        self.anim.finished.connect(self._set_fade_finished)
        self.anim.finished.connect(self._run_after_fade_out)
        self.anim.start()

    def is_fade_running(self):
        return self._fade_running

    def set_funcs_before_fade_in(self, funcs):
        self._funcs_before_fade_in = funcs

    def set_funcs_after_fade_in(self, funcs):
        self._funcs_after_fade_in = funcs

    def set_funcs_before_fade_out(self, funcs):
        self._funcs_before_fade_out = funcs

    def set_funcs_after_fade_out(self, funcs):
        self._funcs_after_fade_out = funcs


class FadingCanvas(FadingDialog):
    """The black semi transparent canvas that covers the application"""
    def __init__(self, parent, opacity, duration, easing_curve, color,
                 tour=None):
        """Create a black semi transparent canvas that covers the app."""
        super(FadingCanvas, self).__init__(parent, opacity, duration,
                                           easing_curve)
        self.parent = parent
        self.tour = tour

        # Canvas color
        self.color = color
        # Decoration color
        self.color_decoration = QColor(SpyderPalette.COLOR_ERROR_2)
        # Width in pixels for decoration
        self.stroke_decoration = 2

        self.region_mask = None
        self.region_subtract = None
        self.region_decoration = None

        self.widgets = None             # The widget to uncover
        self.decoration = None          # The widget to draw decoration
        self.interaction_on = False

        self.path_current = None
        self.path_subtract = None
        self.path_full = None
        self.path_decoration = None

        # widget setup
        self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint)
        self.setAttribute(Qt.WA_TranslucentBackground)
        self.setAttribute(Qt.WA_TransparentForMouseEvents)
        self.setModal(False)
        self.setFocusPolicy(Qt.NoFocus)

        self.set_funcs_before_fade_in([self.update_canvas])
        self.set_funcs_after_fade_out([lambda: self.update_widgets(None),
                                       lambda: self.update_decoration(None)])

    def set_interaction(self, value):
        self.interaction_on = value

    def update_canvas(self):
        w, h = self.parent.size().width(), self.parent.size().height()

        self.path_full = QPainterPath()
        self.path_subtract = QPainterPath()
        self.path_decoration = QPainterPath()
        self.region_mask = QRegion(0, 0, w, h)

        self.path_full.addRect(0, 0, w, h)
        # Add the path
        if self.widgets is not None:
            for widget in self.widgets:
                temp_path = QPainterPath()
                # if widget is not found... find more general way to handle
                if widget is not None:
                    widget.raise_()
                    widget.show()
                    geo = widget.frameGeometry()
                    width, height = geo.width(), geo.height()
                    point = widget.mapTo(self.parent, QPoint(0, 0))
                    x, y = point.x(), point.y()

                    temp_path.addRect(QRectF(x, y, width, height))

                    temp_region = QRegion(x, y, width, height)

                if self.interaction_on:
                    self.region_mask = self.region_mask.subtracted(temp_region)
                self.path_subtract = self.path_subtract.united(temp_path)

            self.path_current = self.path_full.subtracted(self.path_subtract)
        else:
            self.path_current = self.path_full
        if self.decoration is not None:
            for widgets in self.decoration:
                if isinstance(widgets, QWidget):
                    widgets = [widgets]
                geoms = []
                for widget in widgets:
                    widget.raise_()
                    widget.show()
                    geo = widget.frameGeometry()
                    width, height = geo.width(), geo.height()
                    point = widget.mapTo(self.parent, QPoint(0, 0))
                    x, y = point.x(), point.y()
                    geoms.append((x, y, width, height))
                x = min([geom[0] for geom in geoms])
                y = min([geom[1] for geom in geoms])
                width = max([
                    geom[0] + geom[2] for geom in geoms]) - x
                height = max([
                    geom[1] + geom[3] for geom in geoms]) - y
                temp_path = QPainterPath()
                temp_path.addRect(QRectF(x, y, width, height))

                temp_region_1 = QRegion(x-1, y-1, width+2, height+2)
                temp_region_2 = QRegion(x+1, y+1, width-2, height-2)
                temp_region = temp_region_1.subtracted(temp_region_2)

                if self.interaction_on:
                    self.region_mask = self.region_mask.united(temp_region)

                self.path_decoration = self.path_decoration.united(temp_path)
        else:
            self.path_decoration.addRect(0, 0, 0, 0)

        # Add a decoration stroke around widget
        self.setMask(self.region_mask)
        self.update()
        self.repaint()

    def update_widgets(self, widgets):
        self.widgets = widgets

    def update_decoration(self, widgets):
        self.decoration = widgets

    def paintEvent(self, event):
        """Override Qt method"""
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)
        # Decoration
        painter.fillPath(self.path_current, QBrush(self.color))
        painter.strokePath(self.path_decoration, QPen(self.color_decoration,
                                                      self.stroke_decoration))
#        decoration_fill = QColor(self.color_decoration)
#        decoration_fill.setAlphaF(0.25)
#        painter.fillPath(self.path_decoration, decoration_fill)

    def reject(self):
        """Override Qt method"""
        if not self.is_fade_running():
            key = Qt.Key_Escape
            self.key_pressed = key
            self.sig_key_pressed.emit()

    def mousePressEvent(self, event):
        """Override Qt method"""
        pass

    def focusInEvent(self, event):
        """Override Qt method."""
        # To be used so tips do not appear outside spyder
        if self.hasFocus():
            self.tour.gain_focus()

    def focusOutEvent(self, event):
        """Override Qt method."""
        # To be used so tips do not appear outside spyder
        if self.tour.step_current != 0:
            self.tour.lost_focus()


class FadingTipBox(FadingDialog):
    """Dialog that contains the text for each frame in the tour."""
    def __init__(self, parent, opacity, duration, easing_curve, tour=None,
                 color_top=None, color_back=None, combobox_background=None):
        super(FadingTipBox, self).__init__(parent, opacity, duration,
                                           easing_curve)
        self.holder = self.anim  # needed for qt to work
        self.parent = parent
        self.tour = tour

        self.frames = None
        self.offset_shadow = 0
        self.fixed_width = 300

        self.key_pressed = None

        self.setAttribute(Qt.WA_TranslucentBackground)
        self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint |
                            Qt.WindowStaysOnTopHint)
        self.setModal(False)

        # Widgets
        def toolbutton(icon):
            bt = QToolButton()
            bt.setAutoRaise(True)
            bt.setIcon(icon)
            return bt

        self.button_close = toolbutton(ima.icon("tour.close"))
        self.button_home = toolbutton(ima.icon("tour.home"))
        self.button_previous = toolbutton(ima.icon("tour.previous"))
        self.button_end = toolbutton(ima.icon("tour.end"))
        self.button_next = toolbutton(ima.icon("tour.next"))
        self.button_run = QPushButton(_('Run code'))
        self.button_disable = None
        self.button_current = QToolButton()
        self.label_image = QLabel()

        self.label_title = QLabel()
        self.combo_title = QComboBox()
        self.label_current = QLabel()
        self.label_content = QLabel()

        self.label_content.setOpenExternalLinks(True)
        self.label_content.setMinimumWidth(self.fixed_width)
        self.label_content.setMaximumWidth(self.fixed_width)

        self.label_current.setAlignment(Qt.AlignCenter)

        self.label_content.setWordWrap(True)

        self.widgets = [self.label_content, self.label_title,
                        self.label_current, self.combo_title,
                        self.button_close, self.button_run, self.button_next,
                        self.button_previous, self.button_end,
                        self.button_home, self.button_current]

        arrow = get_image_path('hide')

        self.color_top = color_top
        self.color_back = color_back
        self.combobox_background = combobox_background
        self.stylesheet = '''QComboBox {{
                             padding-left: 5px;
                             background-color: {}
                             border-width: 0px;
                             border-radius: 0px;
                             min-height:20px;
                             max-height:20px;
                             }}

                             QComboBox::drop-down  {{
                             subcontrol-origin: padding;
                             subcontrol-position: top left;
                             border-width: 0px;
                             }}

                             QComboBox::down-arrow {{
                             image: url({});
                             }}
                             '''.format(self.combobox_background.name(), arrow)
        # Windows fix, slashes should be always in unix-style
        self.stylesheet = self.stylesheet.replace('\\', '/')

        self.setFocusPolicy(Qt.StrongFocus)
        for widget in self.widgets:
            widget.setFocusPolicy(Qt.NoFocus)
            widget.setStyleSheet(self.stylesheet)

        layout_top = QHBoxLayout()
        layout_top.addWidget(self.combo_title)
        layout_top.addStretch()
        layout_top.addWidget(self.button_close)
        layout_top.addSpacerItem(QSpacerItem(self.offset_shadow,
                                             self.offset_shadow))

        layout_content = QHBoxLayout()
        layout_content.addWidget(self.label_content)
        layout_content.addWidget(self.label_image)
        layout_content.addSpacerItem(QSpacerItem(5, 5))

        layout_run = QHBoxLayout()
        layout_run.addStretch()
        layout_run.addWidget(self.button_run)
        layout_run.addStretch()
        layout_run.addSpacerItem(QSpacerItem(self.offset_shadow,
                                             self.offset_shadow))

        layout_navigation = QHBoxLayout()
        layout_navigation.addWidget(self.button_home)
        layout_navigation.addWidget(self.button_previous)
        layout_navigation.addStretch()
        layout_navigation.addWidget(self.label_current)
        layout_navigation.addStretch()
        layout_navigation.addWidget(self.button_next)
        layout_navigation.addWidget(self.button_end)
        layout_navigation.addSpacerItem(QSpacerItem(self.offset_shadow,
                                                    self.offset_shadow))

        layout = QVBoxLayout()
        layout.addLayout(layout_top)
        layout.addStretch()
        layout.addSpacerItem(QSpacerItem(15, 15))
        layout.addLayout(layout_content)
        layout.addLayout(layout_run)
        layout.addStretch()
        layout.addSpacerItem(QSpacerItem(15, 15))
        layout.addLayout(layout_navigation)
        layout.addSpacerItem(QSpacerItem(self.offset_shadow,
                                         self.offset_shadow))

        layout.setSizeConstraint(QLayout.SetFixedSize)

        self.setLayout(layout)

        self.set_funcs_before_fade_in([self._disable_widgets])
        self.set_funcs_after_fade_in([self._enable_widgets, self.setFocus])
        self.set_funcs_before_fade_out([self._disable_widgets])

        self.setContextMenuPolicy(Qt.CustomContextMenu)

        # signals and slots
        # These are defined every time by the AnimatedTour Class

    def _disable_widgets(self):
        for widget in self.widgets:
            widget.setDisabled(True)

    def _enable_widgets(self):
        self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint |
                            Qt.WindowStaysOnTopHint)
        for widget in self.widgets:
            widget.setDisabled(False)

        if self.button_disable == 'previous':
            self.button_previous.setDisabled(True)
            self.button_home.setDisabled(True)
        elif self.button_disable == 'next':
            self.button_next.setDisabled(True)
            self.button_end.setDisabled(True)
        self.button_run.setDisabled(sys.platform == "darwin")

    def set_data(self, title, content, current, image, run, frames=None,
                 step=None):
        self.label_title.setText(title)
        self.combo_title.clear()
        self.combo_title.addItems(frames)
        self.combo_title.setCurrentIndex(step)
#        min_content_len = max([len(f) for f in frames])
#        self.combo_title.setMinimumContentsLength(min_content_len)

        # Fix and try to see how it looks with a combo box
        self.label_current.setText(current)
        self.button_current.setText(current)
        self.label_content.setText(content)
        self.image = image

        if image is None:
            self.label_image.setFixedHeight(1)
            self.label_image.setFixedWidth(1)
        else:
            extension = image.split('.')[-1]
            self.image = QPixmap(get_image_path(image), extension)
            self.label_image.setPixmap(self.image)
            self.label_image.setFixedSize(self.image.size())

        if run is None:
            self.button_run.setVisible(False)
        else:
            self.button_run.setVisible(True)
            if sys.platform == "darwin":
                self.button_run.setToolTip("Not available on macOS")

        # Refresh layout
        self.layout().activate()

    def set_pos(self, x, y):
        self.x = ceil(x)
        self.y = ceil(y)
        self.move(QPoint(self.x, self.y))

    def build_paths(self):
        geo = self.geometry()
        radius = 0
        shadow = self.offset_shadow
        x0, y0 = geo.x(), geo.y()
        width, height = geo.width() - shadow, geo.height() - shadow

        left, top = 0, 0
        right, bottom = width, height

        self.round_rect_path = QPainterPath()
        self.round_rect_path.moveTo(right, top + radius)
        self.round_rect_path.arcTo(right-radius, top, radius, radius, 0.0,
                                   90.0)
        self.round_rect_path.lineTo(left+radius, top)
        self.round_rect_path.arcTo(left, top, radius, radius, 90.0, 90.0)
        self.round_rect_path.lineTo(left, bottom-radius)
        self.round_rect_path.arcTo(left, bottom-radius, radius, radius, 180.0,
                                   90.0)
        self.round_rect_path.lineTo(right-radius, bottom)
        self.round_rect_path.arcTo(right-radius, bottom-radius, radius, radius,
                                   270.0, 90.0)
        self.round_rect_path.closeSubpath()

        # Top path
        header = 36
        offset = 2
        left, top = offset, offset
        right = width - (offset)
        self.top_rect_path = QPainterPath()
        self.top_rect_path.lineTo(right, top + radius)
        self.top_rect_path.moveTo(right, top + radius)
        self.top_rect_path.arcTo(right-radius, top, radius, radius, 0.0, 90.0)
        self.top_rect_path.lineTo(left+radius, top)
        self.top_rect_path.arcTo(left, top, radius, radius, 90.0, 90.0)
        self.top_rect_path.lineTo(left, top + header)
        self.top_rect_path.lineTo(right, top + header)

    def paintEvent(self, event):
        """Override Qt method."""
        self.build_paths()

        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)

        painter.fillPath(self.round_rect_path, self.color_back)
        painter.fillPath(self.top_rect_path, self.color_top)
        painter.strokePath(self.round_rect_path, QPen(Qt.gray, 1))

        # TODO: Build the pointing arrow?

    def keyReleaseEvent(self, event):
        """Override Qt method."""
        key = event.key()
        self.key_pressed = key

        keys = [Qt.Key_Right, Qt.Key_Left, Qt.Key_Down, Qt.Key_Up,
                Qt.Key_Escape, Qt.Key_PageUp, Qt.Key_PageDown,
                Qt.Key_Home, Qt.Key_End, Qt.Key_Menu]

        if key in keys:
            if not self.is_fade_running():
                self.sig_key_pressed.emit()

    def mousePressEvent(self, event):
        """Override Qt method."""
        # Raise the main application window on click
        self.parent.raise_()
        self.raise_()

        if event.button() == Qt.RightButton:
            pass
#            clicked_widget = self.childAt(event.x(), event.y())
#            if clicked_widget == self.label_current:
#            self.context_menu_requested(event)

    def focusOutEvent(self, event):
        """Override Qt method."""
        # To be used so tips do not appear outside spyder
        self.tour.lost_focus()

    def context_menu_requested(self, event):
        pos = QPoint(event.x(), event.y())
        menu = QMenu(self)

        actions = []
        action_title = create_action(self, _('Go to step: '), icon=QIcon())
        action_title.setDisabled(True)
        actions.append(action_title)
#        actions.append(create_action(self, _(': '), icon=QIcon()))

        add_actions(menu, actions)

        menu.popup(self.mapToGlobal(pos))

    def reject(self):
        """Qt method to handle escape key event"""
        if not self.is_fade_running():
            key = Qt.Key_Escape
            self.key_pressed = key
            self.sig_key_pressed.emit()


class AnimatedTour(QWidget):
    """Widget to display an interactive tour."""

    def __init__(self, parent):
        QWidget.__init__(self, parent)

        self.parent = parent

        # Variables to adjust
        self.duration_canvas = [666, 666]
        self.duration_tips = [333, 333]
        self.opacity_canvas = [0.0, 0.7]
        self.opacity_tips = [0.0, 1.0]
        self.color = Qt.black
        self.easing_curve = [QEasingCurve.Linear]

        self.current_step = 0
        self.step_current = 0
        self.steps = 0
        self.canvas = None
        self.tips = None
        self.frames = None
        self.spy_window = None
        self.initial_fullscreen_state = None

        self.widgets = None
        self.dockwidgets = None
        self.decoration = None
        self.run = None

        self.is_tour_set = False
        self.is_running = False

        # Widgets
        self.canvas = FadingCanvas(self.parent, self.opacity_canvas,
                                   self.duration_canvas, self.easing_curve,
                                   self.color, tour=self)
        self.tips = FadingTipBox(self.parent, self.opacity_tips,
                                 self.duration_tips, self.easing_curve,
                                 tour=self, color_top=MAIN_TOP_COLOR,
                                 color_back=MAIN_BG_COLOR,
                                 combobox_background=MAIN_TOP_COLOR)

        # Widgets setup
        # Needed to fix spyder-ide/spyder#2204.
        self.setAttribute(Qt.WA_TransparentForMouseEvents)

        # Signals and slots
        self.tips.button_next.clicked.connect(self.next_step)
        self.tips.button_previous.clicked.connect(self.previous_step)
        self.tips.button_close.clicked.connect(self.close_tour)
        self.tips.button_run.clicked.connect(self.run_code)
        self.tips.button_home.clicked.connect(self.first_step)
        self.tips.button_end.clicked.connect(self.last_step)
        self.tips.button_run.clicked.connect(
            lambda: self.tips.button_run.setDisabled(True))
        self.tips.combo_title.currentIndexChanged.connect(self.go_to_step)

        # Main window move or resize
        self.parent.sig_resized.connect(self._resized)
        self.parent.sig_moved.connect(self._moved)

        # To capture the arrow keys that allow moving the tour
        self.tips.sig_key_pressed.connect(self._key_pressed)

        # To control the focus of tour
        self.setting_data = False
        self.hidden = False

    def _resized(self, event):
        if self.is_running:
            geom = self.parent.geometry()
            self.canvas.setFixedSize(geom.width(), geom.height())
            self.canvas.update_canvas()

            if self.is_tour_set:
                self._set_data()

    def _moved(self, event):
        if self.is_running:
            geom = self.parent.geometry()
            self.canvas.move(geom.x(), geom.y())

            if self.is_tour_set:
                self._set_data()

    def _close_canvas(self):
        self.tips.hide()
        self.canvas.fade_out(self.canvas.hide)

    def _clear_canvas(self):
        # TODO: Add option to also make it white... might be useful?
        # Make canvas black before transitions
        self.canvas.update_widgets(None)
        self.canvas.update_decoration(None)
        self.canvas.update_canvas()

    def _move_step(self):
        self._set_data()

        # Show/raise the widget so it is located first!
        widgets = self.dockwidgets
        if widgets is not None:
            widget = widgets[0]
            if widget is not None:
                widget.show()
                widget.raise_()

        self._locate_tip_box()

        # Change in canvas only after fadein finishes, for visual aesthetics
        self.tips.fade_in(self.canvas.update_canvas)
        self.tips.raise_()

    def _set_modal(self, value, widgets):
        platform = sys.platform.lower()

        if 'openbsd' in platform:
            pass
        elif 'win' in platform:
            for widget in widgets:
                widget.setModal(value)
                widget.hide()
                widget.show()
        elif 'darwin' in platform:
            pass
        else:
            pass

    def _process_widgets(self, names, spy_window):
        widgets = []
        dockwidgets = []

        for name in names:
            try:
                base = name.split('.')[0]
                try:
                    temp = getattr(spy_window, name)
                except AttributeError:
                    temp = None
                    # Check if it is the current editor
                    if 'get_current_editor()' in name:
                        temp = temp.get_current_editor()
                        temp = getattr(temp, name.split('.')[-1])
                    if temp is None:
                        raise
            except AttributeError:
                temp = eval(f"spy_window.{name}")

            widgets.append(temp)

            # Check if it is a dockwidget and make the widget a dockwidget
            # If not return the same widget
            temp = getattr(temp, 'dockwidget', temp)
            dockwidgets.append(temp)

        return widgets, dockwidgets

    def _set_data(self):
        """Set data that is displayed in each step of the tour."""
        self.setting_data = True
        step, steps, frames = self.step_current, self.steps, self.frames
        current = '{0}/{1}'.format(step + 1, steps)
        frame = frames[step]

        combobox_frames = [u"{0}. {1}".format(i+1, f['title'])
                           for i, f in enumerate(frames)]

        title, content, image = '', '', None
        widgets, dockwidgets, decoration = None, None, None
        run = None

        # Check if entry exists in dic and act accordingly
        if 'title' in frame:
            title = frame['title']

        if 'content' in frame:
            content = frame['content']

        if 'widgets' in frame:
            widget_names = frames[step]['widgets']
            # Get the widgets based on their name
            widgets, dockwidgets = self._process_widgets(widget_names,
                                                         self.spy_window)
        self.widgets = widgets
        self.dockwidgets = dockwidgets

        if 'decoration' in frame:
            widget_names = frames[step]['decoration']
            deco, decoration = self._process_widgets(widget_names,
                                                     self.spy_window)
            self.decoration = decoration

        if 'image' in frame:
            image = frames[step]['image']

        if 'interact' in frame:
            self.canvas.set_interaction(frame['interact'])
            if frame['interact']:
                self._set_modal(False, [self.tips])
            else:
                self._set_modal(True, [self.tips])
        else:
            self.canvas.set_interaction(False)
            self._set_modal(True, [self.tips])

        if 'run' in frame:
            # Assume that the first widget is the console
            run = frame['run']
            self.run = run

        self.tips.set_data(title, content, current, image, run,
                           frames=combobox_frames, step=step)
        self._check_buttons()

        # Make canvas black when starting a new place of decoration
        self.canvas.update_widgets(dockwidgets)
        self.canvas.update_decoration(decoration)
        self.setting_data = False

    def _locate_tip_box(self):
        dockwidgets = self.dockwidgets

        # Store the dimensions of the main window
        geo = self.parent.frameGeometry()
        x, y, width, height = geo.x(), geo.y(), geo.width(), geo.height()
        self.width_main = width
        self.height_main = height
        self.x_main = x
        self.y_main = y

        delta = 20
        offset = 10

        # Here is the tricky part to define the best position for the
        # tip widget
        if dockwidgets is not None:
            if dockwidgets[0] is not None:
                geo = dockwidgets[0].geometry()
                x, y, width, height = (geo.x(), geo.y(),
                                       geo.width(), geo.height())

                point = dockwidgets[0].mapToGlobal(QPoint(0, 0))
                x_glob, y_glob = point.x(), point.y()

                # Put tip to the opposite side of the pane
                if x < self.tips.width():
                    x = x_glob + width + delta
                    y = y_glob + height/2 - self.tips.height()/2
                else:
                    x = x_glob - self.tips.width() - delta
                    y = y_glob + height/2 - self.tips.height()/2

                if (y + self.tips.height()) > (self.y_main + self.height_main):
                    y = (
                        y
                        - (y + self.tips.height() - (
                            self.y_main + self.height_main)) - offset
                    )
        else:
            # Center on parent
            x = self.x_main + self.width_main/2 - self.tips.width()/2
            y = self.y_main + self.height_main/2 - self.tips.height()/2

        self.tips.set_pos(x, y)

    def _check_buttons(self):
        step, steps = self.step_current, self.steps
        self.tips.button_disable = None

        if step == 0:
            self.tips.button_disable = 'previous'

        if step == steps - 1:
            self.tips.button_disable = 'next'

    def _key_pressed(self):
        key = self.tips.key_pressed

        if ((key == Qt.Key_Right or key == Qt.Key_Down or
             key == Qt.Key_PageDown) and self.step_current != self.steps - 1):
            self.next_step()
        elif ((key == Qt.Key_Left or key == Qt.Key_Up or
               key == Qt.Key_PageUp) and self.step_current != 0):
            self.previous_step()
        elif key == Qt.Key_Escape:
            self.close_tour()
        elif key == Qt.Key_Home and self.step_current != 0:
            self.first_step()
        elif key == Qt.Key_End and self.step_current != self.steps - 1:
            self.last_step()
        elif key == Qt.Key_Menu:
            pos = self.tips.label_current.pos()
            self.tips.context_menu_requested(pos)

    def _hiding(self):
        self.hidden = True
        self.tips.hide()

    # --- public api
    def run_code(self):
        codelines = self.run
        console = self.widgets[0]
        for codeline in codelines:
            console.execute_code(codeline)

    def set_tour(self, index, frames, spy_window):
        self.spy_window = spy_window
        self.active_tour_index = index
        self.last_frame_active = frames['last']
        self.frames = frames['tour']
        self.steps = len(self.frames)

        self.is_tour_set = True

    def _handle_fullscreen(self):
        if (self.spy_window.isFullScreen() or
                self.spy_window.layouts._fullscreen_flag):
            if sys.platform == 'darwin':
                self.spy_window.setUpdatesEnabled(True)
                msg_title = _("Request")
                msg = _("To run the tour, please press the green button on "
                        "the left of the Spyder window's title bar to take "
                        "it out of fullscreen mode.")
                QMessageBox.information(self, msg_title, msg,
                                        QMessageBox.Ok)
                return True
            if self.spy_window.layouts._fullscreen_flag:
                self.spy_window.layouts.toggle_fullscreen()
            else:
                self.spy_window.setWindowState(
                    self.spy_window.windowState()
                    & (~ Qt.WindowFullScreen))
        return False

    def start_tour(self):
        self.spy_window.setUpdatesEnabled(False)
        if self._handle_fullscreen():
            return
        self.spy_window.layouts.save_current_window_settings(
            'layout_current_temp/',
            section="quick_layouts",
        )
        self.spy_window.layouts.quick_layout_switch(
            DefaultLayouts.SpyderLayout)
        geo = self.parent.geometry()
        x, y, width, height = geo.x(), geo.y(), geo.width(), geo.height()
#        self.parent_x = x
#        self.parent_y = y
#        self.parent_w = width
#        self.parent_h = height

        # FIXME: reset step to last used value
        # Reset step to beginning
        self.step_current = self.last_frame_active

        # Adjust the canvas size to match the main window size
        self.canvas.setFixedSize(width, height)
        self.canvas.move(QPoint(x, y))
        self.spy_window.setUpdatesEnabled(True)
        self.canvas.fade_in(self._move_step)
        self._clear_canvas()

        self.is_running = True

    def close_tour(self):
        self.tips.fade_out(self._close_canvas)
        self.spy_window.setUpdatesEnabled(False)
        self.canvas.set_interaction(False)
        self._set_modal(True, [self.tips])
        self.canvas.hide()

        try:
            # set the last played frame by updating the available tours in
            # parent. This info will be lost on restart.
            self.parent.tours_available[self.active_tour_index]['last'] =\
                self.step_current
        except Exception:
            pass

        self.is_running = False
        self.spy_window.layouts.quick_layout_switch('current_temp')
        self.spy_window.setUpdatesEnabled(True)

    def hide_tips(self):
        """Hide tips dialog when the main window loses focus."""
        self._clear_canvas()
        self.tips.fade_out(self._hiding)

    def unhide_tips(self):
        """Unhide tips dialog when the main window loses focus."""
        self._clear_canvas()
        self._move_step()
        self.hidden = False

    def next_step(self):
        self._clear_canvas()
        self.step_current += 1
        self.tips.fade_out(self._move_step)

    def previous_step(self):
        self._clear_canvas()
        self.step_current -= 1
        self.tips.fade_out(self._move_step)

    def go_to_step(self, number, id_=None):
        self._clear_canvas()
        self.step_current = number
        self.tips.fade_out(self._move_step)

    def last_step(self):
        self.go_to_step(self.steps - 1)

    def first_step(self):
        self.go_to_step(0)

    def lost_focus(self):
        """Confirm if the tour loses focus and hides the tips."""
        if (self.is_running and
                not self.setting_data and not self.hidden):
            if sys.platform == 'darwin':
                if not self.tour_has_focus():
                    self.hide_tips()
                    if not self.any_has_focus():
                        self.close_tour()
            else:
                if not self.any_has_focus():
                    self.hide_tips()

    def gain_focus(self):
        """Confirm if the tour regains focus and unhides the tips."""
        if (self.is_running and self.any_has_focus() and
           not self.setting_data and self.hidden):
            self.unhide_tips()

    def any_has_focus(self):
        """Returns True if tour or main window has focus."""
        f = (self.hasFocus() or self.parent.hasFocus() or
             self.tour_has_focus() or self.isActiveWindow())
        return f

    def tour_has_focus(self):
        """Returns true if tour or any of its components has focus."""
        f = (self.tips.hasFocus() or self.canvas.hasFocus() or
             self.tips.isActiveWindow())
        return f


class OpenTourDialog(QDialog):
    """Initial widget with tour."""

    def __init__(self, parent, tour_function):
        super().__init__(parent)
        if MAC:
            flags = (self.windowFlags() | Qt.WindowStaysOnTopHint
                     & ~Qt.WindowContextHelpButtonHint)
        else:
            flags = self.windowFlags() & ~Qt.WindowContextHelpButtonHint
        self.setWindowFlags(flags)
        self.tour_function = tour_function

        # Image
        images_layout = QHBoxLayout()
        icon_filename = 'tour-spyder-logo'
        image_path = get_image_path(icon_filename)
        image = QPixmap(image_path)
        image_label = QLabel()
        image_height = int(image.height() * DialogStyle.IconScaleFactor)
        image_width = int(image.width() * DialogStyle.IconScaleFactor)
        image = image.scaled(image_width, image_height, Qt.KeepAspectRatio,
                             Qt.SmoothTransformation)
        image_label.setPixmap(image)

        images_layout.addStretch()
        images_layout.addWidget(image_label)
        images_layout.addStretch()
        if MAC:
            images_layout.setContentsMargins(0, -5, 20, 0)
        else:
            images_layout.setContentsMargins(0, -8, 35, 0)

        # Label
        tour_label_title = QLabel(_("Welcome to Spyder!"))
        tour_label_title.setStyleSheet(f"font-size: {DialogStyle.TitleFontSize}")
        tour_label_title.setWordWrap(True)
        tour_label = QLabel(
            _("Check out our interactive tour to "
              "explore some of Spyder's panes and features."))
        tour_label.setStyleSheet(f"font-size: {DialogStyle.ContentFontSize}")
        tour_label.setWordWrap(True)
        tour_label.setFixedWidth(340)

        # Buttons
        buttons_layout = QHBoxLayout()
        dialog_tour_color = QStylePalette.COLOR_BACKGROUND_2
        start_tour_color = QStylePalette.COLOR_ACCENT_2
        start_tour_hover = QStylePalette.COLOR_ACCENT_3
        start_tour_pressed = QStylePalette.COLOR_ACCENT_4
        dismiss_tour_color = QStylePalette.COLOR_BACKGROUND_4
        dismiss_tour_hover = QStylePalette.COLOR_BACKGROUND_5
        dismiss_tour_pressed = QStylePalette.COLOR_BACKGROUND_6
        font_color = QStylePalette.COLOR_TEXT_1
        self.launch_tour_button = QPushButton(_('Start tour'))
        self.launch_tour_button.setStyleSheet((
          "QPushButton {{ "
          "background-color: {background_color};"
          "border-color: {border_color};"
          "font-size: {font_size};"
          "color: {font_color};"
          "padding: {padding}}}"
          "QPushButton:hover:!pressed {{ "
          "background-color: {color_hover}}}"
          "QPushButton:pressed {{ "
          "background-color: {color_pressed}}}"
        ).format(background_color=start_tour_color,
                 border_color=start_tour_color,
                 font_size=DialogStyle.ButtonsFontSize,
                 font_color=font_color,
                 padding=DialogStyle.ButtonsPadding,
                 color_hover=start_tour_hover,
                 color_pressed=start_tour_pressed))
        self.launch_tour_button.setAutoDefault(False)
        self.dismiss_button = QPushButton(_('Dismiss'))
        self.dismiss_button.setStyleSheet((
          "QPushButton {{ "
          "background-color: {background_color};"
          "border-color: {border_color};"
          "font-size: {font_size};"
          "color: {font_color};"
          "padding: {padding}}}"
          "QPushButton:hover:!pressed {{ "
          "background-color: {color_hover}}}"
          "QPushButton:pressed {{ "
          "background-color: {color_pressed}}}"
        ).format(background_color=dismiss_tour_color,
                 border_color=dismiss_tour_color,
                 font_size=DialogStyle.ButtonsFontSize,
                 font_color=font_color,
                 padding=DialogStyle.ButtonsPadding,
                 color_hover=dismiss_tour_hover,
                 color_pressed=dismiss_tour_pressed))
        self.dismiss_button.setAutoDefault(False)

        buttons_layout.addStretch()
        buttons_layout.addWidget(self.launch_tour_button)
        if not MAC:
            buttons_layout.addSpacing(10)
        buttons_layout.addWidget(self.dismiss_button)

        layout = QHBoxLayout()
        layout.addLayout(images_layout)

        label_layout = QVBoxLayout()
        label_layout.addWidget(tour_label_title)
        if not MAC:
            label_layout.addSpacing(3)
            label_layout.addWidget(tour_label)
        else:
            label_layout.addWidget(tour_label)
            label_layout.addSpacing(10)

        vertical_layout = QVBoxLayout()
        if not MAC:
            vertical_layout.addStretch()
            vertical_layout.addLayout(label_layout)
            vertical_layout.addSpacing(20)
            vertical_layout.addLayout(buttons_layout)
            vertical_layout.addStretch()
        else:
            vertical_layout.addLayout(label_layout)
            vertical_layout.addLayout(buttons_layout)

        general_layout = QHBoxLayout()
        if not MAC:
            general_layout.addStretch()
            general_layout.addLayout(layout)
            general_layout.addSpacing(1)
            general_layout.addLayout(vertical_layout)
            general_layout.addStretch()
        else:
            general_layout.addLayout(layout)
            general_layout.addLayout(vertical_layout)

        self.setLayout(general_layout)

        self.launch_tour_button.clicked.connect(self._start_tour)
        self.dismiss_button.clicked.connect(self.close)
        self.setStyleSheet(f"background-color:{dialog_tour_color}")
        self.setContentsMargins(18, 40, 18, 40)
        if not MAC:
            self.setFixedSize(640, 280)

    def _start_tour(self):
        self.close()
        self.tour_function()


# ----------------------------------------------------------------------------
# Used for testing the functionality
# ----------------------------------------------------------------------------

class TourTestWindow(QMainWindow):
    """ """
    sig_resized = Signal("QResizeEvent")
    sig_moved = Signal("QMoveEvent")

    def __init__(self):
        super(TourTestWindow, self).__init__()
        self.setGeometry(300, 100, 400, 600)
        self.setWindowTitle('Exploring QMainWindow')

        self.exit = QAction('Exit', self)
        self.exit.setStatusTip('Exit program')

        # create the menu bar
        menubar = self.menuBar()
        file_ = menubar.addMenu('&File')
        file_.addAction(self.exit)

        # create the status bar
        self.statusBar()

        # QWidget or its instance needed for box layout
        self.widget = QWidget(self)

        self.button = QPushButton('test')
        self.button1 = QPushButton('1')
        self.button2 = QPushButton('2')

        effect = QGraphicsOpacityEffect(self.button2)
        self.button2.setGraphicsEffect(effect)
        self.anim = QPropertyAnimation(effect, to_binary_string("opacity"))
        self.anim.setStartValue(0.01)
        self.anim.setEndValue(1.0)
        self.anim.setDuration(500)

        lay = QVBoxLayout()
        lay.addWidget(self.button)
        lay.addStretch()
        lay.addWidget(self.button1)
        lay.addWidget(self.button2)

        self.widget.setLayout(lay)

        self.setCentralWidget(self.widget)
        self.button.clicked.connect(self.action1)
        self.button1.clicked.connect(self.action2)

        self.tour = AnimatedTour(self)

    def action1(self):
        frames = get_tour('test')
        index = 0
        dic = {'last': 0, 'tour': frames}
        self.tour.set_tour(index, dic, self)
        self.tour.start_tour()

    def action2(self):
        self.anim.start()

    def resizeEvent(self, event):
        """Reimplement Qt method"""
        QMainWindow.resizeEvent(self, event)
        self.sig_resized.emit(event)

    def moveEvent(self, event):
        """Reimplement Qt method"""
        QMainWindow.moveEvent(self, event)
        self.sig_moved.emit(event)


def local_test():
    from spyder.utils.qthelpers import qapplication

    app = QApplication([])
    win = TourTestWindow()
    win.show()
    app.exec_()


if __name__ == '__main__':
    local_test()
