# -*- 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
import qstylizer.style
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,
    QDialog,
    QDialogButtonBox,
    QGraphicsOpacityEffect,
    QHBoxLayout,
    QLabel,
    QLayout,
    QMainWindow,
    QMessageBox,
    QPushButton,
    QSpacerItem,
    QToolButton,
    QVBoxLayout,
    QWidget,
)

# Local imports
from spyder.api.widgets.dialogs import SpyderDialogButtonBox
from spyder.api.widgets.menus import SpyderMenu
from spyder.api.widgets.mixins import SvgToScaledPixmap
from spyder.api.translations import _
from spyder.api.widgets.comboboxes import SpyderComboBox
from spyder.plugins.layout.layouts import DefaultLayouts
from spyder.utils.icon_manager import ima
from spyder.utils.image_path_manager import get_image_path
from spyder.utils.palette import SpyderPalette
from spyder.utils.qthelpers import add_actions, create_action
from spyder.utils.stylesheet import AppStyle, DialogStyle


MAIN_TOP_COLOR = MAIN_BG_COLOR = QColor(SpyderPalette.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().__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, b"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().__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().__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 = SpyderComboBox()
        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 = SpyderMenu(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
        if self.spy_window.layouts:
            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()
                    self.close_tour()

    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, SvgToScaledPixmap):
    """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.setWindowTitle(_("Spyder tour"))
        self.tour_function = tour_function

        # Image
        image_label = QLabel(self)
        image_label.setPixmap(
            self.svg_to_scaled_pixmap(
                "tour-spyder-logo", rescale=DialogStyle.IconScaleFactor
            )
        )

        images_layout = QHBoxLayout()
        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.setObjectName("title")
        tour_label_title.setWordWrap(True)
        tour_label = QLabel(
            _("Check out our interactive tour to "
              "explore some of Spyder's panes and features."))
        tour_label.setObjectName("content")
        tour_label.setWordWrap(True)
        tour_label.setFixedWidth(340)

        # Buttons
        self.launch_tour_button = QPushButton(_('Start tour'))
        self.launch_tour_button.setObjectName("launch-button")
        self.launch_tour_button.setAutoDefault(False)

        self.dismiss_button = QPushButton(_('Dismiss'))
        self.dismiss_button.setObjectName("dismiss-button")
        self.dismiss_button.setAutoDefault(False)

        bbox = SpyderDialogButtonBox()
        bbox.addButton(self.launch_tour_button, QDialogButtonBox.ActionRole)
        bbox.addButton(self.dismiss_button, QDialogButtonBox.RejectRole)
        bbox.layout().setSpacing(3 * AppStyle.MarginSize)
        bbox.setStyleSheet(self._buttons_stylesheet)

        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.addWidget(bbox)
            vertical_layout.addStretch()
        else:
            vertical_layout.addLayout(label_layout)
            vertical_layout.addWidget(bbox)

        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.setStyleSheet(self._main_stylesheet)

        self.launch_tour_button.clicked.connect(self._start_tour)
        self.dismiss_button.clicked.connect(self.close)
        self.setContentsMargins(18, 40, 18, 40)
        if MAC:
            general_layout.setSizeConstraint(QLayout.SetFixedSize)
        else:
            self.setFixedSize(640, 280)

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

    @property
    def _main_stylesheet(self):
        css = qstylizer.style.StyleSheet()

        # Set background color
        for widget in ["QDialog", "QLabel"]:
            css[widget].setValues(
                backgroundColor=DialogStyle.BackgroundColor
            )

        # Set font size for QLabels
        css["QLabel#title"].setValues(
            fontSize=DialogStyle.TitleFontSize
        )

        css["QLabel#content"].setValues(
            fontSize=DialogStyle.ContentFontSize
        )

        return css.toString()

    @property
    def _buttons_stylesheet(self):
        css = qstylizer.style.StyleSheet()

        # Set base style for buttons
        css.QPushButton.setValues(
            color=SpyderPalette.COLOR_TEXT_1,
            fontSize=DialogStyle.ButtonsFontSize,
            padding=DialogStyle.ButtonsPadding,
        )

        # Set style for launch button
        css["QPushButton#launch-button"].setValues(
            backgroundColor=SpyderPalette.COLOR_ACCENT_2,
            borderColor=SpyderPalette.COLOR_ACCENT_2,
        )

        css["QPushButton#launch-button:hover:!pressed"].setValues(
            backgroundColor=SpyderPalette.COLOR_ACCENT_3,
        )

        css["QPushButton#launch-button:pressed"].setValues(
            backgroundColor=SpyderPalette.COLOR_ACCENT_4,
        )

        # Set style for dismiss button
        css["QPushButton#dismiss-button"].setValues(
            backgroundColor=SpyderPalette.COLOR_BACKGROUND_4,
            borderColor=SpyderPalette.COLOR_BACKGROUND_4,
        )

        css["QPushButton#dismiss-button:hover:!pressed"].setValues(
            backgroundColor=SpyderPalette.COLOR_BACKGROUND_5,
        )

        css["QPushButton#dismiss-button:pressed"].setValues(
            backgroundColor=SpyderPalette.COLOR_BACKGROUND_6,
        )

        return css.toString()

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

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

    def __init__(self):
        super().__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, b"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()
