#!/usr/bin/python3
# -*- coding: utf-8 -*-

#  Copyright © 2012-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 re
import pickle
from array import array
from enum import Enum

from .debug import (debug, DEBUG_DRAW, DEBUG_DRAWNORMALS, DEBUG_DRAWAXES,
                           DEBUG_NOPICK, DEBUG_NOBEVEL, DEBUG_NOLABEL, DEBUG_NOBLACK,
                           DEBUG_NOCACHE, DEBUG_MODELFAST, DEBUG_CACHEUPDATE)
from .config import MODELS_DIR
from .utils import filebyteorder, epsilon, get_texcoords_range, Vector

try:
    _
except NameError:
    _ = lambda t: t
    

def get_texcoords(vector, normal, minx, maxx, miny, maxy):
    x, y = vector.rotationyx_normal(normal)
    return (x - minx) / (maxx - minx), (y - miny) / (maxy - miny)
    
        
class EmptyModel:
    type = ''
    symbols = ''
    symbolsI = ''
    faces = ''
    default_rotation = (0., 0.)
    
    size = sizes = ()
    blocks = []
    cell_count = 0
    cells_visible_faces = []
    
    def __init__(self, *unused_args):
        pass
        
    def __str__(self):
        return 'no puzzle'
        
    def gl_vertex_data(self, *unused_args):
        return (0, b'', (0, 0, 0, 0, 0, 0), ([], 0, 0, 0), []), [()]
        
        
class Model:
    bevel_width = 0.01 if DEBUG_NOBEVEL else 0.1
    
    cache_index = {}
    cache_data = {}
    cache_vectors = {} if DEBUG_CACHEUPDATE else []
    
    def __init__(self, mtype, size):
        items = self.cache_index['type'][mtype].items()
        self.type = mtype
        for key, value in items:
            setattr(self, key, value)
        self.mformat = _(self.mformat)
        # size for UI and config, sizes for plugins
        self.size = self.cache_index['normsize'][mtype].get(size, self.defaultsize)
        self.sizes, filename = self.cache_index['sizes'][mtype][self.size]
        
        self.load_data(filename)
        
        self.cell_count = len(self.cell_indices)
        self.rotation_indices = {s:i for i, s in enumerate(self.rotation_symbols)}
        self._inplace_rotations = None
        if self.slicesmode == 'all':
            self.ignored_slice = [None for sz in self.sizes]
        elif self.slicesmode == 'mid':
            self.ignored_slice = [(sz-1)//2 for sz in self.sizes]
        elif self.slicesmode == 'last1':
            self.ignored_slice = [(None if a == 1 else sz-1) for a, sz in enumerate(self.sizes)]
        else:
            assert False, self.slicesmode
        
        #XXX: self.beveled_fvertdata generated here from a nested list, but
        #     the idices come from the modeldata files
        self.beveled_fvertdata = self.gl_bevel_faces()
        
    @property
    def inplace_rotations(self):
        if self._inplace_rotations is None:
            # slow and only used in edit mode
            self._inplace_rotations = []
            for cellidx in range(self.cell_count):
                inplace_rotations = []
                for rotsym, blocknum_blocknum2 in self.rotated_position.items():
                    for blocknum, blocknum2 in enumerate(blocknum_blocknum2):
                        if blocknum == blocknum2 == cellidx and rotsym:
                            inplace_rotations.append(rotsym)
                self._inplace_rotations.append(sorted(inplace_rotations, key=len)[:2])
        return self._inplace_rotations
        
    def is_user_slice(self, iaxis, mslice):
        return self.ignored_slice[iaxis] is None or self.ignored_slice[iaxis] != mslice
        
    def __str__(self):
        return self.mformat.format(*self.size)
        
    @classmethod
    def load_index(cls):
        if DEBUG_NOCACHE:
            try:
                from buildlib import modeldata
            except ImportError:
                pass
            else:
                debug('generating model index')
                cls.cache_index = modeldata.pool_create_indexdata()
                return
        indexfilename = os.path.join(MODELS_DIR, 'f00index' if DEBUG_MODELFAST else 'd00index')
        debug('loading model index:', indexfilename)
        with open(indexfilename, 'rb') as indexfile:
            cls.cache_index = pickle.load(indexfile)
            
    @staticmethod
    def read_vectors(datafile):
        vlen = array('I')
        vlen.fromfile(datafile, 1)
        if sys.byteorder != filebyteorder:
            vlen.byteswap()
        vlen = vlen[0]
        vectors = array('f')
        vectors.fromfile(datafile, vlen)
        if sys.byteorder != filebyteorder:
            vectors.byteswap()
        return vectors
        
    @staticmethod
    def isplit3_vectors(vals):
        it = iter(vals)
        for x in it:
            try:
                yield Vector((x, next(it), next(it)))
            except StopIteration:
                raise ValueError('len must be a multiple of 3')
                
    @classmethod
    def _load_data(self, filename):
        if DEBUG_NOCACHE:
            try:
                from buildlib import modeldata
            except ImportError:
                pass
            else:
                print('generating model data:', filename)
                _data, _vectors, *unused = modeldata.pool_create_modelfiledata(filename)
                _vectors = [Vector(v) for v in _vectors]
                return _data, _vectors
        filename = os.path.join(MODELS_DIR, filename)
        debug('loading model data:', filename)
        with open(filename, 'rb') as datafile:
            data = pickle.load(datafile)
            vectors = list(self.isplit3_vectors(self.read_vectors(datafile)))
        return data, vectors
        
    def load_data(self, filename):
        debug('requesting model data:', filename, self.type, self.sizes)
        try:
            data = (self.cache_data[filename] if DEBUG_CACHEUPDATE else self.cache_data)[self.type][self.sizes]
        except KeyError:
            data, vectors = self._load_data(filename)
            if DEBUG_CACHEUPDATE:
                self.cache_data[filename] = data
                self.cache_vectors[filename] = vectors
            else:
                self.__class__.cache_data = data
                self.__class__.cache_vectors = vectors
            data = data[self.type][self.sizes]
        self.vectors = self.cache_vectors[filename] if DEBUG_CACHEUPDATE else self.cache_vectors
        for key, value in data.items():
            setattr(self, key, value)
            
    @classmethod
    def from_string(cls, modelstr):
        if modelstr == '*':
            return '*', '*', (), None
        re_model = r'''^(\w+)
                        (?: \s+(\w+)
                            (?: \s*(\W)
                                \s*(\w+)
                                (?: \s*\3
                                    \s*(\w+)
                            )?)?
                            (?:\s+with
                                \s+(.+)
                        )?)?\s*$'''
        match = re.match(re_model, modelstr, re.X)
        if match is None:
            raise ValueError('Invalid model: ' + modelstr)
        #TODO: len(sizes) > 3
        mtype, width, height, depth, exp = match.group(1, 2, 4, 5, 6)
        if mtype not in cls.cache_index['types']:
            raise ValueError('Unknown model type %r' % mtype)
        def convert_if_int(value):
            if value is None:
                return value
            try:
                return int(value)
            except ValueError:
                return value
        sizes = tuple(convert_if_int(s) for s in (width, height, depth))
        return modelstr, mtype, sizes, exp
        
    def norm_symbol(self, sym):
        try:
            return self.normal_rotation_symbols[sym]
        except KeyError:
            new_sym = ''
            for c in sym:
                try:
                    new_sym = self.normal_rotation_symbols[new_sym+c]
                except KeyError:
                    raise ValueError('invalid symbol:', sym)
            return new_sym
            
    def blocksym_to_blockpos(self, blocksym):
        for i, faces in enumerate(self.cells_visible_faces):
            #FIXME: different blocks can have same visible faces (eg. edges for 4-Cubes)
            if sorted(faces) == sorted(list(blocksym)):
                return i
        raise ValueError('Invalid block symbol:', blocksym)
        
    def blockpos_to_blocksym(self, blockpos, rotsym):
        #TODO: use cells_visible_faces, see blocksym_to_blockpos
        def axisidx_to_sym(axis, idx):
            if idx <= self.sizes[axis] // 2:
                sym = self.symbols[axis]
            else:
                idx = self.sizes[axis]-1 - idx
                sym = self.symbolsI[axis]
            if idx == 0:
                # skip idx for corners
                return sym, self.face_symbolic_to_face_color(sym, rotsym)
            elif idx == 1 and self.sizes[axis] == 3:
                # for size == 3 there is only one edge
                return '', ''
            else:
                # symbol with index to distinguish edge, but no color because the face is not visible
                return sym + str(idx+1), '?'
        #FIXME: only for models with 3 axes, function only used with debug flags
        x, y, z = self.cell_indices[blockpos]
        symx, colorsymx = axisidx_to_sym(0, x)
        symy, colorsymy = axisidx_to_sym(1, y)
        symz, colorsymz = axisidx_to_sym(2, z)
        return symx + symy + symz, colorsymx + colorsymy + colorsymz
        
    def face_symbolic_to_face_color(self, face, rot):
        for k, v in self.face_permutations[rot].items():
            if v == face:
                return k
        assert False, (face, rot)
        
    def sym_to_move(self, sym, mslice=-1):
        try:
            maxis = self.symbols.index(sym)
            return maxis, mslice, False
        except ValueError:
            maxis = self.symbolsI.index(sym)
            return maxis, mslice, True
        
    def normalize_complete_moves(self, moves):
        syms = ''.join((self.symbols if not mdir else self.symbolsI)[maxis] for maxis, unused_mslice, mdir in moves)
        syms = self.norm_symbol(syms)
        for sym in syms:
            yield self.sym_to_move(sym)
        
    def rotate_symbolic(self, axis, rdir, block, sym):
        rsym = (self.symbols if not rdir else self.symbolsI)[axis]
        block = self.rotated_position[rsym][block]
        sym = self.norm_symbol(sym + rsym)
        return block, sym
        
    def rotate_move(self, complete_move, move):
        caxis, unused_cslice, cdir = complete_move
        maxis, mslice, mdir = move
        caxissym = (self.symbols if cdir else self.symbolsI)[caxis]
        maxissym = (self.symbols if not mdir else self.symbolsI)[maxis]
        raxissym = self.face_permutations[caxissym][maxissym]
        raxis, unused_rslice, rdir = self.sym_to_move(raxissym)
        if mdir != rdir:
            mslice = self.sizes[raxis] - 1 - mslice
        return raxis, mslice, rdir
        
    class MoveParallel (Enum):
        NO, LT, COMPAT, GT = range(4)
        
    def is_parallel(self, iaxis1, slice1, iaxis2, slice2):
        for idx in range(self.cell_count):
            pos, rot = idx, ''
            bslice = self.cell_indices[pos][iaxis1]
            if bslice != slice1:
                continue
            pos, rot = self.rotate_symbolic(iaxis1, False, pos, rot)
            
            bslice = self.cell_indices[pos][iaxis2]
            if bslice != slice2:
                continue
            pos, rot = self.rotate_symbolic(iaxis2, False, pos, rot)
            
            bslice = self.cell_indices[pos][iaxis1]
            if bslice == slice1:
                pos, rot = self.rotate_symbolic(iaxis1, True, pos, rot)
                
            bslice = self.cell_indices[pos][iaxis2]
            if bslice == slice2:
                pos, rot = self.rotate_symbolic(iaxis2, True, pos, rot)
                
            if idx != pos or rot != '':
                return False
        else:
            return True
        assert False
        
    def parallel(self, move1, move2):
        if move1.axis == move2.axis:
            if move1.slice == move2.slice:  return self.MoveParallel.COMPAT
            if move1.slice < move2.slice:   return self.MoveParallel.LT
            if move1.slice > move2.slice:   return self.MoveParallel.GT
        if self.is_parallel(move1.axis, move1.slice, move2.axis, move2.slice):
            if move1.axis < move2.axis: 
                return self.MoveParallel.LT
            else:  # move1.axis > move2.axis
                return self.MoveParallel.GT
        return self.MoveParallel.NO
        
    def _get_selected_moves(self, cellidx, vector):
        for iaxis, axis in enumerate(self.axes):
            vda = vector.dot(axis)
            if -epsilon < vda < epsilon:
                continue
            mslice = self.cell_indices[cellidx][iaxis]
            if mslice is not None:
                if not self.is_user_slice(iaxis, mslice):
                    mslice = -1
                yield iaxis, mslice, vda
            
    def _get_other_moves(self, cellidx, vector):
        '''Moves with axes that are not parallel to vector'''
        for iaxis, axis in enumerate(self.axes):
            vca = vector.cross(axis)
            if all(-epsilon < x < epsilon for x in vca):
                continue
            mslice = self.cell_indices[cellidx][iaxis]
            if mslice is not None:
                if not self.is_user_slice(iaxis, mslice):
                    mslice = -1
                yield iaxis, mslice, vca.normalised()
                
    def get_selected_moves2(self, cellidx, vert1, vert2):
        res = sorted(self._get_selected_moves(cellidx, vert2 - vert1), key=lambda m: -abs(m[2]))
        a1, s1, d1 = res[0]
        if len(res) == 1:
            return True, (a1, s1, d1>0)
        a2, s2, d2 = res[1]
        if s1 >= 0 and s2 < 0:
            return True, (a1, s1, d1>0)
        if s1 < 0 and s2 >= 0:
            return True, (a2, s2, d2>0)
        return False, (a1, s1, d1>0,  a2, s2, d2>0)
        
    def get_selected_move_center(self, cellidx, vector):
        moves = ((ma, ms, md) for ma, ms, md in self._get_selected_moves(cellidx, vector) if ms in [0,self.sizes[ma]-1])
        ma, ms, md = max(moves, key=lambda m: abs(m[2]))
        return ma, ms, md>0
        
    @staticmethod
    def triangulate(indices, reverse=False):
        indices = iter(indices)
        first = next(indices)
        prev = next(indices)
        for index in indices:
            yield first
            if reverse:
                yield index
                yield prev
            else:
                yield prev
                yield index
            prev = index
            
    @staticmethod
    def triangulate_barycentric(count, indices, reverse=False):
        if count == 3:
            index1, index2, index3 = indices
            yield index1, (1., 0., 0.)
            index2 = index2, (0., 1., 0.)
            index3 = index3, (0., 0., 1.)
            if reverse:
                yield index3; yield index2
            else:
                yield index2; yield index3
        elif count == 4:
            # +---+  4---3  +-1-+ 
            # | / |  | / |  3 2 1 
            # +---+  1---2  +-3-+ 
            index1, index2, index3, index4 = indices
            index1 = index1, (1., 1., 0.)
            index2 = index2, (0., 1., 0.)
            index3 = index3, (0., 1., 1.)
            index4 = index4, (0., 1., 0.)
            if reverse:
                yield index1; yield index3; yield index2; yield index1; yield index4; yield index3
            else:
                yield index1; yield index2; yield index3; yield index1; yield index3; yield index4
        elif count == 5:
            #   +---+    4---3    +---+ 
            #  /|  /|   /|  /|   2|  /| 
            # + | / |  5 | / |  + 3 3 2 
            #  \|/  |   \|/  |   1|/  | 
            #   +---+    1---2    +-1-+ 
            index1, index2, index3, index4, index5 = indices
            index1 = index1, (0., 1., 1.)
            index2 = index2, (0., 0., 1.)
            index3 = index3, (1., 0., 1.)
            index4 = index4, (1., 0., 1.)
            index5 = index5, (0., 0., 1.)
            if reverse:
                yield index1; yield index3; yield index2; yield index1; yield index4; yield index3
                yield index1; yield index5; yield index4
            else:
                yield index1; yield index2; yield index3; yield index1; yield index3; yield index4
                yield index1; yield index4; yield index5
        elif count == 6:
            #   +---+      1---6      +---+   
            #  /|\  |\    /|\  |\    1|\  |1  
            # + | \ | +  2 | \ | 5  + 3 \ 3 + 
            #  \|  \|/    \|  \|/    2|  \|2  
            #   +---+      3---4      +---+   
            index1, index2, index3, index4, index5, index6 = indices
            index1a = index1, (0., 1., 1.)
            index1b = index1, (0., 1., 1.)
            index2 = index2, (0., 0., 1.)
            index3 = index3, (1., 0., 1.)
            index4 = index4, (1., 0., 1.)
            index5 = index5, (0., 0., 1.)
            index6 = index6, (0., 1., 1.)
            if reverse:
                yield index1a; yield index3; yield index2; yield index1b; yield index4; yield index3
                yield index1b; yield index6; yield index4; yield index4; yield index6; yield index5
            else:
                yield index1a; yield index2; yield index3; yield index1a; yield index3; yield index4
                yield index1a; yield index4; yield index6; yield index4; yield index5; yield index6
        else:
            assert False, count
            
    def pickarrowdir(self, maxis, mdir, normal):
        if mdir:
            return Vector(self.axes[maxis]).cross(normal).normalised()
        else:
            return normal.cross(Vector(self.axes[maxis])).normalised()
            
    def _gl_pick_triangles_pickdata_helper(self, cellidx, symbol, normal, vc, vert1, vert2, pickdata):
        one, masd = self.get_selected_moves2(cellidx, vert1, vert2)
        if one:
            maxis, mslice, mdir = masd
            arrowdir = self.pickarrowdir(maxis, mdir, normal)
            pickdata.append((maxis, mslice, mdir, cellidx, symbol, arrowdir))
        else:
            maxis1, mslice1, mdir1,  maxis2, mslice2, mdir2 = masd
            vert1_scaled = vc*.1 + vert1*.9 - self.vectors[self.cell_centers[cellidx]]
            #d = sqrt(v1² + v2² + v3² - (v1*a1 + v2*a2 + v3*a3)²)
            sdist11_sdist12 = abs(vert1_scaled.dot(self.axes[maxis1])) - abs(vert1_scaled.dot(self.axes[maxis2]))
            if self.reversepick:
                sdist11_sdist12 = -sdist11_sdist12
            if sdist11_sdist12 < 0:
                arrowdir = self.pickarrowdir(maxis1, mdir1, normal)
                pickdata.append((maxis1, mslice1, mdir1, cellidx, symbol, arrowdir))
                arrowdir = self.pickarrowdir(maxis2, mdir2, normal)
                pickdata.append((maxis2, mslice2, mdir2, cellidx, symbol, arrowdir))
            else:
                arrowdir = self.pickarrowdir(maxis2, mdir2, normal)
                pickdata.append((maxis2, mslice2, mdir2, cellidx, symbol, arrowdir))
                arrowdir = self.pickarrowdir(maxis1, mdir1, normal)
                pickdata.append((maxis1, mslice1, mdir1, cellidx, symbol, arrowdir))
        return one
            
    def _gl_pick_triangulate(self, color1, vertsl, normal, mirror_distance):
        for i, verts in enumerate(vertsl):
            for vert in self.triangulate(verts):
                yield color1 + i, vert
        if mirror_distance is not None:
            mirror_offset = normal * mirror_distance
            vertsl = [[v + mirror_offset for v in verts] for verts in vertsl]
            for i, verts in enumerate(vertsl):
                for vert in self.triangulate(verts, reverse=True):
                    yield color1 + i, vert
                    
    def gl_pick_triangles_center(self, cellidx, edges_iverts, symbol, normal, mirror_distance, pickdata):
        # selection_mode: simple, no adjacent visible faces
        maxis, mslice, mdir = self.get_selected_move_center(cellidx, normal)
        color = len(pickdata)
        pickdata.append((maxis, mslice, mdir, cellidx, symbol, None))
        verts = [self.vectors[i] for i in edges_iverts]
        for vert in self.triangulate(verts):
            yield color, vert
        if mirror_distance is not None:
            mirror_offset = normal * mirror_distance
            color = len(pickdata)
            pickdata.append((maxis, mslice, not mdir, cellidx, symbol, None))
            for vert in self.triangulate(verts, reverse=True):
                yield color, vert + mirror_offset
                
    def gl_pick_triangles_gesture(self, cellidx, edges_iverts, symbol, normal, mirror_distance, pickdata):
        # selection_mode: gesture
        moves = list(self._get_other_moves(cellidx, normal))
        color = len(pickdata)
        pickdata.append((None, moves, False, cellidx, symbol, None))
        verts = [self.vectors[i] for i in edges_iverts]
        for vert in self.triangulate(verts):
            yield color, vert
        if mirror_distance is not None:
            mirror_offset = normal * mirror_distance
            for vert in self.triangulate(verts, reverse=True):
                yield color, vert + mirror_offset
            
    def gl_pick_triangles_edge3(self, cellidx, edges_iverts, symbol, normal, mirror_distance, pickdata):
        # selection_mode: simple, face is on an edge block of a tetrahedron
        # the first and second vertex are on the edge of the tetrahedron
        vert1, vert2, vert3 = (self.vectors[i] for i in edges_iverts)
        vc = (vert1+vert2+vert3) / 3
        color1 = len(pickdata)
        one = self._gl_pick_triangles_pickdata_helper(cellidx, symbol, normal, vc, vert1, vert2, pickdata)
        if one:
            vertsl = [[vert1, vert2, vert3]]
        else:
            v12 = (vert1 + vert2) / 2
            vertsl = [[v12, vert2, vert3], [vert1, v12, vert3]]
        yield from self._gl_pick_triangulate(color1, vertsl, normal, mirror_distance)
            
    def gl_pick_triangles_edge6(self, cellidx, edges_iverts, symbol, normal, mirror_distance, pickdata):
        # selection_mode: simple, face is on an edge block and has 6 vertices, rotates 2 slices
        # the first and second vertex are on the edge
        vert1, vert2, vert3, vert4, vert5, vert6 = (self.vectors[i] for i in edges_iverts)
        vc = (vert1+vert2+vert3+vert4+vert5+vert6) / 6
        v12 = (vert1 + vert2) / 2
        v45 = (vert4 + vert5) / 2
        color = len(pickdata)
        one = self._gl_pick_triangles_pickdata_helper(cellidx, symbol, normal, vc, vert1, vert2, pickdata)
        if one:
            vertsl = [[vert1, vert2, vert3, vert4, vert5, vert6]]
        else:
            vertsl = [[v12, vert2, vert3, vert4, v45], [vert1, v12, v45, vert5, vert6]]
        yield from self._gl_pick_triangulate(color, vertsl, normal, mirror_distance)
                
    def gl_pick_triangles_edge4(self, cellidx, edges_iverts, symbol, normal, mirror_distance, pickdata):
        # selection_mode: simple, face is on an edge block, 4 vertices, rotate 1 or 2 slices,
        # the first and second vertex are on the edge
        vert1, vert2, vert3, vert4 = (self.vectors[i] for i in edges_iverts)
        vc = (vert1+vert2+vert3+vert4) / 4
        color = len(pickdata)
        one = self._gl_pick_triangles_pickdata_helper(cellidx, symbol, normal, vc, vert1, vert2, pickdata)
        if one:
            vertsl = [[vert1, vert2, vert3, vert4]]
        else:
            #assert ncolors == 2
            ec12 = (vert1 + vert2) / 2
            ec34 = (vert3 + vert4) / 2
            vertsl = [[ec12, vert2, vert3, ec34], [vert1, ec12, ec34, vert4]]
        yield from self._gl_pick_triangulate(color, vertsl, normal, mirror_distance)
                
    def gl_pick_triangles_corner34(self, cellidx, edges_iverts, symbol, normal, mirror_distance, pickdata):
        # selection_mode: simple, face is on a corner block, 3 or 4 vertices,
        # the second vert is on the corner, the first and third are on the edges
        if len(edges_iverts) == 3:
            vert1, vert2, vert3 = (self.vectors[iv] for iv in edges_iverts)
            vert4 = (vert3 + vert1) / 2
        else:
            vert1, vert2, vert3, vert4 = (self.vectors[iv] for iv in edges_iverts)
        vc = (vert1+vert2+vert3+vert4) / 4
        # first triangle
        color = len(pickdata)
        one = self._gl_pick_triangles_pickdata_helper(cellidx, symbol, normal, vc, vert1, vert2, pickdata)
        if one:
            vertsl = [[vert1, vert2, vert4]]
        else:
            ec12 = (vert1 + vert2) / 2
            vertsl = [[ec12, vert2, vert4], [vert1, ec12, vert4]]
        # second triangle
        one = self._gl_pick_triangles_pickdata_helper(cellidx, symbol, normal, vc, vert2, vert3, pickdata)
        if one:
            vertsl.append([vert2, vert3, vert4])
        else:
            ec23 = (vert2 + vert3) / 2
            vertsl.extend([[ec23, vert3, vert4], [vert2, ec23, vert4]])
        yield from self._gl_pick_triangulate(color, vertsl, normal, mirror_distance)
                    
    def gl_pick_triangles_corner5(self, cellidx, edges_iverts, symbol, normal, mirror_distance, pickdata):
        # selection_mode: simple, face is on a corner block, 5 vertices,
        # the second vert is on the corner, the first and third are on the edges
        vert1, vert2, vert3, vert4, vert5 = (self.vectors[iv] for iv in edges_iverts)
        vc = (vert1+vert2+vert3+vert4+vert5) / 5
        vc13 = (vert1 + vert3) / 2
        ec45 = (vert4 + vert5) / 2
        color1 = len(pickdata)
        one = self._gl_pick_triangles_pickdata_helper(cellidx, symbol, normal, vc, vert1, vert2, pickdata)
        if one:
            vertsl = [[vert1, vert2, ec45, vert5]]
        else:
            vertsl = [[vert1, vert2, vc13], [vert1, vc13, ec45, vert5]]
        one = self._gl_pick_triangles_pickdata_helper(cellidx, symbol, normal, vc, vert2, vert3, pickdata)
        if one:
            vertsl.append([vert2, vert3, vert4, ec45])
        else:
            vertsl.extend([[vc13, vert3, vert4, ec45], [vert2, vert3, vc13]])
        yield from self._gl_pick_triangulate(color1, vertsl, normal, mirror_distance)
        
    def gl_pick_triangles_any(self, cellidx, edges_iverts, symbol, normal, mirror_distance, pickdata):
        # either selection_mode is quadrant or weird model (size==1 in at least one dimension)
        verts = [self.vectors[i] for i in edges_iverts]
        vc = sum(verts, Vector()) / len(verts)
        vert1 = verts[-1]
        for vert2 in verts:
            color1 = len(pickdata)
            one = self._gl_pick_triangles_pickdata_helper(cellidx, symbol, normal, vc, vert1, vert2, pickdata)
            if one:
                vertsl = [[vc, vert1, vert2]]
            else:
                #assert ncolors == 2
                ec = (vert1 + vert2) / 2
                vertsl = [[vc, vert1, ec], [vc, ec, vert2]]
            yield from self._gl_pick_triangulate(color1, vertsl, normal, mirror_distance)
            vert1 = vert2
                
    def gl_pick_triangles(self, selection_mode, mirror_distance, pickdata):
        if DEBUG_NOPICK:
            return
        for unused_fvertdata, (normal, cellidx), (fid, picktype, edges_iverts) in self.beveled_fvertdata:
            if picktype is None:
                continue
            symbol = self.faces[fid]
            if selection_mode == 0:
                yield from self.gl_pick_triangles_any(cellidx, edges_iverts, symbol, normal, mirror_distance, pickdata)
            elif selection_mode == 1:
                if picktype == -1:
                    yield from self.gl_pick_triangles_any(cellidx, edges_iverts, symbol, normal, mirror_distance, pickdata)
                elif picktype == 0:
                    yield from self.gl_pick_triangles_center(cellidx, edges_iverts, symbol, normal, mirror_distance, pickdata)
                elif picktype == 1:
                    yield from self.gl_pick_triangles_edge3(cellidx, edges_iverts, symbol, normal, mirror_distance, pickdata)
                elif picktype == 2:
                    yield from self.gl_pick_triangles_edge4(cellidx, edges_iverts, symbol, normal, mirror_distance, pickdata)
                elif picktype == 3:
                    yield from self.gl_pick_triangles_corner34(cellidx, edges_iverts, symbol, normal, mirror_distance, pickdata)
                elif picktype == 5:
                    yield from self.gl_pick_triangles_corner5(cellidx, edges_iverts, symbol, normal, mirror_distance, pickdata)
                elif picktype == 4:
                    yield from self.gl_pick_triangles_edge6(cellidx, edges_iverts, symbol, normal, mirror_distance, pickdata)
                else:
                    assert False, (selection_mode, picktype)
            elif selection_mode == 2:
                yield from self.gl_pick_triangles_gesture(cellidx, edges_iverts, symbol, normal, mirror_distance, pickdata)
            else:
                assert False, (selection_mode, picktype)
                    
    def bevel_cell_label(self, cidx, c_faces):
        # label faces that replace the old faces
        for hf_face, hf_face_id, hf_verts in c_faces:
            fvertdata1, (fnormal, fcidx), unused = self.beveled_fvertdata[hf_face]
            if cidx != fcidx:
                fnormal = -fnormal
            yield hf_face_id, fnormal, [fvertdata1[v] for v in hf_verts]
                
    def bevel_cell_black(self, cidx, c_faces, c_edges, c_verts):
        fton = lambda fnormal, fcidx: fnormal if cidx == fcidx else -fnormal
        if not DEBUG_NOBEVEL:
            # faces that replace the old verts
            for v, vertdata in c_verts:
                vertdata = [(self.beveled_fvertdata[f][0][v], fton(*self.beveled_fvertdata[f][1])) for f in vertdata]
                yield vertdata
            # faces that replace the old edges
            for f1, f3, v1, v3 in c_edges:
                fvertdata1, finfo, unused = self.beveled_fvertdata[f1]
                normal1 = fton(*finfo)
                fvertdata3, finfo, unused = self.beveled_fvertdata[f3]
                normal3 = fton(*finfo)
                vertdata = [(fvertdata1[v3], normal1), (fvertdata3[v3], normal3),
                            (fvertdata3[v1], normal3), (fvertdata1[v1], normal1)]
                yield vertdata
        # black faces that replace the old faces
        for hf_face, hf_verts in c_faces:
            fvertdata1, finfo, unused = self.beveled_fvertdata[hf_face]
            normal1 = fton(*finfo)
            yield [(fvertdata1[v], normal1) for v in hf_verts]
            
    def gl_label_vertices(self, facesdata, faces, mirror_distance):
        if DEBUG_NOLABEL:
            return
        for faceno, normal, vertices in facesdata:
            fd = faces[faceno]
            color = fd.color
            if fd.imagemode:
                texrange = self.texranges_mosaic[faceno]
            else:
                texrange = get_texcoords_range(vertices, normal)
            texpos = (get_texcoords(v, normal, *texrange) for v in vertices)
            x1,y1,x2,y2 = fd.texrect
            texpos = [(tpx*(x2-x1) + x1, tpy*(y2-y1) + y1) for tpx, tpy in texpos]
            
            for (vertex, tp), baryc in self.triangulate_barycentric(
                                                    len(vertices),
                                                    zip(vertices, texpos)):
                yield vertex, normal, color, tp, baryc
            if mirror_distance is not None:
                offset = normal * mirror_distance
                mirror_normal = -normal
                for (vertex, tp), baryc in self.triangulate_barycentric(
                                                    len(vertices),
                                                    zip(vertices, texpos),
                                                    reverse=True):
                    yield vertex + offset, mirror_normal, color, tp, baryc
                    
    def gl_black_vertices(self, facesdata):
        if DEBUG_NOBLACK:
            return
        for vertices in facesdata:
            for ivertex, normal in self.triangulate(vertices):
                yield ivertex, normal
                
    def gl_bevel_faces(self):
        width = self.bevel_width
        if width == 0:
            def bevel_points(ps):
                return {p: p for p in ps}
        else:
            def bevel_points(ps):
                def bevel_point(point1, point2, point3):
                    point1 = self.vectors[point1]
                    point2 = self.vectors[point2]
                    point3 = self.vectors[point3]
                    nd1 = (point1 - point2).normalised()
                    nd2 = (point3 - point2).normalised()
                    return point2 + (nd1 + nd2) * width / nd1.cross(nd2).length()
                lps = len(ps)
                return {p: bevel_point(ps[(i-1)%lps], p, ps[(i+1)%lps]) for i, p in enumerate(ps)}
        def face_normal(p1, p2, p3, *unused_args):
            p1 = self.vectors[p1]
            p2 = self.vectors[p2]
            p3 = self.vectors[p3]
            return (p3 - p2).cross(p1 - p2).normalised()
        return [(bevel_points(ps), (face_normal(*ps), cidx), (fid, picktype, ps))
                        for cidx, cfd in enumerate(self.facesdata) for fid, picktype, ps in cfd]
                    
    def gl_debug_vertices(self, faces, mirror_distance):
        # yield vertices, colors
        # normals
        if DEBUG_DRAWNORMALS:
            for cidx, (c_faces_label, c_faces_black, c_edges, c_verts) in enumerate(self.blocksdata):
                for faceno, normal, vertices in self.bevel_cell_label(cidx, c_faces_label):
                    fd = faces[faceno]
                    color = fd.color
                    
                    vertex = sum(vertices, Vector()) / len(vertices)
                    yield vertex, [0,0,0]
                    yield vertex+normal, color
                    if mirror_distance is not None:
                        offset = normal * mirror_distance
                        yield vertex+offset, [0,0,0]
                        yield vertex+offset-normal, color
        # axes
        if DEBUG_DRAWAXES:
            for i, axis in enumerate(self.axes):
                symbol = self.symbols[i]
                faceno = self.faces.index(symbol)
                fd = faces[faceno]
                color = fd.color
                vaxis = Vector(axis)*self.sizes[i]
                yield -vaxis, [0,0,0]
                yield -vaxis+axis, color
                yield vaxis, [0,0,0]
                yield vaxis+axis, color
        # coordinates
        x = float(-self.sizes[0])
        y = float(-self.sizes[1])
        z = float(-self.sizes[2])
        yield [x-1, y-1, z,  -x,  y-1,  z], [255, 0, 0,  255, 0, 0]
        yield [x-1, y-1, z,  x-1, -y,   z], [0, 255, 0,  0, 255, 0]
        yield [x-1, y-1, z,  x-1, y-1, -z], [0, 0, 255,  0, 0, 255]
        
    def gl_debug_vertices_2D(self):
        # selection (modelview)
        yield [0, 0, 0,  1, 1, 1], [255, 255, 255,  255, 255, 255]
        # selection (viewport)
        yield [0, 0, 0,  1, 1, 0], [255, 255, 0,  255, 0, 0]
        
    def gl_vertex_data(self, selection_mode, theme, mirror_distance):
        vertices = []
        normals = []
        colors = []
        texpos = []
        baryc = []
        # list of [count vertices, face index]
        cnts_block = []
        faces = [theme.faces[fk] for fk in self.facekeys]
        
        for cidx, (c_faces_label, c_faces_black, c_edges, c_verts) in enumerate(self.blocksdata):
            # bevel
            beveled_facesdata_label = self.bevel_cell_label(cidx, c_faces_label)
            beveled_facesdata_black = self.bevel_cell_black(cidx, c_faces_black, c_edges, c_verts)
            
            cnt = 0
            # label vertices
            for v, n, c, t, b in self.gl_label_vertices(beveled_facesdata_label, faces, mirror_distance):
                vertices += v
                normals += n
                colors += c
                texpos += t
                baryc += b
                cnt += 1
            # bevel, hidden labels
            for v, n in self.gl_black_vertices(beveled_facesdata_black):
                vertices += v
                normals += n
                #TODO: colors and texpos unused
                colors += (0,0,0)
                texpos += (0,0)
                baryc += (0,0,0)
                cnt += 1
            cnts_block.append(cnt)
        
        idx_debug = len(vertices) // 3
        cnt_debug = 0
        if DEBUG_DRAW:
            assert len(cnts_block) == self.cell_count
            assert sum(cnts_block) == idx_debug
            for v, c in self.gl_debug_vertices(faces, mirror_distance):
                vertices += v
                colors += c
            cnt_debug = len(vertices) // 3 - idx_debug
            for v, c in self.gl_debug_vertices_2D():
                vertices += v
                colors += c
            assert len(vertices) == len(colors)
            
        pickvertices, pickcolors, pickdata = self.gl_pick_data(selection_mode, mirror_distance)
        cnt_pick = len(pickvertices) // 3
        
        vertices = array('f', vertices).tobytes()
        normals = array('f', normals).tobytes()
        colors = array('B', colors).tobytes()
        texpos = array('f', texpos).tobytes()
        barycentric = array('f', baryc).tobytes()
        pickvertices = array('f', pickvertices).tobytes()
        pickcolors = array('B', pickcolors).tobytes()
        
        vertexdata = vertices + normals + colors + texpos + barycentric + pickvertices + pickcolors
        debug('GL data: %d bytes' % len(vertexdata))
        
        ptrnormals = len(vertices)
        ptrcolors = ptrnormals + len(normals)
        ptrtexpos = ptrcolors + len(colors)
        ptrbarycentric = ptrtexpos + len(texpos)
        ptrpickvertices = ptrbarycentric + len(barycentric)
        ptrpickcolors = ptrpickvertices + len(pickvertices)
        
        return (    self.cell_count,
                    vertexdata,
                    (ptrnormals, ptrcolors, ptrtexpos, ptrbarycentric, ptrpickvertices, ptrpickcolors),
                    (cnts_block, idx_debug, cnt_debug, cnt_pick),
                    self.rotation_matrices,
               ), pickdata
                
    def gl_pick_data(self, selection_mode, mirror_distance):
        # Pick TRIANGLES vertices
        vertices = []
        colors = []
        pickdata = [()]  # list of (maxis, mslice, mdir, face, center, cellidx, symbol, arrowdir)
        for col, v in self.gl_pick_triangles(selection_mode, mirror_distance, pickdata):
            vertices += v
            colors += ((col>>4) & 0xf0, (col) & 0xf0, (col<<4) & 0xf0)
        assert len(vertices) == len(colors)
        return vertices, colors, pickdata
        
        
empty_model = EmptyModel()
    
    
