#-*- coding:utf-8 -*-

#  Copyright © 2009-2015  B. Clausius <barcc@gmx.de>
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  This program 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 General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.


import sys, os
from collections import OrderedDict

from PyQt5.QtCore import Qt, QFileSystemWatcher, QTimer
from PyQt5.QtCore import pyqtSignal as Signal, pyqtSlot as Slot
from PyQt5.QtGui import (QPainter, QFontMetrics, QLinearGradient, QPalette, QPen, QBrush, QIcon,
                         QKeySequence, QStandardItemModel, QStandardItem)
from PyQt5.QtWidgets import (QMainWindow, QSizePolicy, QAction, QMessageBox,
                        QLabel, QLineEdit, QPushButton, QTreeView, QFrame, QAbstractItemView)

from .debug import (debug, DEBUG_ROTATE, DEBUG_DRAW, DEBUG_FPS,
                    DEBUG_LIVESHADER, DEBUG_LIVEPLUGINS)
from . import config
from .settings import settings
from . import dialogs
from . import pluginlib
from .model import Model, empty_model
from .gamestate import GameState
from . import drawingarea

try:
    _, ngettext
except NameError:
    debug('gettext not found')
    _ = lambda string: string
    ngettext = lambda singular, plural, cnt: singular if cnt == 1 else plural
    
    
class AnimType: # pylint: disable=R0903
    __slots__ = ()
    NONE, FWD, BWD, KEY, NEW = range(5)
AnimType = AnimType()
    
    
class ElideLabel (QLabel):
    def paintEvent(self, unused_event):
        p = QPainter(self)
        fm = QFontMetrics(self.font())
        rect = self.contentsRect()
        if fm.width(self.text()) > rect.width():
            gradient = QLinearGradient(rect.topLeft(), rect.topRight())
            start = (rect.width() - 2*rect.height()) / rect.width()
            gradient.setColorAt(start, self.palette().color(QPalette.WindowText))
            gradient.setColorAt(1.0, self.palette().color(QPalette.Window))
            pen = QPen()
            pen.setBrush(QBrush(gradient))
            p.setPen(pen)
        p.drawText(self.rect(), Qt.TextSingleLine, self.text())
        
        
class QMoveEdit (QLineEdit):
    nextword = Signal()
    prevword = Signal()
    swapnext = Signal()
    swapprev = Signal()
    
    def keyPressEvent(self, event):
        if event.matches(QKeySequence.MoveToNextWord):
            self.nextword.emit()
        elif event.matches(QKeySequence.MoveToPreviousWord):
            self.prevword.emit()
        elif event.key() == Qt.Key_Right and event.modifiers() == Qt.AltModifier:
            self.swapnext.emit()
        elif event.key() == Qt.Key_Left and event.modifiers() == Qt.AltModifier:
            self.swapprev.emit()
        else:
            QLineEdit.keyPressEvent(self, event)
            
        
class MainWindow (QMainWindow):
    ### init ###
    def __init__(self, opts, **kwargs):
        super().__init__(**kwargs)
        
        if not os.path.exists(config.USER_DATA_DIR):
            os.makedirs(config.USER_DATA_DIR)
            
        try:
            Model.load_index()
        except OSError as e:
            error_message = str(e)
            debug(error_message)
            QTimer.singleShot(1000, lambda: self.error_dialog(error_message))
            
        self.games_file = opts.games_file
        
        # initialize settings
        try:
            version = settings.load(opts.config_file)
        except OSError as e:
            error_message = _('An error occurred while reading the settings:\n'
                            '{error_message}').format(error_message=e)
            debug(error_message)
            settings.load('')
            #FIXME: don't guess, show message if window is visible
            QTimer.singleShot(1000, lambda: self.error_dialog(error_message))
        else:
            if version == 1:
                from . import schema
                schema.migrate_theme_face_N_color_image_mode(settings)
                values = schema.migrate_game_size_blocks_moves_position(settings)
                #XXX: block indices are not the same in previous version
                #if values is not None:
                #    GameState.migrate_1to2(self.games_file, *values)
            
        # Set default format before any widget is created, so that everything uses the same format.
        # To test this use DEBUG_VFPS. The framerate should be then >60.
        drawingarea.CubeArea.set_default_surface_format()
        
        # UI
        self.ui = dialogs.UI(self, 'main.ui')
        from .ui.main import retranslate
        retranslate(self)
        
        self.ui.splitter.setCollapsible(0, False)
        self.ui.splitter.setStretchFactor(0, 1)
        self.ui.splitter.setStretchFactor(1, 0)
        self.ui.label_debug_text.setVisible(DEBUG_DRAW or DEBUG_FPS)
        
        # set icons
        self.setWindowIcon(QIcon(config.APPICON_FILE))
        self.ui.button_edit_exec.setIcon(QIcon.fromTheme('system-run'))
        self.ui.button_edit_clear.setIcon(QIcon.fromTheme('edit-clear'))
        self.ui.action_quit.setIcon(QIcon.fromTheme('application-exit'))
        self.ui.action_rewind.setIcon(QIcon.fromTheme('media-seek-backward'))
        self.ui.action_previous.setIcon(QIcon.fromTheme('media-skip-backward'))
        self.ui.action_stop.setIcon(QIcon.fromTheme('media-playback-stop'))
        self.ui.action_play.setIcon(QIcon.fromTheme('media-playback-start'))
        self.ui.action_next.setIcon(QIcon.fromTheme('media-skip-forward'))
        self.ui.action_forward.setIcon(QIcon.fromTheme('media-seek-forward'))
        self.ui.action_add_mark.setIcon(QIcon.fromTheme('list-add'))
        self.ui.action_remove_mark.setIcon(QIcon.fromTheme('list-remove'))
        self.ui.action_preferences.setIcon(QIcon.fromTheme('document-properties'))
        self.ui.action_info.setIcon(QIcon.fromTheme('help-about'))
        self.ui.action_help.setIcon(QIcon.fromTheme('help'))
        def set_shortcut(qobj, key):
            # Workaround, need to keep a reference to keysequence to avoid exception
            keysequence = QKeySequence(key)
            qobj.setShortcut(keysequence)
        set_shortcut(self.ui.action_selectmodel, settings.action.selectmodel)
        set_shortcut(self.ui.action_initial_state, settings.action.initial_state)
        set_shortcut(self.ui.action_reset_rotation, settings.action.reset_rotation)
        set_shortcut(self.ui.action_preferences, settings.action.preferences)
        
        # widgets that are not created with Qt Designer
        self.playbarstate = self.create_toolbar()
        self.edit_moves = QMoveEdit()
        self.edit_moves.setObjectName("edit_moves")
        self.edit_moves.setFrame(False)
        self.ui.layout_moves.insertWidget(1, self.edit_moves)
        self.edit_moves.returnPressed.connect(self.on_edit_moves_returnPressed)
        self.edit_moves.nextword.connect(self.on_edit_moves_nextword)
        self.edit_moves.prevword.connect(self.on_edit_moves_prevword)
        self.edit_moves.swapprev.connect(self.on_edit_moves_swapprev)
        self.edit_moves.swapnext.connect(self.on_edit_moves_swapnext)
        self.cube_area = self.create_cube_area(opts)
        self.cube_area.drop_color.connect(self.on_cubearea_drop_color)
        self.cube_area.drop_file.connect(self.on_cubearea_drop_file)
        self.cube_area.debug_info.connect(self.on_cubearea_debug_info)
        self.setTabOrder(self.edit_moves, self.cube_area)
        self.setTabOrder(self.cube_area, self.ui.box_sidepane)
        self.cube_area.setFocus(Qt.OtherFocusReason)
        self.status_text = ElideLabel()
        p = self.status_text.sizePolicy()
        p.setHorizontalStretch(1)
        p.setHorizontalPolicy(QSizePolicy.Ignored)
        self.status_text.setSizePolicy(p)
        self.ui.statusbar.addWidget(self.status_text)
        # created when needed
        self.progress_dialog = None
        
        # actions that belongs to no widget
        self.action_jump_to_editbar = QAction(self)
        self.action_jump_to_editbar.setObjectName('action_jump_to_editbar')
        self.action_jump_to_editbar.setShortcut(QKeySequence(settings.action.edit_moves))
        self.action_jump_to_editbar.triggered.connect(self.on_action_jump_to_editbar_triggered)
        self.addAction(self.action_jump_to_editbar)
        self.action_edit_cube = QAction(self)
        self.action_edit_cube.setObjectName('action_edit_cube')
        self.action_edit_cube.setShortcut(QKeySequence(settings.action.edit_cube))
        self.action_edit_cube.triggered.connect(self.on_action_edit_cube_triggered)
        self.addAction(self.action_edit_cube)
        
        if DEBUG_LIVESHADER:
            self.filesystemwatcher = QFileSystemWatcher()
            self.filesystemwatcher.addPath(config.SHADER_DIR)
            self.filesystemwatcher.directoryChanged.connect(lambda unused_path: self.cube_area.reload_shader())
        if DEBUG_LIVEPLUGINS:
            from . import plugins
            self.filesystemwatcher_plugins = QFileSystemWatcher()
            self.filesystemwatcher_plugins.addPaths([config.PLUGINS_DIR,
                                                     os.path.dirname(plugins.__file__)])
            def reload_plugins(unused_path):
                import importlib
                for name, module in sys.modules.items():
                    if name.startswith(('pybiklib.plugins.')):
                        importlib.reload(module)
                plugin_group = self.active_plugin_group
                self.add_plugins_to_sidepane()
                plugin_group = min(plugin_group, len(self.plugin_group_widgets)-1)
                if plugin_group < 0:
                    plugin_group = None
                self.set_active_plugin_group(plugin_group)
            self.filesystemwatcher_plugins.directoryChanged.connect(reload_plugins)
        
        # plugins
        self.treestore = QStandardItemModel()
        self.selected_tree_path = None
        self.active_plugin_group = None
        self.plugin_group_widgets = []  # tuples of (button, treeview)
        self.plugin_helper = pluginlib.PluginHelper()
        self.add_plugins_to_sidepane(False)
        
        self.unsolved = False
        self.animtype = AnimType.NONE
        
        # Load window state from settings
        self.ui.action_toolbar.setChecked(settings.window.toolbar)
        self.ui.action_editbar.setChecked(settings.window.editbar)
        self.ui.action_statusbar.setChecked(settings.window.statusbar)
        self.ui.toolbar.setVisible(settings.window.toolbar)
        self.ui.frame_editbar.setVisible(settings.window.editbar)
        self.ui.statusbar.setVisible(settings.window.statusbar)
        self.move_keys = self.get_move_key_dict()
        
        settings.keystore.changed.connect(self.on_settings_changed, Qt.QueuedConnection)
        settings.keystore.error.connect(self.on_settings_error)
        
        # Reload last game
        self.game = GameState()
        self.update_ui()
        
        self.resize(*settings.window.size)
        divider_position = settings.window.divider
        self.show()
        sizes = self.ui.splitter.sizes()
        sizes = [divider_position, sum(sizes)-divider_position]
        self.ui.splitter.setSizes(sizes)
        
        QTimer.singleShot(0, self.postcreate)
        
    def create_cube_area(self, opts):
        cube_area = drawingarea.CubeArea(opts=opts, model=empty_model)
        cube_area.setObjectName('drawingarea')
        sizepolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        sizepolicy.setHorizontalStretch(0)
        sizepolicy.setVerticalStretch(0)
        cube_area.setSizePolicy(sizepolicy)
        self.ui.verticalLayout.addWidget(cube_area)
        cube_area.animation_ending.connect(self.on_cubearea_animation_ending)
        cube_area.request_rotation.connect(self.on_cubearea_request_rotation)
        cube_area.request_swap_blocks.connect(self.on_cubearea_request_swap_blocks)
        cube_area.request_rotate_block.connect(self.on_cubearea_request_rotate_block)
        glformat = cube_area.format()
        dialogs.PreferencesDialog.sample_buffers = max(glformat.samples(), 1)
        return cube_area
        
    def create_toolbar(self):
        play_actions = [self.ui.action_rewind, self.ui.action_previous,
                        self.ui.action_stop, self.ui.action_play, self.ui.action_next,
                        self.ui.action_forward, self.ui.action_add_mark, self.ui.action_remove_mark]
        for action in play_actions:
            self.ui.toolbar.addAction(action)
        self.ui.toolbar.addSeparator()
        self.ui.toolbar.addAction(self.ui.action_help)
        playbarstate = PlaybarState(play_actions)
        return playbarstate
        
    def postcreate(self):
        self.load_current_game()
        self.add_plugins_to_sidepane()
        
    ### helper functions ###
    
    @staticmethod
    def get_settings():
        # only for pybiktest.utils
        return settings
        
    @staticmethod
    def keyval_from_name(keystr):
        key = QKeySequence('+'.join(k for k in keystr.split('+') if k != 'KP'))
        if key.count() == 0:
            return 0
        key = key[0]
        if 'KP' in keystr.split('+'):
            key |= int(Qt.KeypadModifier)
        return key
        
    def get_move_key_dict(self):
        return {self.keyval_from_name(key): move for (move, key) in settings.draw.accels}
        
    def add_plugins_to_sidepane(self, load=True):
        # remove old plugins
        self.treestore.clear()
        for button, treeview in self.plugin_group_widgets:
            self.ui.layout_sidepane.removeWidget(button)
            button.deleteLater()
            treeview.setModel(None)
            self.ui.layout_sidepane.removeWidget(treeview)
            treeview.deleteLater()
        self.plugin_group_widgets.clear()
        # get a list of all plugins
        if load:
            plugins = self.plugin_helper.load_plugins()
            if self.plugin_helper.error_messages:
                self.error_dialog('\n\n'.join(self.plugin_helper.error_messages))
        else:
            plugins = []
        # convert plugin list to a tree structure
        ptree = OrderedDict()
        for plugin_path, func in plugins:
            subtree = [None, ptree, None]
            for name, transl in plugin_path:
                subtree = subtree[1].setdefault(name, [None, OrderedDict(), transl])
            if plugin_path:
                subtree[0] = func
        # fill treestore with plugins
        def fill_treestore(parent, tree):
            for name, [func, subtree, transl] in tree.items():
                item = QStandardItem(transl)
                item.setData(func)
                parent.appendRow(item)
                fill_treestore(item, subtree)
        fill_treestore(self.treestore, ptree)
        # create widgets in the sidepane to display the plugins
        for i, (name, [func, subtree, transl]) in enumerate(ptree.items()):
            button = QPushButton(self.ui.box_sidepane)
            button.setText(transl)
            button.setFlat(True)
            button.setFocusPolicy(Qt.TabFocus)
            self.ui.layout_sidepane.addWidget(button)
            obsc = lambda i: lambda: self.on_button_sidepane_clicked(i)
            button.clicked.connect(obsc(i))
            
            treeview = QTreeView(self.ui.box_sidepane)
            treeview.setFrameShape(QFrame.NoFrame)
            treeview.setEditTriggers(QAbstractItemView.NoEditTriggers)
            treeview.setUniformRowHeights(True)
            treeview.setAnimated(True)
            treeview.setHeaderHidden(True)
            treeview.hide()
            treeview.setModel(self.treestore)
            index = self.treestore.index(i, 0)
            treeview.setRootIndex(index)
            self.ui.layout_sidepane.addWidget(treeview)
            treeview.activated.connect(self.on_treeview_activated)
            
            self.plugin_group_widgets.append((button, treeview))
        # visibility
        self.set_active_plugin_group(0 if self.plugin_group_widgets else None)
        if load:
            self.hide_incompatible_plugins()
        
    def hide_incompatible_plugins(self):
        def hide_row(treeview, index):
            func_idx = index.data(Qt.UserRole + 1)
            if func_idx is not None:
                try:
                    self.plugin_helper.get_function(self.game.current_state.model, func_idx)
                except pluginlib.PluginModelCompatError:
                    return True
            hide_all = func_idx is None
            rows = self.treestore.rowCount(index)
            for r in range(rows):
                hide = hide_row(treeview, self.treestore.index(r, 0, index))
                treeview.setRowHidden(r, index, hide)
                hide_all = hide_all and hide
            return hide_all
        for i, (button, treeview) in enumerate(self.plugin_group_widgets):
            index = treeview.rootIndex()
            if hide_row(treeview, index):
                button.hide()
                if i == self.active_plugin_group:
                    self.set_active_plugin_group(0)
            else:
                button.show()
        
    def set_active_plugin_group(self, i):
        if self.active_plugin_group is not None:
            unused_button, treeview = self.plugin_group_widgets[self.active_plugin_group]
            treeview.hide()
        if i is not None:
            unused_button, treeview = self.plugin_group_widgets[i]
            treeview.show()
        self.active_plugin_group = i
        
    def load_current_game(self):
        mtype = settings.game.type
        size = settings.games[mtype].size
        model = Model(mtype, size)
        self.game.load(self.games_file, model)
        self.cube_area.set_glmodel_full(model, self.game.current_state.rotations)
        self.update_ui()
        
    def load_game(self, mtype, size):
        self.save_game()
        if self.is_animating():
            return
        assert mtype is not None
        model = Model(mtype, size)
        settings.game.type = model.type
        settings.games[model.type].size = model.size
        self.game.load(self.games_file, model)
        self.unsolved = False
        self.cube_area.set_glmodel_full(model, self.game.current_state.rotations)
        self.update_ui()
        self.hide_incompatible_plugins()
        
    def save_game(self):
        self.game.save(self.games_file)
        
    def new_game(self, mtype=None, size=None, solved=False):
        if self.is_animating():
            return
        self.game.new_game(None, solved)
        self.unsolved = not solved
        self.cube_area.set_glmodel_full(None, self.game.current_state.rotations)
        self.update_ui()
        
    def is_animating(self):
        return self.cube_area.animation_active
        
    def start_animation(self, move_data, stop_after, animtype):
        if move_data:
            blocks = self.game.current_state.identify_rotation_blocks(
                                move_data.axis, move_data.slice)
            self.cube_area.animate_rotation(move_data, blocks, stop_after)
            self.playbarstate.playing(self.game.mark_before)
            self.animtype = animtype
            return True
        return False
        
    def update_statusbar(self):
        '''This function must be called before any action that change the move queue length
        to reflect total count of moves and after each single move'''
        
        if self.cube_area.editing_model:
            mesg = _('Press the Esc key to exit Edit Mode')
            self.unsolved = False
        else:
            current = self.game.move_sequence.current_place
            total = self.game.move_sequence.queue_length
            solved = self.game.current_state.is_solved()
            
            model_text = str(self.game.current_state.model)
            # substitution for {move_text} in statusbar text
            move_text = ngettext("{current} / {total} move",
                                 "{current} / {total} moves",
                                 total).format(current=current, total=total)
            # substitution for {solved_text} in statusbar text
            solved_text = _('solved') if solved else _('not solved')
            
            # statusbar text
            mesg = _('{model_text}, {move_text}, {solved_text}').format(
                            model_text=model_text,
                            move_text=move_text,
                            solved_text=solved_text)
            if self.unsolved and solved:
                self.unsolved = False
                self.show_solved_message()
        self.status_text.setText(mesg)
        
    def update_ui(self):
        # update toolbar
        if self.game.move_sequence.at_start():
            if self.game.move_sequence.at_end():
                self.playbarstate.empty()
            else:
                self.playbarstate.first()
        else:
            if self.game.move_sequence.at_end():
                self.playbarstate.last(self.game.move_sequence.is_mark_after(-1))
            else:
                self.playbarstate.mid(self.game.move_sequence.is_mark_current())
                
        # update formula
        code, pos = self.game.move_sequence.format(self.game.current_state.model)
        self.set_edit_moves(code, pos)
        
        self.update_statusbar()
        
    def show_solved_message(self):
        dialog = QMessageBox(self)
        dialog.setWindowTitle(_(config.APPNAME))
        dialog.setText(_('Congratulations, you have solved the puzzle!'))
        dialog.setIcon(QMessageBox.Information)
        dialog.setStandardButtons(QMessageBox.Close)
        self.cube_area.set_std_cursor()
        QTimer.singleShot(100, dialog.exec_)
        
    def error_dialog(self, message, delay=0):
        dialog = QMessageBox(self)
        dialog.setWindowTitle(_(config.APPNAME))
        dialog.setText(message)
        dialog.setIcon(QMessageBox.Warning)
        dialog.setStandardButtons(QMessageBox.Close)
        self.cube_area.set_std_cursor()
        if delay:
            QTimer.singleShot(delay, dialog.exec_)
        else:
            dialog.exec_()
        
    def set_progress(self, step, message=None, value_max=None):
        if self.progress_dialog is None:
            self.progress_dialog = dialogs.ProgressDialog(parent=self)
        canceled = self.progress_dialog.tick(step, message, value_max)
        from PyQt5.QtWidgets import qApp
        qApp.processEvents()
        return canceled
        
    def end_progress(self):
        if self.progress_dialog is not None:
            self.progress_dialog.done()
            
    ### application handlers ###
    
    def resizeEvent(self, event):
        settings.window.size = event.size().width(), event.size().height()
        sizes = self.ui.splitter.sizes()
        if sum(sizes) > 0:  # ignore the first resize event where sizes==[0,0]
            settings.window.divider = sizes[0]
        QMainWindow.resizeEvent(self, event)
        
    def closeEvent(self, event):
        try:
            dialogs.Dialog.delete()
            self.cube_area.stop()
            settings.disconnect()
            self.save_game()
            settings.flush()
        finally:
            QMainWindow.closeEvent(self, event)
        
    def on_settings_changed(self, key):
        if key == 'draw.accels':
            self.move_keys = self.get_move_key_dict()
            
    def on_settings_error(self, message):
        self.status_text.setText(message)
        
    MODIFIER_MASK = int(Qt.ShiftModifier | Qt.ControlModifier | Qt.KeypadModifier)
    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Escape:
            self.cube_area.set_editing_mode(False)
            self.update_ui()
            return
        try:
            move = self.move_keys[event.key() | int(event.modifiers()) & self.MODIFIER_MASK]
        except KeyError:
            event.ignore()
            QMainWindow.keyPressEvent(self, event)
            return
        else:
            if self.is_animating() and self.animtype != AnimType.KEY:
                return
            if not self.is_animating():
                self.game.move_sequence.truncate()
            self.game.move_sequence.parse(move, len(move), self.game.current_state.model)
            self.update_ui()
            if not self.is_animating():
                move_data = self.game.step_next()
                self.start_animation(move_data, stop_after=False, animtype=AnimType.KEY)
            
    ### GL area handlers ###
    
    def on_cubearea_animation_ending(self, last_move):
        self.cube_area.set_transformations(self.game.current_state.rotations)
        
        if not last_move:
            # go again
            self.update_statusbar()
            if self.animtype == AnimType.BWD:
                move_data = self.game.step_back()
            else:
                move_data = self.game.step_next()
            if self.start_animation(move_data, stop_after=False, animtype=self.animtype):
                return
        self.cube_area.animation_end()
        self.update_ui()
            
    def on_cubearea_request_rotation(self, maxis, mslice, mdir):
        self.game.set_next((maxis, mslice, mdir))
        self.update_statusbar()
        move_data = self.game.step_next()
        self.start_animation(move_data, stop_after=False, animtype=AnimType.NEW)
        
    def on_cubearea_request_swap_blocks(self, blockpos, maxis, mslice, mdir):
        self.game.swap_block(blockpos, maxis, mslice, mdir)
        self.cube_area.set_transformations(self.game.current_state.rotations)
        
    def on_cubearea_request_rotate_block(self, blockpos, rdir):
        self.game.rotate_block(blockpos, rdir)
        self.cube_area.set_transformations(self.game.current_state.rotations)
        
    def on_cubearea_drop_color(self, blockpos, facesym, colorname):
        if blockpos < 0:
            settings.theme.bgcolor = colorname
        else:
            colornum = self.game.current_state.get_colornum(blockpos, facesym)
            facekey = self.game.current_state.model.facekeys[colornum]
            settings.theme.faces[facekey].color = colorname
            
    def on_cubearea_drop_file(self, blockpos, facesym, filename):
        colornum = self.game.current_state.get_colornum(blockpos, facesym)
        facekey = self.game.current_state.model.facekeys[colornum]
        settings.theme.faces[facekey].image = filename
        
    def on_cubearea_debug_info(self, mode, info):
        if mode == 'fps':
            text = "FPS %.1f" % info
        elif mode == 'pick':
            pos = self.cube_area.pickdata.blockpos
            cube = self.game.current_state
            idx, rot = cube._blocksr[pos] #FIXME: access to protected member
            color = cube.get_colorsymbol(pos, self.cube_area.pickdata.symbol)
            angle = self.cube_area.pickdata.angle and format(self.cube_area.pickdata.angle, '.1f')
            text = ('idx {idx}, pos {pos} {indices}, rot {rot!r}\n'
                    'face {0.symbol} ({face}), color {color}\n'
                    'axis {0.maxis}, slice {0.mslice}, dir {0.mdir}, angle {angle}\n'
                    'mouse {1[0]}, {1[1]}').format(self.cube_area.pickdata, self.cube_area.mouse_xy,
                    idx=idx, pos=pos, indices=cube.model.cell_indices[pos], rot=rot,
                    color=color, angle=angle, **info)
        else:
            text = 'unknown mode: ' + mode
        self.ui.label_debug_text.setText(text)
        
    ###
    
    @Slot(int, int)
    def on_splitter_splitterMoved(self, pos, index):    # pylint: disable=R0201
        if index == 1:
            settings.window.divider = pos
            
    ### sidepane handlers ###
    
    def on_button_sidepane_clicked(self, i):
        self.set_active_plugin_group(i)
        
    def on_treeview_activated(self, index):
        if self.is_animating():
            return
        func_idx = index.data(Qt.UserRole + 1)
        if func_idx is None:
            return
        if self.game.current_state.model is empty_model:
            return
        try:
            func = self.plugin_helper.get_function(self.game.current_state.model, func_idx)
        except pluginlib.PluginModelCompatError as e:
            self.error_dialog(e.args[0])
            return
        # model accepted, now run the plugin function
        game = self.game.copy()
        try:
            func(game)
        except pluginlib.PluginSolverAbort as e:
            self.error_dialog(e.args[0])
            return
        if game.plugin_mode == 'append':
            # append moves
            self.unsolved = False
            position = self.game.move_sequence.mark_and_extend(game.move_sequence)
            changed = position >= 0
            if changed:
                self.game.goto_next_pos(position)
                changed = 'animate'
        elif game.plugin_mode == 'replace':
            # replace game
            self.game.initial_state = game.initial_state
            self.game.current_state = game.current_state
            self.game.move_sequence = game.move_sequence
            self.unsolved = False
            changed = True
        elif game.plugin_mode == 'challenge':
            # challenge
            self.game.initial_state = game.initial_state
            self.game.current_state = game.initial_state.copy()
            self.game.move_sequence.reset()
            self.unsolved = True
            changed = True
        else:
            assert False
        if changed:
            self.cube_area.set_transformations(self.game.current_state.rotations)
            self.update_ui()
            if changed == 'animate':
                move_data = self.game.step_next()
                self.start_animation(move_data, stop_after=False, animtype=AnimType.NEW)
        self.cube_area.update()
        
    ### action handlers ###
    
    @Slot()
    def on_action_new_triggered(self):
        self.new_game(solved=False)
    @Slot()
    def on_action_new_solved_triggered(self):
        self.new_game(solved=True)
    @Slot()
    def on_action_selectmodel_triggered(self):
        dialog = dialogs.SelectModelDialog.run(parent=self)
        if dialog:
            dialog.response_ok.connect(self.load_game)
    @Slot()
    def on_action_quit_triggered(self):
        self.close()
    @Slot()
    def on_action_preferences_triggered(self):
        dialogs.PreferencesDialog.run(parent=self)
    @Slot(bool)
    def on_action_toolbar_toggled(self, checked):
        self.ui.toolbar.setVisible(checked)
        settings.window.toolbar = checked
    @Slot(bool)
    def on_action_editbar_toggled(self, checked):
        self.ui.frame_editbar.setVisible(checked)
        settings.window.editbar = checked
    @Slot(bool)
    def on_action_statusbar_toggled(self, checked):
        self.ui.statusbar.setVisible(checked)
        settings.window.statusbar = checked
    @Slot()
    def on_action_reset_rotation_triggered(self):
        self.cube_area.reset_rotation()
    @Slot()
    def on_action_help_triggered(self):
        dialogs.HelpDialog.run(parent=self)
    @Slot()
    def on_action_info_triggered(self):
        about = dialogs.AboutDialog(parent=self)
        about.run()
        
    @Slot()
    def on_action_rewind_triggered(self):
        if self.is_animating():
            self.cube_area.stop_requested = True
            if self.animtype == AnimType.BWD:
                self.game.step_next() # undo the already applied move
            self.game.goto_prev_mark()
            self.cube_area.animate_abort()
        else:
            needs_update = self.game.goto_prev_mark()
            self.cube_area.set_transformations(self.game.current_state.rotations)
            if needs_update:
                self.cube_area.update()
                self.update_ui()
    @Slot()
    def on_action_previous_triggered(self):
        if self.is_animating():
            if self.animtype == AnimType.BWD:
                # request another BWD move
                self.cube_area.stop_requested = False
            else:
                self.cube_area.stop_requested = True
                self.game.step_back()
            self.cube_area.animate_abort()
            self.cube_area.stop_requested = True
        else:
            move_data = self.game.step_back()
            self.start_animation(move_data, stop_after=True, animtype=AnimType.BWD)
    @Slot()
    def on_action_stop_triggered(self):
        if self.is_animating():
            if self.cube_area.stop_requested:
                self.cube_area.animate_abort()
            else:
                self.cube_area.stop_requested = True
    @Slot()
    def on_action_play_triggered(self):
        if not self.is_animating():
            move_data = self.game.step_next()
            self.start_animation(move_data, stop_after=False, animtype=AnimType.FWD)
    @Slot()
    def on_action_next_triggered(self):
        if self.is_animating():
            sr = self.cube_area.stop_requested
            if self.animtype == AnimType.BWD:
                self.cube_area.stop_requested = True
                self.game.step_next()
            else:
                self.cube_area.stop_requested = False
            self.cube_area.animate_abort()
            self.cube_area.stop_requested = sr
        else:
            move_data = self.game.step_next()
            self.start_animation(move_data, stop_after=True, animtype=AnimType.FWD)
    @Slot()
    def on_action_forward_triggered(self):
        if self.is_animating():
            self.cube_area.stop_requested = True
            if self.animtype != AnimType.BWD:
                self.game.step_back() # undo the already applied move
            self.game.goto_next_mark()
            self.cube_area.animate_abort()
        else:
            self.game.goto_next_mark()
            self.cube_area.set_transformations(self.game.current_state.rotations)
            self.cube_area.update()
            self.update_ui()
    @Slot()
    def on_action_add_mark_triggered(self):
        self.game.move_sequence.mark_current(True)
        self.update_ui()
    @Slot()
    def on_action_remove_mark_triggered(self):
        self.game.move_sequence.mark_current(False)
        self.update_ui()
        
    @Slot()
    def on_action_initial_state_triggered(self):
        if self.is_animating():
            return
        self.game.set_as_initial_state()
        self.update_ui()
        
    ### edit formula handlers ###
    
    def on_action_jump_to_editbar_triggered(self):
        self.edit_moves.setFocus()
        
    def on_edit_moves_returnPressed(self):
        if self.is_animating():
            self.cube_area.animate_abort(update=False)
        code = self.edit_moves.text()
        pos = self.edit_moves.cursorPosition()
        self.game.set_code(code, pos)
        self.cube_area.set_transformations(self.game.current_state.rotations)
        self.cube_area.update()
        self.update_ui()
        
    def on_edit_moves_nextword(self):
        code = self.edit_moves.text()
        pos = self.edit_moves.cursorPosition()
        moves, mpos, cpos = self.game.move_sequence.new_from_code(code, pos,
                                        self.game.current_state.model)
        moves.current_place = mpos
        if cpos <= pos:
            moves.advance()
        unused_code, pos = moves.format(self.game.current_state.model)
        self.edit_moves.setCursorPosition(pos)
        
    def on_edit_moves_prevword(self):
        code = self.edit_moves.text()
        pos = self.edit_moves.cursorPosition()
        moves, mpos, cpos = self.game.move_sequence.new_from_code(code, pos,
                                        self.game.current_state.model)
        moves.current_place = mpos
        if cpos >= pos:
            moves.retard()
        unused_code, pos = moves.format(self.game.current_state.model)
        self.edit_moves.setCursorPosition(pos)
        
    def on_edit_moves_swapnext(self):
        code = self.edit_moves.text()
        pos = self.edit_moves.cursorPosition()
        moves, mpos, cpos = self.game.move_sequence.new_from_code(code, pos,
                                        self.game.current_state.model)
        moves.current_place = mpos
        if pos < cpos:
            moves.retard()
        moves.swapnext()
        code, pos = moves.format(self.game.current_state.model)
        self.edit_moves.setText(code)
        self.edit_moves.setCursorPosition(pos)
        
    def on_edit_moves_swapprev(self):
        code = self.edit_moves.text()
        pos = self.edit_moves.cursorPosition()
        moves, mpos, cpos = self.game.move_sequence.new_from_code(code, pos,
                                        self.game.current_state.model)
        moves.current_place = mpos
        if pos < cpos:
            moves.retard()
        moves.swapprev()
        code, pos = moves.format(self.game.current_state.model)
        self.edit_moves.setText(code)
        self.edit_moves.setCursorPosition(pos)
        
    @Slot()
    def on_button_edit_clear_clicked(self):
        self.edit_moves.setText('')
        self.on_edit_moves_returnPressed()
        
    @Slot()
    def on_button_edit_exec_clicked(self):
        self.on_edit_moves_returnPressed()
        
    def set_edit_moves(self, code, pos):
        self.edit_moves.setText(code)
        self.edit_moves.setCursorPosition(pos)
        
    def on_action_edit_cube_triggered(self):
        self.cube_area.animate_abort(update=False)
        self.game.goto_start()
        if DEBUG_ROTATE:
            self.game.current_state.debug_blocksymbols(allsyms=True)
        self.cube_area.set_transformations(self.game.current_state.rotations)
        self.cube_area.set_editing_mode(True)
        self.update_ui()
        
        
class PlaybarState:
    def __init__(self, play_actions):
        self.play_button_list = play_actions
        self.empty()
        
    def set_toolbar_state(self, sflags, vflags, mark):
        if mark:
            vflags ^= 0b11
        for a in reversed(self.play_button_list):
            a.setEnabled(sflags & 1)
            a.setVisible(vflags & 1)
            sflags >>= 1
            vflags >>= 1
            
    # pylint: disable=C0321,C0326
    def empty(self):          self.set_toolbar_state(0b00000000, 0b11011101, False)
    def first(self):          self.set_toolbar_state(0b00011100, 0b11011101, False)
    def mid(self, mark):      self.set_toolbar_state(0b11011111, 0b11011110, mark)
    def last(self, mark):     self.set_toolbar_state(0b11000011, 0b11011110, mark)
    def playing(self, mark):  self.set_toolbar_state(0b11101100, 0b11101110, mark)
    # pylint: enable=C0321
    

