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

#  Pybik -- A 3 dimensional magic cube game.
#  Copyright © 2009-2013  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/>.

# Ported from GNUbik
# Original filename: widget-set.c
# Original copyright and license: 2003, 2004  John Darrington, GPL3+



from collections import OrderedDict

# pylint: disable=W0614,W0401
from PyQt4.QtCore import *
from PyQt4.QtCore import pyqtSignal as Signal, pyqtSlot as Slot
from PyQt4.QtGui import *

from .debug import *
# pylint: enable=W0614,W0401
from . import config
from . import dialogs
from . import plugins
from .model import models
from . import gamestate
from . import drawingarea
from .settings import settings

try:
    _, ngettext
except NameError:
    debug('gettext not found')
    _ = lambda string: string
    ngettext = lambda singular, plural, cnt: singular if cnt == 1 else plural
    
    
class AnimType (object): # pylint: disable=R0903
    NONE, FWD, BWD, KEY, NEW = list(range(5))
    
    
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):
        QMainWindow.__init__(self)
        self.ui = dialogs.UI(self, 'main.ui')
        from .ui.main import retranslate
        retranslate(self)
        
        # UI
        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.IMAGE_FILE))
        self.ui.button_edit_exec.setIcon(QIcon.fromTheme('system-run'))
        self.ui.button_edit_clear.setIcon(QIcon.fromTheme('edit-clear'))
        self.ui.action_new.setIcon(QIcon.fromTheme('document-new'))
        self.ui.action_new_solved.setIcon(QIcon.fromTheme('document-new'))
        #self.ui.action_selectmodel.setIcon(QIcon.fromTheme('document-properties'))
        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_selectmodel.setShortcut(QKeySequence(settings.action.selectmodel))
        self.ui.action_initial_state.setShortcut(QKeySequence(settings.action.initial_state))
        self.ui.action_reset_rotation.setShortcut(QKeySequence(settings.action.reset_rotation))
        self.ui.action_invert_moves.setShortcut(QKeySequence(settings.action.invert_moves))
        self.ui.action_reload_scripts.setShortcut(QKeySequence(settings.action.reload_scripts))
        self.ui.action_preferences.setShortcut(QKeySequence(settings.action.preferences))
        self.ui.action_normalize_complete_rotations.setShortcut(
                                        QKeySequence(settings.action.normalize_complete_rotations))
        
        # 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_text.connect(self.on_cubearea_debug_text)
        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_model = QAction(self)
        self.action_edit_model.setObjectName('action_edit_model')
        self.action_edit_model.setShortcut(QKeySequence(settings.action.edit_model))
        self.action_edit_model.triggered.connect(self.on_action_edit_model_triggered)
        self.addAction(self.action_edit_model)
        self.action_reload_shader = QAction(self)
        self.action_reload_shader.setObjectName('action_reload_shader')
        self.action_reload_shader.setShortcut(QKeySequence(settings.action.reload_shader))
        self.action_reload_shader.triggered.connect(self.on_action_reload_shader_triggered)
        self.addAction(self.action_reload_shader)
        
        # plugins
        self.treestore = QStandardItemModel()
        self.selected_tree_path = None
        self.active_script_group = 0
        self.plugin_helper = plugins.PluginHelper()
        self.add_scripts_to_sidepane(False)
        QTimer.singleShot(0, self.add_scripts_to_sidepane)
        
        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.gamestate = gamestate.GameState()
        QTimer.singleShot(0, self.load_game)
        self.update_ui()
        
        self.resize(*settings.window.size)
        divider_position = settings.window.divider
        self.show()
        sizes = self.ui.splitter.sizes()
        self.ui.splitter.setSizes([divider_position, sum(sizes)-divider_position])
        
    def create_cube_area(self, opts):
        if DEBUG_THREAD:
            cube_area = drawingarea.CubeAreaThreaded(opts)
        else:
            cube_area = drawingarea.CubeArea(opts)
        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)
        cube_area.start()
        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)
        playbarstate = PlaybarState(play_actions)
        return playbarstate
        
    ### helper functions ###
    
    @staticmethod
    def keyval_from_name(keystr):
        key = QKeySequence(keystr)
        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_scripts_to_sidepane(self, load=True):
        self.ui.treeview.setModel(None)
        self.treestore.clear()
        for widget in self.ui.box_sidepane.children():
            if isinstance(widget, QPushButton):
                self.ui.layout_sidepane.removeWidget(widget)
                widget.deleteLater()
        if load:
            algorithms = self.plugin_helper.load_plugins()
            if self.plugin_helper.error_messages:
                self.error_dialog('\n\n'.join(self.plugin_helper.error_messages))
        else:
            algorithms = []
        ptree = OrderedDict()
        for plugin_path, func in algorithms:
            subtree = [None, ptree]
            for name in plugin_path:
                subtree = subtree[1].setdefault(name, [None, OrderedDict()])
            if plugin_path:
                subtree[0] = func
        def fill_treeview(parent, tree):
            for name, [func, subtree] in tree.items():
                item = QStandardItem(_(name))
                item.setData(func)
                parent.appendRow(item)
                fill_treeview(item, subtree)
        for i, (name, [func, subtree]) in enumerate(ptree.items()):
            button = QPushButton(self.ui.box_sidepane)
            button.setText(_(name))
            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))
        fill_treeview(self.treestore, ptree)
        self.set_active_script_group(self.active_script_group)
        
    def set_active_script_group(self, i):
        self.ui.layout_sidepane.insertWidget(i+1, self.ui.treeview)
        self.ui.treeview.setModel(self.treestore)
        index = self.treestore.index(i, 0)
        self.ui.treeview.setRootIndex(index)
        self.active_script_group = i
        
    def load_game(self):
        mtype, size, blocks = settings.game.type, settings.game.size, settings.game.blocks
        Model = models[mtype]
        mirror_distance = settings.draw.mirror_distance if settings.draw.mirror_faces else None
        model = Model(size, mirror_distance)
        self.gamestate.set_state(model, blocks, settings.game.moves, settings.game.position)
        self.cube_area.set_model(model, self.gamestate.current_cube_state.blocks)
        self.update_ui()
        
    def new_game(self, mtype=None, size=None, solved=False):
        if self.is_animating():
            return
        if mtype is None:
            model = self.gamestate.current_cube_state.model
            if size is None:
                size = model.sizes
            Model = model.__class__
        else:
            Model = models[mtype]
            assert size is not None
        mirror_distance = settings.draw.mirror_distance if settings.draw.mirror_faces else None
        model = Model(size, mirror_distance)
        self.gamestate.new_game(model, solved)
        self.unsolved = not solved
        self.cube_area.set_model(model, self.gamestate.current_cube_state.blocks)
        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.gamestate.current_cube_state.identify_rotation_blocks(
                                move_data.axis, move_data.slice)
            self.cube_area.animate_rotation(move_data, blocks, stop_after)
            self.playbarstate.playing(self.gamestate.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.gamestate.all_moves.current_place
            total = self.gamestate.all_moves.queue_length
            solved = self.gamestate.current_cube_state.is_solved()
            
            model_text = str(self.gamestate.current_cube_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
                QTimer.singleShot(0, self.show_solved_message)
        self.status_text.setText(mesg)
        
    def update_ui(self):
        # update toolbar
        if self.gamestate.all_moves.at_start():
            if self.gamestate.all_moves.at_end():
                self.playbarstate.empty()
            else:
                self.playbarstate.first()
        else:
            if self.gamestate.all_moves.at_end():
                self.playbarstate.last(self.gamestate.all_moves.is_mark_after(-1))
            else:
                self.playbarstate.mid(self.gamestate.all_moves.is_mark_current())
                
        # update formula
        code, pos, markup = self.gamestate.all_moves.format(
                                    self.gamestate.current_cube_state.model)
        self.set_edit_moves(code, pos, markup)
        
        self.update_statusbar()
        
    def show_solved_message(self):
        dialog = QMessageBox(self)
        dialog.setWindowTitle(_(config.APPNAME))
        mtype = _(self.gamestate.current_cube_state.model.type)
        dialog.setText(_('Congratulations, you have solved the puzzle!'))
        dialog.setIcon(QMessageBox.Information)
        dialog.setStandardButtons(QMessageBox.Close)
        self.cube_area.set_std_cursor()
        dialog.exec_()
        
    def error_dialog(self, message):
        '''Popup an error dialog box'''
        dialog = QMessageBox(self)
        dialog.setWindowTitle(_(config.APPNAME))
        dialog.setText(message)
        dialog.setIcon(QMessageBox.Warning)
        dialog.setStandardButtons(QMessageBox.Close)
        self.cube_area.set_std_cursor()
        dialog.exec_()
        
    def set_progress(self, step, message=None, value_max=None):
        if self.progress_dialog is None:
            self.progress_dialog = dialogs.ProgressDialog(self)
        canceled = self.progress_dialog.tick(step, message, value_max)
        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()
            model, blocks, moves, position = self.gamestate.get_state()
            try:
                mtype = models.index(model.__class__)
            except ValueError:
                print('unknown model')
            else:
                settings.keystore.changed.disconnect(self.on_settings_changed)
                settings.keystore.error.disconnect(self.on_settings_error)
                settings.game.blocks = blocks
                settings.game.moves = moves
                settings.game.position = position
                settings.game.type = mtype
                settings.game.size = model.sizes
            finally:
                settings.close()
            # Needed for PyQt4 to prevent warning:
            # QObject::startTimer: QTimer can only be used with threads started with QThread
            self.ui.treeview.setModel(None)
        finally:
            QMainWindow.closeEvent(self, event)
        
    def on_settings_changed(self, key):
        if key in ['draw.mirror_faces', 'draw.mirror_distance']:
            sizes = self.gamestate.initial_cube_state.model.sizes
            Model = self.gamestate.initial_cube_state.model.__class__
            mirror_distance = settings.draw.mirror_distance if settings.draw.mirror_faces else None
            model = Model(sizes, mirror_distance)
            self.gamestate.set_model(model)
            self.cube_area.set_model(model)
        elif 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.gamestate.all_moves.truncate()
            self.gamestate.all_moves.parse(move, len(move), self.gamestate.current_cube_state.model)
            self.update_ui()
            if not self.is_animating():
                move_data = self.gamestate.request_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.gamestate.current_cube_state)
        
        if not last_move:
            # go again
            self.update_statusbar()
            if self.animtype == AnimType.BWD:
                move_data = self.gamestate.request_back()
            else:
                move_data = self.gamestate.request_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.gamestate.request_rotation(maxis, mslice, mdir)
        self.update_statusbar()
        move_data = self.gamestate.request_next()
        self.start_animation(move_data, stop_after=False, animtype=AnimType.NEW)
        
    def on_cubearea_request_swap_blocks(self, blockpos, maxis, mslice, mdir):
        self.gamestate.swap_block(blockpos, maxis, mslice, mdir)
        self.cube_area.set_transformations(self.gamestate.current_cube_state)
        
    def on_cubearea_request_rotate_block(self, blockpos, rdir):
        self.gamestate.rotate_block(blockpos, rdir)
        self.cube_area.set_transformations(self.gamestate.current_cube_state)
        
    def on_cubearea_drop_color(self, blocknum, facesym, colorname):
        if blocknum < 0:
            settings.theme.bgcolor = colorname
        else:
            colornum = self.gamestate.current_cube_state.get_colornum(blocknum, facesym)
            settings.theme.face[colornum].color = colorname
            
    def on_cubearea_drop_file(self, blocknum, facesym, filename):
        #TODO: background images
        colornum = self.gamestate.current_cube_state.get_colornum(blocknum, facesym)
        settings.theme.face[colornum].image = filename
        
    def on_cubearea_debug_text(self, text):
        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_script_group(i)
        
    @Slot(QModelIndex)
    def on_treeview_activated(self, index):
        if self.is_animating():
            return
        func = index.data(Qt.UserRole + 1)
        if func is None:
            return
        state, moves = self.plugin_helper.call(self, func)
        if state is None:
            self.unsolved = False
        else:
            self.gamestate.initial_cube_state = state
            self.gamestate.current_cube_state = state.copy()
            self.gamestate.all_moves.reset()
            self.unsolved = True
        position = self.gamestate.all_moves.mark_and_extend(moves)
        if position >= 0:
            self.gamestate.do_fast_forward(position)
            self.cube_area.set_transformations(self.gamestate.current_cube_state)
            self.update_ui()
            move_data = self.gamestate.request_next()
            self.start_animation(move_data, stop_after=False, animtype=AnimType.NEW)
        elif state is not None:
            self.cube_area.set_transformations(self.gamestate.current_cube_state)
            self.update_ui()
        self.cube_area.update()
        
    ### action handlers ###
    
    @Slot()
    def on_action_new_activated(self):
        self.new_game(solved=False)
    @Slot()
    def on_action_new_solved_activated(self):
        self.new_game(solved=True)
    @Slot()
    def on_action_selectmodel_activated(self):
        dialog = dialogs.SelectModelDialog.run(self)
        if dialog:
            def response_ok(mtype, size, solved):
                self.new_game(mtype, size, solved)
            dialog.response_ok.connect(response_ok)
    @Slot()
    def on_action_quit_activated(self):
        self.close()
    @Slot()
    def on_action_preferences_activated(self):
        dialogs.PreferencesDialog.run(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_activated(self):
        self.cube_area.reset_rotation()
    @Slot()
    def on_action_info_activated(self):
        about = dialogs.AboutDialog(self)
        about.run()
        
    @Slot()
    def on_action_rewind_activated(self):
        if self.is_animating():
            self.cube_area.stop_requested = True
            if self.animtype == AnimType.BWD:
                self.gamestate.request_next() # undo the already applied move
            self.gamestate.do_rewind_to_mark()
            self.cube_area.animate_abort()
        else:
            needs_update = self.gamestate.do_rewind_to_mark()
            self.cube_area.set_transformations(self.gamestate.current_cube_state)
            if needs_update:
                self.cube_area.update()
                self.update_ui()
    @Slot()
    def on_action_previous_activated(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.gamestate.request_back()
            self.cube_area.animate_abort()
            self.cube_area.stop_requested = True
        else:
            move_data = self.gamestate.request_back()
            self.start_animation(move_data, stop_after=True, animtype=AnimType.BWD)
    @Slot()
    def on_action_stop_activated(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_activated(self):
        if not self.is_animating():
            move_data = self.gamestate.request_next()
            self.start_animation(move_data, stop_after=False, animtype=AnimType.FWD)
    @Slot()
    def on_action_next_activated(self):
        if self.is_animating():
            sr = self.cube_area.stop_requested
            if self.animtype == AnimType.BWD:
                self.cube_area.stop_requested = True
                self.gamestate.request_next()
            else:
                self.cube_area.stop_requested = False
            self.cube_area.animate_abort()
            self.cube_area.stop_requested = sr
        else:
            move_data = self.gamestate.request_next()
            self.start_animation(move_data, stop_after=True, animtype=AnimType.FWD)
    @Slot()
    def on_action_forward_activated(self):
        if self.is_animating():
            self.cube_area.stop_requested = True
            if self.animtype != AnimType.BWD:
                self.gamestate.request_back() # undo the already applied move
            self.gamestate.do_fast_forward()
            self.cube_area.animate_abort()
        else:
            self.gamestate.do_fast_forward()
            self.cube_area.set_transformations(self.gamestate.current_cube_state)
            self.cube_area.update()
            self.update_ui()
    @Slot()
    def on_action_add_mark_activated(self):
        self.gamestate.all_moves.mark_current(True)
        self.update_ui()
    @Slot()
    def on_action_remove_mark_activated(self):
        self.gamestate.all_moves.mark_current(False)
        self.update_ui()
        
    @Slot()
    def on_action_initial_state_activated(self):
        if self.is_animating():
            return
        self.gamestate.set_as_initial_state()
        self.update_ui()
    @Slot()
    def on_action_reload_scripts_activated(self):
        self.add_scripts_to_sidepane()
    @Slot()
    def on_action_invert_moves_activated(self):
        if self.is_animating():
            return
        self.gamestate.invert_moves()
        self.cube_area.set_transformations(self.gamestate.current_cube_state)
        self.cube_area.update()
        self.update_ui()
    @Slot()
    def on_action_normalize_complete_rotations_activated(self):
        if self.is_animating():
            return
        self.gamestate.normalize_complete_rotations()
        self.cube_area.set_transformations(self.gamestate.current_cube_state)
        self.cube_area.update()
        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.gamestate.set_from_formula(code, pos)
        self.cube_area.set_transformations(self.gamestate.current_cube_state)
        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.gamestate.all_moves.converter.parse(code, pos,
                                        self.gamestate.current_cube_state.model)
        moves.current_place = mpos
        if cpos <= pos:
            moves.advance()
        code, pos, unused_markup = moves.format(self.gamestate.current_cube_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.gamestate.all_moves.converter.parse(code, pos,
                                        self.gamestate.current_cube_state.model)
        moves.current_place = mpos
        if cpos >= pos:
            moves.retard()
        code, pos, unused_markup = moves.format(self.gamestate.current_cube_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.gamestate.all_moves.converter.parse(code, pos,
                                        self.gamestate.current_cube_state.model)
        moves.current_place = mpos
        if pos < cpos:
            moves.retard()
        moves.swapnext()
        code, pos, unused_markup = moves.format(self.gamestate.current_cube_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.gamestate.all_moves.converter.parse(code, pos,
                                        self.gamestate.current_cube_state.model)
        moves.current_place = mpos
        if pos < cpos:
            moves.retard()
        moves.swapprev()
        code, pos, unused_markup = moves.format(self.gamestate.current_cube_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, unused_markup=None):
        self.edit_moves.setText(code)
        self.edit_moves.setCursorPosition(pos)
        
    def on_action_edit_model_triggered(self):
        self.cube_area.animate_abort(update=False)
        self.gamestate.do_rewind_to_start()
        if DEBUG_ROTATE:
            self.gamestate.current_cube_state.debug_blocksymbols(allsyms=True)
        self.cube_area.set_transformations(self.gamestate.current_cube_state)
        self.cube_area.set_editing_mode(True)
        self.update_ui()
        
    def on_action_reload_shader_triggered(self):
        self.cube_area.reload_shader()
        
class PlaybarState (object):
    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
    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
    

