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

#  Copyright © 2009-2017  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
import math
from collections import namedtuple, OrderedDict

from .debug import (debug, DEBUG, DEBUG_MSG, DEBUG_DRAW, DEBUG_ROTATE, DEBUG_ERROR, DEBUG_MOUSEPOS,
                    DEBUG_LIVEQML, DEBUG_LIVESHADER, DEBUG_LIVEPLUGINS, DEBUG_FUNC,
                    DEBUG_VFPS, DEBUG_EMPTY,
                   )
from . import config
from .settings import settings
from .theme import Theme
from .ext import glarea, qt
from . import pluginlib
from .model import Model, empty_model
from .gamestate import GameState
from .utils import ExceptionMeta

try:
    _, ngettext
except NameError:
    _ = lambda string: string
    ngettext = lambda singular, plural, cnt: singular if cnt == 1 else plural
    
if DEBUG_FUNC:
    from .debug import meta
    Meta = meta(ExceptionMeta)
else:
    Meta = ExceptionMeta
    
    
class AnimType: # pylint: disable=R0903
    __slots__ = ()
    NONE, FWD, BWD, KEY, NEW = range(5)
AnimType = AnimType()
    
    
class MainWindow (metaclass=Meta):
    ### init ###
    def __init__(self, opts):
        self.games_file = opts.games_file
        self.qmlfile = opts.qml_file
        
        # dialogs
        self.preferences_dialog_created = False
        self.progress_dialog = None  # created when needed
        
        # plugins
        self.plugin_helper = pluginlib.PluginHelper()
        self.challenge_open = False
        self.challenge = None
        
        # glarea
        self.theme = Theme()
        self.mirror_distance = None
        self.glpickdata = None
        self.pickdata = None
        self.use_cursor = False
        self.last_mouse_x = -1
        self.last_mouse_y = -1
        self.button_down_background = False
        self.editing_model = False
        self.drop_colorname = None
        self.drop_filename = None
        self.animation_active = False
        self.stop_requested = False
        
        self.animtype = AnimType.NONE
        self.gesture_total = None
        self.move_keys = {}
        self.model = empty_model
        self.game = GameState()
        
        Model.load_index()  # needed by settings
        
        # initialize settings
        version = settings.load(opts.config_file)
        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)
        if version < 3:
            from . import schema
            schema.migrate_2_3(settings)
            
        if opts.demo is not None:
            import importlib
            demo = importlib.import_module(opts.demo)
            demo.run(self)
            
    def cb_before_mainloop(self):
        qt.create_window(self)
        # UI
        fixedshaders = self.gl_read_shader('pick.vert'), self.gl_read_shader('pick.frag')
        if DEBUG_DRAW:
            fixedshaders += self.gl_read_shader('hud.vert'),
        self.theme_init()
        self.ui_update_cursor()
        
        glarea.init_engine()
        self.gl_update_background_color()
        self.gl_prepare_faces_atlas()
        glarea.set_fixedshaders(fixedshaders)
        self.gl_read_shaders()
        glarea.set_antialiasing(qt.get_samples() > 1)
        self.camrotation = glarea.set_rotation_xy(*self.model.default_rotation)
        self.last_camrotation = self.camrotation
        
        self.ui_toolbar_empty()
        qt.load_ui(self.qmlfile)
        self.ui_fill_sidepane()
        self.move_keys = self.get_move_key_dict()
        
        settings.connnect(self.on_settings_changed)
        
        qt.initial_resize()
        
        if DEBUG:
            self.init_live_hacking()
            
    def on_init_finalize(self):
        if DEBUG_EMPTY:
            self.ui_update()
            qt.update_sidepane()
        else:
            self.ui_load_current_game()
            
    def cb_after_mainloop(self):
        qt.destroy_window()
        
    def on_qml_directoryChanged(self):  # only variant qtq
        print('reloading qml')
        qt.load_ui(self.qmlfile)
        self.ui_update()
            
    def on_reload_plugins(self):
        import importlib
        for name, module in sys.modules.items():
            if name.startswith(('pybiklib.plugins.')):
                print('reloading', name)
                importlib.reload(module)
        self.ui_fill_sidepane()
        qt.update_sidepane()
        
    def init_live_hacking(self):
        if DEBUG_LIVEQML:  # only variant qtq
            qt.create_filesystemwatcher(os.path.join(config.UI_DIR, 'qt'), self.on_qml_directoryChanged)
            if os.path.dirname(self.qmlfile) != os.path.join(config.UI_DIR, 'qt'):
                qt.create_filesystemwatcher(self.qmlfile, self.on_qml_directoryChanged)
        if DEBUG_LIVESHADER:
            qt.create_filesystemwatcher(config.SHADER_DIR, self.on_gl_reload_shader)
        if DEBUG_LIVEPLUGINS:
            from . import plugins
            qt.create_filesystemwatcher(config.PLUGINS_DIR, self.on_reload_plugins)
            qt.create_filesystemwatcher(os.path.dirname(plugins.__file__), self.on_reload_plugins)
            
        
    ### helper functions ###
    
    def theme_load_face_image(self, facekey):
        filename = settings['theme.faces',facekey,'image']
        if not self.theme.load_face_image(facekey, filename):
            del settings['theme.faces',facekey,'image']
        
    def theme_init(self):
        for facekey, unused_facename in Model.cache_index['facenames']:
            color = qt.parse_color(settings['theme.faces',facekey,'color'])
            self.theme.faces[facekey].color = color
            self.theme.faces[facekey].imagemode = settings['theme.faces',facekey,'mode']
            self.theme_load_face_image(facekey)
            
    @staticmethod
    def get_move_key_dict():
        return {qt.keyval_from_name(key): move for (move, key) in settings['draw.accels']}
        
    def ui_fill_sidepane(self):
        # get a list of all plugins
        plugins = self.plugin_helper.load_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
        def genplugintree(tree):
            for unused_name, [func, subtree, transl] in tree.items():
                yield transl, func, list(genplugintree(subtree))
        ptree = list(genplugintree(ptree))
        qt.fill_sidepane(ptree)
        
    def on_plugin_test_idx(self, funcidx):
        if funcidx is not None:
            try:
                self.plugin_helper.get_function(self.game.current_state.model, funcidx)
            except pluginlib.PluginModelCompatError:
                return False
        return True
        
    def ui_load_game(self, mtype, size):
        model = Model(mtype, size)
        self.game.load(self.games_file, model)
        self.model = model
        self.camrotation = glarea.set_rotation_xy(*self.model.default_rotation)
        self.gl_update_frustum()
        self.gl_update_gldata()
        self.gl_set_transformations(self.game.current_state.rotations)
        self.gl_update_selection()
        qt.update_drawingarea()
        qt.set_title(_(config.APPNAME) +' – '+ str(self.game.current_state.model))
        self.ui_update()
        qt.update_sidepane()
        return model.type, model.size
        
    def ui_load_current_game(self):
        mtype = settings['game.type']
        size = settings['games',mtype,'size']
        self.ui_load_game(mtype, size)
        
    def ui_load_other_game(self, mtype, size):
        self.ui_save_game()
        if self.animation_active:
            return
        assert mtype is not None
        self.challenge = None
        self.challenge_open = False
        mtype, size = self.ui_load_game(mtype, size)
        settings['game.type'] = mtype
        settings['games',mtype,'size'] = size
        
    on_load_other_game = ui_load_other_game
    
    def ui_save_game(self):
        self.game.save(self.games_file)
        
    def ui_new_game(self, solved=False):
        if self.animation_active:
            return
        self.game.random(count=0 if solved else -1)
        self.challenge_open = not solved
        self.gl_update_gldata()
        self.gl_set_transformations(self.game.current_state.rotations)
        self.gl_update_selection()
        qt.update_drawingarea()
        self.ui_update()
        
    def animation_start(self, move_data, stop_after, animtype):
        if not move_data:
            return False
        self.animation_active = True
        self.animtype = animtype
        self.stop_requested = stop_after
        
        blocks = self.game.current_state.identify_rotation_blocks(
                            move_data.axis, move_data.slice)
        angle_max = 360. / self.model.symmetries[move_data.axis]
        axisx, axisy, axisz = self.model.axes[move_data.axis]
        if move_data.dir:
            glarea.set_animation_start(blocks, -axisx, -axisy, -axisz)
        else:
            glarea.set_animation_start(blocks, axisx, axisy, axisz)
        qt.animate_timer_start(0 if DEBUG_VFPS else 20, angle_max)
        self.gl_update_selection()
        qt.update_drawingarea()
        self.ui_toolbar_playing(self.game.mark_before)
        return True
        
    def ui_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.editing_model:
            mesg = _('Press the Esc key to exit Edit Mode')
            self.challenge_open = False
        else:
            current = self.game.move_sequence.current_place
            total = self.game.move_sequence.queue_length
            solved = self.game.current_state.is_solved()
            
            #TRANSLATORS: substitution for {move_text} in statusbar text
            move_text = ngettext("{current} / {total} move",
                                 "{current} / {total} moves",
                                 total).format(current=current, total=total)
            #TRANSLATORS: substitution for {solved_text} in statusbar text
            solved_text = _('solved') if solved else _('not solved')
            
            #TRANSLATORS: statusbar text
            mesg = _('{move_text}, {solved_text}').format(
                            move_text=move_text,
                            solved_text=solved_text)
            if self.challenge_open and solved:
                self.challenge_open = False
                qt.show_message(_('Congratulations, you have solved the puzzle!'))
        qt.set_status_text(mesg)
        
    def ui_update(self):
        # update toolbar
        if self.game.move_sequence.at_start():
            if self.game.move_sequence.at_end():
                self.ui_toolbar_empty()
            else:
                self.ui_toolbar_first()
        else:
            if self.game.move_sequence.at_end():
                self.ui_toolbar_last(self.game.move_sequence.is_mark_after(-1))
            else:
                self.ui_toolbar_mid(self.game.move_sequence.is_mark_current())
                
        # update formula
        code, pos = self.game.move_sequence.format(self.game.current_state.model)
        qt.set_edit_moves(code, pos)
        
        self.ui_update_statusbar()
        
    def ui_update_progress(self, step, message=None, value_max=None):
        if self.progress_dialog is None:
            from . import dialogs
            self.progress_dialog = dialogs.ProgressDialog()
        canceled = self.progress_dialog.tick(step, message, value_max)
        return canceled
        
    def ui_end_progress(self):
        if self.progress_dialog is not None:
            self.progress_dialog.done()
            
    ### glarea helpers ###
    
    def gl_prepare_faces_atlas(self):
        width, height = 0, 0
        faces = [self.theme.faces[fk] for fk, fn in Model.cache_index['facenames']]
        atlasdata = []
        for fd in faces:
            x = width
            w, h, data = fd.image
            width += w
            height = max(h, height)
            atlasdata.append((data, x, w, h))
        w = h = 64
        while w < width:
            w *= 2
        while h < height:
            h *= 2
        width, height = w, h
        
        for fd, (unused_data, x, w, h) in zip(faces, atlasdata):
            fd.texrect = (x+.5)/width, .5/height, (x+w-.5)/width, (h-.5)/height
        qt.set_atlas_size(width, height)
        for i, (data, x, w, h) in enumerate(atlasdata):
            glarea.set_atlas_data(i, data, x, w, h)
            
    @staticmethod
    def gl_update_background_color():
        glarea.set_background_color(*qt.parse_color_f(settings['theme.bgcolor']))
        
    @staticmethod
    def gl_update_samples():
        if qt.get_samples():
            samples = settings['draw.samples']
            samples = 2**samples
            if samples == 1:
                glarea.set_antialiasing(False)
            else:
                glarea.set_antialiasing(True)
            qt.update_drawingarea()
            
    @staticmethod
    def gl_update_zoom():
        zoom = settings['draw.zoom']
        glarea.set_frustum(0, zoom)
        qt.update_drawingarea()
        
    def gl_update_selection(self):
        if self.animation_active:
            if self.pickdata is not None:
                self.pickdata = None
                self.ui_update_cursor()
            return
        if self.use_cursor:
            qt.set_pick_requested(True)
            qt.update_drawingarea()
            
    def ui_update_cursor(self):
        if not self.use_cursor:
            return
        if self.pickdata is None or self.button_down_background:
            index = -1
        elif self.pickdata.angle is None:
            index = -2
        else:
            index = int((self.pickdata.angle+180) / 22.5 + 0.5) % 16
        qt.set_cursor(index)
        
    def ui_unset_cursor(self):
        if self.use_cursor:
            qt.unset_cursor()
            
    def gl_set_transformations(self, rotations):
        self.rotations = rotations
        glarea.set_transformations(rotations)
        
    def gl_update_gldata(self, editing=False):
        selection_mode = 0 if editing else settings['draw.selection']
        glmodeldata, self.glpickdata = self.model.gl_vertex_data(selection_mode, self.theme, self.mirror_distance)
        glarea.set_data(*glmodeldata)
        use_cursor = selection_mode != 2
        if use_cursor != self.use_cursor:
            if use_cursor:
                self.use_cursor = use_cursor
                self.ui_update_cursor()
            else:
                self.ui_unset_cursor()
                self.use_cursor = use_cursor
                
    def gl_update_frustum(self):
        #TODO: The bounding_sphere_radius is optimised for the far clipping plane,
        #      for the near clipping plane the radius without the mirror_distance
        #      would be sufficient.
        s = max(self.model.sizes) if self.model.sizes else 1
        if settings['draw.mirror_faces']:
            self.mirror_distance = settings['draw.mirror_distance'] * s
            sm = s + self.mirror_distance
        else:
            self.mirror_distance = None
            sm = s
        bounding_sphere_radius = math.sqrt(2*s*s + sm*sm)
        glarea.set_frustum(bounding_sphere_radius, settings['draw.zoom'])
        
    def ui_set_editing_mode(self, enable):
        self.editing_model = enable
        self.gl_update_gldata(editing=enable)
        self.gl_update_selection()
        
    @staticmethod
    def gl_read_shader(filename):
        if DEBUG_MSG or DEBUG_LIVESHADER:
            print('Loading shader:', filename)
        filename = os.path.join(config.SHADER_DIR, filename)
        with open(filename, 'rb') as sfile:
            source = sfile.read()
        return source
        
    @classmethod
    def gl_read_shaders(self):
        shadername = settings['draw.shader_nick']
        shaders = self.gl_read_shader(shadername+'.vert'), self.gl_read_shader(shadername+'.frag')
        glarea.set_shaders(*shaders)
        
    def gl_reload_shader(self):
        self.gl_read_shaders()
        qt.update_drawingarea()
    on_gl_reload_shader = gl_reload_shader
        
    def ui_update_dropped_color(self):
        if self.pickdata is not None:
            pickdata = self.pickdata
            if type(pickdata) is list:
                pickdata = pickdata[0]
            unused_idx, unused_rotnum, rot = self.rotations[pickdata.blockpos]
            colorsym = self.model.face_symbolic_to_face_color(pickdata.symbol, rot)
            colornum = self.model.faces.index(colorsym)
            facekey = self.model.facekeys[colornum]
            settings['theme.faces',facekey,'color'] = self.drop_colorname
        else:
            settings['theme.bgcolor'] = self.drop_colorname
            
    def ui_update_dropped_file(self):
        if self.pickdata is not None:
            pickdata = self.pickdata
            if type(pickdata) is list:
                pickdata = pickdata[0]
            unused_idx, unused_rotnum, rot = self.rotations[pickdata.blockpos]
            colorsym = self.model.face_symbolic_to_face_color(pickdata.symbol, rot)
            colornum = self.model.faces.index(colorsym)
            facekey = self.model.facekeys[colornum]
            settings['theme.faces',facekey,'image'] = self.drop_filename
        else:
            debug('Background image is not supported.')
            
    ### application handlers ###
    
    def on_closing(self):
        settings.disconnect()
        self.ui_save_game()
        settings.flush()
        
    def on_settings_timer_timeout(self):
        settings.flush()
        
    def on_settings_changed(self, key):
        qt.settings_timer_start()
        if key is None:
            return
        if key == 'draw.accels':
            self.move_keys = self.get_move_key_dict()
        elif key == 'draw.speed':
            qt.set_animation_speed()
        elif key == 'draw.shader':
            self.gl_reload_shader()
        elif key == 'draw.samples':
            self.gl_update_samples()
        elif key == 'draw.selection':
            self.gl_update_gldata()
            self.gl_update_selection()
            qt.update_drawingarea()
        elif key in ['draw.mirror_faces', 'draw.mirror_distance']:
            self.gl_update_frustum()
            self.gl_update_gldata()
            self.gl_update_selection()
            qt.update_drawingarea()
        elif key.startswith('theme.faces.'):
            facekey, faceattr = key.split('.')[2:]
            if faceattr == 'color':
                color = qt.parse_color(settings['theme.faces',facekey,'color'])
                self.theme.faces[facekey].color = color
                self.gl_update_gldata()
            elif faceattr == 'image':
                self.theme_load_face_image(facekey)
                self.gl_prepare_faces_atlas()
                self.gl_update_gldata()
            elif faceattr == 'mode':
                self.theme.faces[facekey].imagemode = settings['theme.faces',facekey,'mode']
                self.gl_update_gldata()
            qt.update_drawingarea()
        elif key == 'theme.bgcolor':
            self.gl_update_background_color()
            qt.update_drawingarea()
        elif key == 'draw.zoom':
            self.gl_update_zoom()
        else:
            debug('Unknown settings key changed:', key)
            
    def on_key_pressed(self, key, modifiers, fromdrawingarea):
        if DEBUG_ERROR and key == qt.QtKeys.Key_Period:
            print('DEBUG_ERROR')
            qt.show_message('DEBUG_ERROR from on_key_pressed')
            return True
            
        if fromdrawingarea:
            if modifiers & (qt.QtKeys.ShiftModifier | qt.QtKeys.ControlModifier):
                pass
            elif key == qt.QtKeys.Key_Right:
                self.camrotation_changed(2, 0)
                return True
            elif key == qt.QtKeys.Key_Left:
                self.camrotation_changed(-2, 0)
                return True
            elif key == qt.QtKeys.Key_Up:
                self.camrotation_changed(0, -2)
                return True
            elif key == qt.QtKeys.Key_Down:
                self.camrotation_changed(0, 2)
                return True
                
        if key == qt.QtKeys.Key_Escape:
            self.ui_set_editing_mode(False)
            self.ui_update()
            return True
        try:
            move = self.move_keys[key | modifiers]
        except KeyError:
            return False
        else:
            if self.animation_active and self.animtype != AnimType.KEY:
                return True
            if not self.animation_active:
                self.game.move_sequence.truncate()
            self.game.move_sequence.parse(move, len(move), self.game.current_state.model)
            self.ui_update()
            if not self.animation_active:
                move_data = self.game.step_next()
                self.animation_start(move_data, stop_after=False, animtype=AnimType.KEY)
        return True
            
    ### GL area handlers ###
    
    def animation_stop(self):
        self.animation_active = False
        self.stop_requested = False
        qt.animate_timer_stop()
        
    def animation_ending(self):
        self.gl_set_transformations(self.game.current_state.rotations)
        
        if not self.stop_requested:
            # go again
            self.ui_update_statusbar()
            if self.animtype == AnimType.BWD:
                move_data = self.game.step_back()
            else:
                move_data = self.game.step_next()
            if self.animation_start(move_data, stop_after=False, animtype=self.animtype):
                self.stop_requested = False
                self.gl_update_selection()
                qt.update_drawingarea()
                return
        self.animation_stop()
        self.ui_update()
        self.gl_update_selection()
        qt.update_drawingarea()
            
    on_animation_ending = animation_ending
    
    PickData = namedtuple('PickData', 'maxis mslice mdir blockpos symbol angle')
    
    def gl_resolve_pickindex(self, index):
        if not index:
            return None
        try:
            maxis, mslice, mdir, blockpos, symbol, arrowdir = self.glpickdata[index]
        except IndexError:
            #FIXME: should not happen, but it does
            return None
        
        if maxis is None:
            pickdata = []
            for maxis, mslice, arrowdir in mslice:
                angle = glarea.get_cursor_angle(arrowdir)
                pickdata.append(self.PickData(maxis, mslice, mdir, blockpos, symbol, angle))
            return pickdata
        if arrowdir is None:
            angle = None
        else:
            angle = glarea.get_cursor_angle(arrowdir)
        pickdata = self.PickData(maxis, mslice, mdir, blockpos, symbol, angle)
        return pickdata
        
    def debug_update_pick_text(self):
        if self.pickdata is not None:
            pos = self.pickdata.blockpos
            face = self.model.faces.index(self.pickdata.symbol)
            idx, unused_rotnum, rot = self.rotations[self.pickdata.blockpos]
            colorsym = self.model.face_symbolic_to_face_color(self.pickdata.symbol, rot)
            angle = self.pickdata.angle and format(self.pickdata.angle, '.1f')
            text = ('idx {idx}, pos {pos} {indices}, rot {rot!r}\n'
                    'face {0.symbol} ({face}), color {colorsym}\n'
                    'axis {0.maxis}, slice {0.mslice}, dir {0.mdir}, angle {angle}'
                   ).format(self.pickdata,
                    idx=idx, pos=pos, indices=self.model.cell_indices[pos], rot=rot,
                    colorsym=colorsym, angle=angle, face=face)
            qt.set_debug_text(text)
            
    def mouse_rotate_picked_slice(self, total, reverse):
        pd = self.pickdata
        move = pd and (pd.maxis,  (-1 if total else pd.mslice),  (not pd.mdir if reverse else pd.mdir))
        self.game.set_next(move)
        self.ui_update_statusbar()
        move_data = self.game.step_next()
        self.animation_start(move_data, stop_after=False, animtype=AnimType.NEW)
        
    def mouse_start_gesture(self, x, y, height):
        self.last_mouse_x = x
        self.last_mouse_y = y
        self.last_camrotation = self.camrotation
        glarea.set_pick_position(x, height-y)
        qt.set_pick_requested(True)
        qt.update_drawingarea()
        
    def mouse_get_gesture_move(self, x, y, total):
        pds = self.pickdata
        if not pds:
            return None
        dx = x - self.last_mouse_x
        dy = y - self.last_mouse_y
        if dx == dy == 0:
            return None
        angle = (math.atan2(-dx, dy) * 180. / math.pi) % 360.
        mangle = 360. * 2
        move = None
        for pd in pds:
            tangle = (20. if pd.mslice == -1 else 0.)
            dangle = min((angle-pd.angle)%360., (pd.angle-angle)%360.) + tangle
            if dangle < mangle:
                mangle = dangle
                move = (pd.maxis,  (-1 if total else pd.mslice),  pd.mdir)
            dangle = min((angle-pd.angle+180)%360., (pd.angle-angle+180)%360.) + tangle
            if dangle < mangle:
                mangle = dangle
                move = (pd.maxis,  (-1 if total else pd.mslice),  not pd.mdir)
        return move
        
    def mouse_rotate_gesture_slice(self, x, y):
        if self.gesture_total is None:
            return None
        move = self.mouse_get_gesture_move(x, y, self.gesture_total)
        self.gesture_total = None
        if move is None:
            return
        self.game.set_next(move)
        self.ui_update_statusbar()
        move_data = self.game.step_next()
        self.animation_start(move_data, stop_after=False, animtype=AnimType.NEW)
        
    def mouse_swap_picked_block(self):
        pd = self.pickdata
        self.game.swap_block(pd.blockpos, pd.maxis, pd.mslice, pd.mdir)
        self.gl_set_transformations(self.game.current_state.rotations)
        
    def mouse_rotate_picked_block(self, rdir):
        blockpos = self.pickdata.blockpos
        self.game.rotate_block(blockpos, rdir)
        self.gl_set_transformations(self.game.current_state.rotations)
        
    def camrotation_start(self, x=None, y=None):
        self.button_down_background = True
        if x is not None:
            self.last_mouse_x = x
            self.last_mouse_y = y
            self.last_camrotation = self.camrotation
            
    def camrotation_changed(self, dx, dy):
        rx, ry = self.camrotation
        self.camrotation = glarea.set_rotation_xy(rx+dx, ry+dy)
        #TODO: configurable rotation methods
        #glarea.set_rotation_rel(dx, dy)
        self.gl_update_selection()
        qt.update_drawingarea()
        
    def camrotation_end(self):
        self.button_down_background = False
        self.gl_update_selection()
            
    def on_mouse_pressed(self, modifiers, button, x, y, height):
        if DEBUG_MOUSEPOS:
            print('mouse press modifiers={} button={} x={} y={}'.format(modifiers, button, x, y))
        total = modifiers & qt.QtKeys.ControlModifier
        if settings['draw.selection'] == 2:
            self.gesture_total = total
            self.mouse_start_gesture(x, y, height)
        elif self.pickdata is not None:
            if self.animation_active:
                return
            # make a move
            if self.editing_model:
                if modifiers & qt.QtKeys.ControlModifier:
                    if button == qt.QtKeys.LeftButton:
                        self.mouse_rotate_picked_block(False)
                    elif button == qt.QtKeys.RightButton:
                        self.mouse_rotate_picked_block(True)
                else:
                    if button == qt.QtKeys.LeftButton:
                        self.mouse_swap_picked_block()
            else:
                if button == qt.QtKeys.LeftButton:
                    self.mouse_rotate_picked_slice(total, False)
                elif button == qt.QtKeys.RightButton and settings['draw.selection_nick'] == 'simple':
                    self.mouse_rotate_picked_slice(total, True)
        elif button == qt.QtKeys.LeftButton:
            self.camrotation_start(x, y)
        qt.update_drawingarea()
        
    def on_mouse_released(self, x, y):
        if DEBUG_MOUSEPOS:
            print('mouse release x={} y={}'.format(x, y))
        if self.button_down_background:
            self.camrotation_end()
        else:
            self.mouse_rotate_gesture_slice(x, y)
        
    def on_camrotation_changed(self, dx, dy):  # only variant qtq
        self.camrotation_changed(dx, dy)
        
    def on_mouse_entered(self):  # only variant qtq
        self.ui_update_cursor()
        
    def on_mouse_moved(self, buttons, x, y, height, pixeldensity):
        if self.use_cursor:
            glarea.set_pick_position(x, height-y)
        if DEBUG_MOUSEPOS:
            print('mouse {} buttons={} x={} y={}'.format('move' if buttons else 'hover', buttons, x, height-y))
        
        if not (buttons and self.button_down_background):
            self.gl_update_selection()
            return
            
        # perform rotation
        rx, ry = self.last_camrotation
        dx = (x - self.last_mouse_x) / pixeldensity * 4
        dy = (y - self.last_mouse_y) / pixeldensity * 4
        self.camrotation = glarea.set_rotation_xy(rx+dx, ry+dy)
        qt.update_drawingarea()
        
    def on_mouse_drop_color(self, x, height_y, color):
        if DEBUG_MOUSEPOS:
            print('dropped color', x, height_y, color)
        glarea.set_pick_position(x, height_y)
        self.drop_colorname = color
        qt.set_pick_requested(True)
        qt.update_drawingarea()
        
    def on_mouse_drop_url(self, x, height_y, url):
        if DEBUG_MOUSEPOS:
            print('dropped url', x, height_y, url)
        glarea.set_pick_position(x, height_y)
        import urllib.parse, urllib.request
        type, filename = urllib.parse.splittype(url)
        host, filename = urllib.parse.splithost(filename)
        if type != 'file' or host:
            debug('"%s" not a local file.' % url)
            return
        filename = urllib.request.url2pathname(filename)
        if not filename or not os.path.exists(filename):
            debug('"%s" not found.' % filename)
            return
        self.drop_filename = filename
        qt.set_pick_requested(True)
        qt.update_drawingarea()
        
    def on_mouse_zoom(self, delta):
        if DEBUG_MOUSEPOS:
            print('wheel event', delta)
        zoom = settings['draw.zoom'] * math.pow(1.1, -delta)
        zoom_min, zoom_max = settings['draw.zoom_range']
        if zoom < zoom_min:
            zoom = zoom_min
        if zoom > zoom_max:
            zoom = zoom_max
        settings['draw.zoom'] = zoom
        self.gl_update_selection()
            
    def on_picking_result(self, index):
        if self.animation_active:
            if self.pickdata is not None:
                self.pickdata = None
                self.ui_update_cursor()
            return
            
        self.pickdata = self.gl_resolve_pickindex(index)
        self.ui_update_cursor()
        if DEBUG_DRAW:
            self.debug_update_pick_text()
            qt.update_drawingarea()
        if self.drop_colorname is not None:
            self.ui_update_dropped_color()
            self.drop_colorname = None
        elif self.drop_filename is not None:
            self.ui_update_dropped_file()
            self.drop_filename = None
        elif not self.use_cursor and self.pickdata is None:
            self.camrotation_start()
        qt.set_pick_requested(False)
        
    ### sidepane handlers ###
    
    def ui_plugin_activated(self, func_idx):
        if self.animation_active:
            return
        if func_idx < 0:
            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:
            qt.show_message(e.args[0])
            return
        # model accepted, now run the plugin function
        game = self.game.copy()
        try:
            func(game)
        except pluginlib.PluginSolverAbort as e:
            self.challenge = None
            qt.show_message(e.args[0])
            return
        if game.plugin_mode == 'append':
            # append moves
            self.challenge_open = 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.challenge_open = 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.challenge = func_idx
            self.challenge_open = True
            changed = True
        else:
            assert False
        if changed:
            self.gl_set_transformations(self.game.current_state.rotations)
            self.ui_update()
            if changed == 'animate':
                move_data = self.game.step_next()
                self.animation_start(move_data, stop_after=False, animtype=AnimType.NEW)
        qt.update_drawingarea()
        
    on_plugin_activated = ui_plugin_activated
    
    ### action handlers ###
    
    def on_action_challenge_triggered(self):
        if self.challenge is None:
            self.ui_new_game(solved=False)
        else:
            self.ui_plugin_activated(self.challenge)
    def on_action_new_solved_triggered(self):
        self.ui_new_game(solved=True)
    def on_action_selectmodel_triggered(self):
        from . import dialogs
        return dialogs.ModelSelection()
    def on_action_quit_triggered(self):  # only variant qtq
        qt.close_mainwindow()
    def on_action_preferences_triggered(self):
        if not self.preferences_dialog_created:
            from . import dialogs
            sample_buffers = max(qt.get_samples(), 1)
            dialogs.init_preferences(sample_buffers)
            self.preferences_dialog_created = True
        qt.show_preferences()
    def on_action_reset_rotation_triggered(self):
        '''Reset cube rotation'''
        self.camrotation = glarea.set_rotation_xy(*self.model.default_rotation)
        qt.update_drawingarea()
        
    def on_action_rewind_triggered(self):
        if self.animation_active:
            self.stop_requested = True
            if self.animtype == AnimType.BWD:
                self.game.step_next() # undo the already applied move
            self.game.goto_prev_mark()
            self.animation_ending()
        else:
            needs_update = self.game.goto_prev_mark()
            self.gl_set_transformations(self.game.current_state.rotations)
            if needs_update:
                qt.update_drawingarea()
                self.ui_update()
    def on_action_previous_triggered(self):
        if self.animation_active:
            if self.animtype == AnimType.BWD:
                # request another BWD move
                self.stop_requested = False
            else:
                self.stop_requested = True
                self.game.step_back()
            self.animation_ending()
            self.stop_requested = True
        else:
            move_data = self.game.step_back()
            self.animation_start(move_data, stop_after=True, animtype=AnimType.BWD)
    def on_action_stop_triggered(self):
        if self.animation_active:
            if self.stop_requested:
                self.animation_ending()
            else:
                self.stop_requested = True
    def on_action_play_triggered(self):
        if not self.animation_active:
            move_data = self.game.step_next()
            self.animation_start(move_data, stop_after=False, animtype=AnimType.FWD)
    def on_action_next_triggered(self):
        if self.animation_active:
            sr = self.stop_requested
            if self.animtype == AnimType.BWD:
                self.stop_requested = True
                self.game.step_next()
            else:
                self.stop_requested = False
            self.animation_ending()
            self.stop_requested = sr
        else:
            move_data = self.game.step_next()
            self.animation_start(move_data, stop_after=True, animtype=AnimType.FWD)
    def on_action_forward_triggered(self):
        if self.animation_active:
            self.stop_requested = True
            if self.animtype != AnimType.BWD:
                self.game.step_back() # undo the already applied move
            self.game.goto_next_mark()
            self.animation_ending()
        else:
            self.game.goto_next_mark()
            self.gl_set_transformations(self.game.current_state.rotations)
            qt.update_drawingarea()
            self.ui_update()
    def on_action_mark_set_triggered(self):
        self.game.move_sequence.mark_current(True)
        self.ui_update()
    def on_action_mark_remove_triggered(self):
        self.game.move_sequence.mark_current(False)
        self.ui_update()
        
    def on_action_initial_state_triggered(self):
        if self.animation_active:
            return
        self.game.set_as_initial_state()
        self.ui_update()
        
    ### edit formula handlers ###
    
    def on_edit_finished(self, code, pos):
        if self.animation_active:
            self.animation_stop()
        self.game.set_code(code, pos)
        self.gl_set_transformations(self.game.current_state.rotations)
        qt.update_drawingarea()
        self.ui_update()
        
    def on_edit_moves_nextword(self, code, pos):  # only variant qtw
        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)
        return pos
        
    def on_edit_moves_prevword(self, code, pos):  # only variant qtw
        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)
        return pos
        
    def on_edit_moves_swapnext(self, code, pos):  # only variant qtw
        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)
        qt.set_edit_moves(code, pos)
        
    def on_edit_moves_swapprev(self, code, pos):  # only variant qtw
        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)
        qt.set_edit_moves(code, pos)
        
    ### misc ###
    
    def on_action_edit_cube_triggered(self):
        self.animation_stop()
        self.game.goto_start()
        if DEBUG_ROTATE:
            self.game.current_state.debug_blocksymbols(allsyms=True)
        self.gl_set_transformations(self.game.current_state.rotations)
        self.ui_set_editing_mode(True)
        self.ui_update()
        
    def on_dialog_text(self, key):
        from . import dialogs
        return dialogs.DialogValues.get_text(key)
        
    def dialog_get_image_data(self, imagefile):  # only variant qtw
        stockfiles = Theme.textures.get_icons()
        index_icon = 1 + len(stockfiles)
        if imagefile.startswith('/'):
            index = index_icon
        else:
            try:
                index = stockfiles.index(imagefile) + 1
            except ValueError:
                index = 0
            imagefile = None
        return index_icon, imagefile, index
        
    def on_dialog_change_current_face(self, current_facekey):  # only variant qtw
        color = settings['theme.faces',current_facekey,'color']
        imageindex_icon, imagefile, imageindex = self.dialog_get_image_data(settings['theme.faces',current_facekey,'image'])
        imagemode = settings['theme.faces',current_facekey,'mode']
        qt.set_preferences_current_face_theme(color, imageindex_icon, imagefile, imageindex, imagemode)
        
    def on_dialog_change_current_image(self, current_facekey, index):  # only variant qtw
        stockfiles = Theme.textures.get_icons()
        if index == 0:
            filename = ''
        elif 0 < index <= len(stockfiles):
            filename = stockfiles[index-1]
        else:
            filename = qt.get_filedialog_imagefile()
            if filename == '':
                # canceled, set the old image
                filename = settings['theme.faces',current_facekey,'image']
                index_icon, imagefile, index = self.dialog_get_image_data(filename)
                qt.set_combobox_current_image(index_icon, imagefile, index)
            else:
                index_icon, imagefile, index = self.dialog_get_image_data(filename)
                qt.set_combobox_current_image(index_icon, imagefile, -1)
        settings['theme.faces',current_facekey,'image'] = filename
        
    def on_dialog_reset_current_image(self, current_facekey):  # only variant qtw
        del settings['theme.faces',current_facekey,'image']
        index_icon, imagefile, index = self.dialog_get_image_data(settings['theme.faces',current_facekey,'image'])
        qt.set_combobox_current_image(index_icon, imagefile, index)
        
    # a: rewind enabled, previous enabled
    # b: play enabled, next enabled, forward enabled
    # c: add_mark enabled, remove_mark enabled
    # d: stop visible, play !visible
    # e: add_mark visible, remove_mark !visible
    #                                                      a  b  c  d         e
    def ui_toolbar_empty(self):         qt.set_toolbar_state((0, 0, 0, 0,        0))
    def ui_toolbar_first(self):         qt.set_toolbar_state((0, 1, 0, 0,        0))
    def ui_toolbar_mid(self, mark):     qt.set_toolbar_state((1, 1, 1, 0, not mark))
    def ui_toolbar_last(self, mark):    qt.set_toolbar_state((1, 0, 1, 0, not mark))
    def ui_toolbar_playing(self, mark): qt.set_toolbar_state((1, 1, 0, 1, not mark))
    
    

