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

#  Copyright © 2012-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/>.



import os
from math import sin, cos, pi, sqrt
import re
import pickle as pickle
from collections import OrderedDict
import array

import numpy as np

from .debug import DEBUG, DEBUG_NOLABEL, DEBUG_NOBEVEL, DEBUG_INNER, DEBUG_DRAW
from .config import MODELS_DIR

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

use_modeldata = True
modeldata_cache = {}


class Face (object):
    def __init__(self, symbol, vindices=None, vertices=None):
        self.symbol = symbol
        self.vindices = vindices
        self.vertices = vertices
        self.normal = None
        self.scaled = None # Face with indices for beveled_vertices
        self.faces = None # adjacent faces
        
    def __repr__(self):
        return 'Face({}, {})'.format(self.symbol, self.vindices)
        
    def edges(self):
        vertices = self.vertices if self.vindices is None else self.vindices
        v1 = vertices[0]
        for v2 in vertices[1:]:
            yield v1, v2
            v1 = v2
        yield v1, vertices[0]
        
    def center(self, vertices=None):
        if vertices is None:
            vertices = self.vertices
        if self.vindices is not None:
            vertices = [vertices[vi] for vi in self.vindices]
        return [sum(_i)/len(_i) for _i in zip(*vertices)]
        
    def create_scaled(self, vertices, factor, new_vertices):
        bf = []
        for vi in self.vindices:
            # Scale orthogonal to the center
            bv = [(v-c)*factor+c for v, c in zip(vertices[vi], self.center(vertices))]
            bf.append(len(new_vertices))
            new_vertices.append(bv)
        self.scaled = Face(self.symbol, bf)
        
    def translated(self, vertices, offset, factor=1):
        tf = []
        if vertices is None:
            for v in self.vertices:
                tv = [xv+xo*factor for xv, xo in zip(v, offset)]
                tf.append(tv)
        else:
            for vi in self.vindices:
                tv = [xv+xo*factor for xv, xo in zip(vertices[vi], offset)]
                tf.append(tv)
        return Face(self.symbol, vertices=tf)
        
    def roll(self, n):
        if self.vindices is not None:
            vlen = len(self.vindices)
            self.vindices = [self.vindices[(i-n)%vlen] for i in range(vlen)]
        elif self.vertices is not None:
            vlen = len(self.vertices)
            self.vertices = [self.vertices[(i-n)%vlen] for i in range(vlen)]
            
    def init_adjacent_faces(self, faces):
        self.faces = []
        for edge in self.edges():
            edge = tuple(reversed(edge))
            for f in faces:
                for e in f.edges():
                    if e == edge:
                        break
                else:
                    continue
                self.faces.append(f.symbol)
                break
            else:
                assert False, 'No adjacent face for edge: ' + str((self, edge, faces))
        assert len(self.faces) == len(self.vindices)
        
    def flip(self):
        self.vertices.reverse()
        if self.normal is not None:
            for i in range(len(self.normal)):
                self.normal[i] = -self.normal[i]
        
        
class Block (object):
    def __init__(self, model, index, data=None):
        if data is not None:
            self.__dict__.update(data)
            return
        self.index = index
        self.indices = (index % model.sizes[0],
                        index // model.sizes[0] % model.sizes[1],
                        index // model.sizes[0] // model.sizes[1])
        self.coords = [float(2*idx - model.sizes[i] + 1) for i, idx in enumerate(self.indices)]
        self.coords.append(1.)
        self.symbol_to_move = {}
        self.visible_faces = []
        self.visible_glfaces = []
        for symbol in model.faces:
            mdir = symbol not in model.symbols
            maxis = model.symbols.index(model.invers_symbols[symbol] if mdir else symbol)
            mslice = self.axis_to_slice(maxis)
            self.symbol_to_move[symbol] = (maxis, mslice, mdir)
            if mslice == (model.sizes[maxis]-1 if mdir else 0):
                self.visible_faces.append(symbol)
        self.inplace_rotations = []
        
    def init_inplace_rotations(self, model):
        for (blocknum, rotsym), blocknum2 in model.rotated_position.items():
            if blocknum == blocknum2 == self.index and rotsym:
                self.inplace_rotations.append(rotsym)
        self.inplace_rotations = sorted(self.inplace_rotations, key=len)[:2]
        
    def __repr__(self):
        return 'Block({}, {}, {}, {})'.format(self.index, self.indices, self.coords, self.visible_faces)
        
    def axis_to_slice(self, maxis):
        return self.indices[maxis]
        
        
class Geometry (object):
    def __init__(self, vertices=None, faces=None, bevel_factor=None):
        self.vertices = vertices
        self.faces = faces
        self.beveled_vertices = [] # list of vertex positions
        
        for f in self.faces.values():
            f.normal = f.center(self.vertices)
            f.create_scaled(self.vertices, bevel_factor, self.beveled_vertices)
            f.init_adjacent_faces(list(self.faces.values()))
            
    def create_permutation(self, matrix):
        m = np.matrix(matrix)
        indices = {}
        for i, v in enumerate(self.vertices):
            rv = (m * np.matrix(v+[0]).T).T.tolist()[0][:-1]
            ri = self.vertices.index(rv)
            indices[i] = ri
        symbols = {}
        for sym, f in self.faces.items():
            rvindices = [indices[i] for i in f.vindices]
            for rsym, rf in self.faces.items():
                if set(rvindices) == set(rf.vindices):
                    symbols[rsym.lower()] = sym.lower()
        return symbols
        
        
class EmptyModel (object):
    type = 'Empty'
    mformat = 'Empty'
    symbols = ''
    symbolsI = ''
    faces = ''
    
    sizes = [0, 0, 0]
    size = None
    bounding_sphere_radius = 1.
    blocks = []
    
    def __init__(self, *unused_args):
        pass
        
    def gl_vertex_data(self, unused_selection_mode, unused_design):
        return b'', b'', b'', b'', [], 0, [], 0, 0
        
        
class BrickModel (object):
    type = N_('Brick')
    mformat = _('{0}×{1}×{2}-Brick')
    symmetry = 180., 180., 180.
    
    axes = [(-1,0,0), (0,-1,0), (0,0,-1)]   # pylint: disable=C0324
    symbols = 'LDB'
    symbolsI = 'RUF'
    faces = 'UDLRFB'
    epsilon = 0.00001
    
    #### geometry of the cube ####
    #  vertex      standard
    #  indices    orientation    face symbols
    # 2------3                     +---+
    # |\     |\      y             | U |
    # | 6------7     |         +---+---+---+---+
    # | |    | |     o--x      | L | F | R | B |
    # 0-|----1 |      \        +---+---+---+---+
    #  \|     \|       z           | D |
    #   4------5                   +---+
    
    geom = Geometry(
                # vertex-positions, used for unbeveled faces and picking
                vertices=[[-1,-1,-1], [1,-1,-1], [-1, 1,-1], [1, 1,-1],
                          [-1,-1, 1], [1,-1, 1], [-1, 1, 1], [1, 1, 1]],
                # vertex-indices for unbeveled faces, used for picking
                faces=OrderedDict((
                        ('U', Face('U', [2,6,7,3])), ('D', Face('D', [4,0,1,5])),
                        ('L', Face('L', [2,0,4,6])), ('R', Face('R', [7,5,1,3])),
                        ('F', Face('F', [6,4,5,7])), ('B', Face('B', [3,1,0,2])))),
                bevel_factor=0.9
            )
    texpos_tiled = [(0, 0),  (0, 1),  (1, 1),  (1, 0)]
    face_axes = OrderedDict((
                        ('U', (0, 2)), ('D', (0, 2)),
                        ('L', (2, 1)), ('R', (2, 1)),
                        ('F', (0, 1)), ('B', (0, 1))))
    
    def __init__(self, size, mirror_distance):
        self.sizes = self.norm_sizes(size) # tuple of lenght 3
        assert len(self.sizes) == 3
        for _size in self.sizes:
            assert 1 <= _size <= 10
        self.size = size # derived classes can assign a different value
        self.mirror_distance = mirror_distance and mirror_distance * max(self.sizes)
        
        #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.sizes)
        sm = s + (self.mirror_distance or 0)
        self.bounding_sphere_radius = sqrt(2*s*s + sm*sm)
        
        self.invers_symbols = {}
        for sym, isym in zip(self.symbols, self.symbolsI):
            self.invers_symbols[sym] = isym
            self.invers_symbols[isym] = sym
        self.axesI = [tuple(-x for x in a) for a in self.axes]
        
        self.normal_rotation_symbols = {}
        self.rotation_matrices = {}
        self._init_rotations()
        self.face_permutations = self._create_permutations()
        
        if use_modeldata:
            global modeldata_cache
            try:
                modeldata = modeldata_cache[(self.type, self.sizes)]
            except KeyError:
                datafilename = self.get_datafilename(self.sizes)
                datafilename = os.path.join(MODELS_DIR, datafilename)
                with open(datafilename, 'rb') as datafile:
                    modeldata = pickle.load(datafile)
                modeldata_cache = modeldata
                modeldata = modeldata_cache[(self.type, self.sizes)]
            self.blocks = [Block(None, None, data=data) for data in modeldata['blocks']]
            self.rotated_position = modeldata['rotated_position']
            self.gl_never_visible_face = self._gl_never_visible_face_cached
        else:
            nblocks = self.sizes[0] * self.sizes[1] * self.sizes[2]
            self.blocks = [Block(self, i) for i in range(nblocks)]
            self.rotated_position = self._create_rotated_position()
            for block in self.blocks:
                block.init_inplace_rotations(self)
            self.gl_never_visible_face = self._gl_never_visible_face_create_cache
        
        self.pick_vector = {}
        for maxis, axis in enumerate(self.axes):
            for symbol, face in self.geom.faces.items():
                self.pick_vector[maxis, False, symbol] = np.cross(axis, face.normal).tolist()
                self.pick_vector[maxis, True, symbol] = np.cross(face.normal, axis).tolist()
                
        self.pick_data = [()]  # list of (maxis, mslice, mdir, face, center, block, symbol)
        
    @classmethod
    def norm_sizes(cls, sizes):
        return sizes
        
    @classmethod
    def displaystring(cls, size):
        return cls.mformat.format(*size)
        
    def __str__(self):
        return self.displaystring(self.sizes)
        
    @classmethod
    def _create_rotation(cls, axis, angle):
        angle = angle / 180. * pi
        sa = sin(angle)
        ca = cos(angle)
        e_ca = 1 - ca
        n1 = axis[0]
        n2 = axis[1]
        n3 = axis[2]
        m = np.matrix([
            [n1*n1*e_ca + ca,    n1*n2*e_ca - n3*sa, n1*n3*e_ca + n2*sa, 0.],
            [n2*n1*e_ca + n3*sa, n2*n2*e_ca + ca,    n2*n3*e_ca - n1*sa, 0.],
            [n3*n1*e_ca - n2*sa, n3*n2*e_ca + n1*sa, n3*n3*e_ca + ca,    0.],
            [0.,    0.,     0.,     1.],
        ])
        #XXX: try to keep the matrix clean
        mx, my = m.shape
        for y in range(my):
            for x in range(mx):
                if abs(m.A[y][x]) < cls.epsilon:
                    m.A[y][x] = 0.
        return m
        
    @classmethod
    def _matrix_equal(cls, m1, m2):
        assert m1.shape == m2.shape, (m1, m2)
        mx, my = m1.shape
        for y in range(my):
            for x in range(mx):
                if abs(m1.A[y][x] - m2.A[y][x]) > cls.epsilon:
                    return False
        return True
        
    def _init_rotations(self):
        prim = [(sym, self._create_rotation(axis, angle))
                    for axis, sym, angle in zip(self.axes + self.axesI,
                                         self.symbols + self.symbolsI,
                                         self.symmetry + self.symmetry)]
        self.normal_rotation_symbols[''] = ''
        transform = [('', np.matrix(np.identity(4)))]
        for sp, p in prim:
            transform.append((sp, p))
        for sm, m in transform:
            for sp, p in prim:
                n = m * p
                sn = sm + sp
                for st, t in transform:
                    if self._matrix_equal(t, n):
                        self.normal_rotation_symbols[sn] = st
                        break
                else:
                    self.normal_rotation_symbols[sn] = sn
                    transform.append((sn, n))
        for sm, m in transform:
            self.rotation_matrices[sm] = m.A.tolist()
        
    def _create_permutations(self):
        face_permutations = {}
        for msym, matrix in self.rotation_matrices.items():
            face_permutations[msym] = self.geom.create_permutation(matrix)
        return face_permutations
        
    def _create_rotated_position(self):
        rotated_position = {}
        for b, block in enumerate(self.blocks):
            for sym, rotation in self.rotation_matrices.items():
                coords = (np.matrix([block.coords]) * rotation).A.tolist()[0]
                for p, pos in enumerate(self.blocks):
                    if pos.coords == coords:
                        rotated_position[b, sym] = p
                        break
                else:
                    assert False, 'not a permutation'
        return rotated_position
        
    @classmethod
    def get_datafilename(cls, sizes):
        x, y, unused_z = sizes
        if x <= 2:
            return 'mdata_01-02'
        else:
            return 'mdata_{:02}_{}'.format(x, 0 if x<=5 else y%2 if x<=8 else y%3)
        
    def get_savedata(self):
        self.calc_data()
        blocks = [block.__dict__ for block in self.blocks]
        return {'blocks': blocks, 'rotated_position': self.rotated_position}
            
    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 block_indices_to_index(self, indices):
        indices = tuple(indices)
        for b, block in enumerate(self.blocks):
            if block.indices == indices:
                return b
        raise ValueError('Invalid block indices:', indices)
        
    def rotation_symbolic_to_matrix(self, block, sym):
        m = self.rotation_matrices[sym][:]
        m[3] = self.blocks[block].coords
        return m
        
    def block_symbolic_to_block_index(self, symblock):
        indices = [1] * len(self.axes)
        for match in re.finditer(r'(.)(\d*)', symblock):
            blockface, blockslice = match.groups()
            blockface = blockface.upper()
            blockslicenum = int(blockslice)-1 if blockslice else 0
            if blockface in self.symbolsI:
                axis = self.symbolsI.index(blockface)
                blockslicenum = self.sizes[axis]-1 - blockslicenum
                blockface = self.invers_symbols[blockface]
            else:
                axis = self.symbols.index(blockface)
            indices[axis] = blockslicenum
        return self.block_indices_to_index(indices)
        
    def block_index_to_block_symbolic(self, blockpos, rotsym):
        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]
            sym = sym.lower()
            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), '?'
        x, y, z = self.blocks[blockpos].indices
        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
        else:
            assert False, (face, rot)
        
    def rotate_symbolic(self, axis, rdir, block, sym):
        rsym = (self.symbols if not rdir else self.symbolsI)[axis]
        block = self.rotated_position[block, rsym]
        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.lower()].upper()
        rdir = raxissym not in self.symbols
        raxis = self.symbols.index(self.invers_symbols[raxissym] if rdir else raxissym)
        if mdir != rdir:
            mslice = self.sizes[raxis] - 1 - mslice
        return raxis, mslice, rdir
        
    @staticmethod
    def get_selected_move(block, face, edgeno):
        '''block: a block in the rotation slice
           face -> edgeno is the direction of slice rotation
        '''
        edgeno = (edgeno - 1) % 4
        rotation_symbol = face.faces[edgeno]
        return block.symbol_to_move[rotation_symbol]
        
    def compare_move_to_pick(self, maxis, unused_mslice, mdir, face, faceedge):
        axis = self.axes[maxis]
        if mdir:
            vpick = np.cross(axis, face.normal).tolist()
        else:
            vpick = np.cross(face.normal, axis).tolist()
        #TODO: rather test whether face.center+pick_vector intersects with the edge
        return vpick == faceedge.normal
        
    @staticmethod
    def get_selected_move_center(block, face):
        maxis, mslice, mdir = block.symbol_to_move[face.symbol]
        return maxis, mslice, not mdir
        
    def _gl_never_visible_face_cached(self, block, face):   # pylint: disable=R0201
        return face not in block.visible_glfaces
        
    def _gl_never_visible_face_create_cache(self, block, face):
        if not self._gl_never_visible_face_calculated(block, face):
            block.visible_glfaces.append(face)
        # create the cache and discard the real gl face
        return True
        
    def _gl_never_visible_face_calculated(self, block, face):
        if max(self.sizes) == 2 and self.sizes.count(2) >= 2:
            #FIXME: the algorithm to detect invisible faces is wrong,
            # deactivate the test for the cubes that are most affected.
            return False
        vertices = self.geom.beveled_vertices
        c = block.coords
        sqr0 = self.sizes[0] * self.sizes[0]
        sqr1 = self.sizes[1] * self.sizes[1]
        sqr2 = self.sizes[2] * self.sizes[2]
        for vi in face:
            v = [vk+ck for vk, ck in zip(vertices[vi], c)]
            if (v[0]*v[0] + v[1]*v[1] > min(sqr0, sqr1) or
                v[0]*v[0] + v[2]*v[2] > min(sqr0, sqr2) or
                v[1]*v[1] + v[2]*v[2] > min(sqr1, sqr2)):
                return False
        return True
        
    def texpos_tiled_to_mosaic(self, block, face, texpos_tiled):
        axisx, axisy = self.face_axes[face.symbol]
        sizex, sizey = self.sizes[axisx], self.sizes[axisy]
        tptx, tpty = texpos_tiled
        
        vertices = [self.geom.vertices[vi] for vi in face.vindices]
        subx = [(c1-c0)/2 for c0, c1 in zip(vertices[0], vertices[-1])]
        suby = [(c1-c0)/2 for c0, c1 in zip(vertices[0], vertices[1])]
        coords = block.coords[:3]
        tpmx = sum(tc*bc for tc, bc in zip(subx, coords))
        tpmy = sum(tc*bc for tc, bc in zip(suby, coords))
        
        texx = ((sizex-1 + tpmx) / 2. + tptx) / sizex
        texy = ((sizey-1 + tpmy) / 2. + tpty) / sizey
        return texx, texy
        
    def gl_label_quads(self, block, visible):
        if DEBUG:
            if DEBUG_INNER:
                for i in range(3):
                    if block.indices[i] in [0, self.sizes[i]-1]:
                        return
            if DEBUG_NOLABEL and visible:
                return
            if DEBUG_NOBEVEL and not visible:
                return
        for faceno, symbol in enumerate(self.faces):
            face = self.geom.faces[symbol]
            if (symbol in block.visible_faces) != visible:
                continue
            f = face.scaled
            if self.gl_never_visible_face(block, f.vindices):
                continue
            for i in [0, 1, 2, 0, 2, 3]:
                vi = f.vindices[i]
                v = self.geom.beveled_vertices[vi]
                texpos_tiled = self.texpos_tiled[i]
                texpos_mosaic = self.texpos_tiled_to_mosaic(block, face, texpos_tiled)
                yield (v, face.normal, faceno, texpos_tiled, texpos_mosaic)
            if self.mirror_distance is not None and visible:
                f = f.translated(self.geom.beveled_vertices, face.normal, self.mirror_distance)
                f.normal = face.normal[:]
                f.flip()
                for i in [0, 1, 2, 0, 2, 3]:
                    v = f.vertices[i]
                    tptx, tpty = self.texpos_tiled[i]
                    texpos_tiled = 1. - tptx, tpty
                    texpos_mosaic = self.texpos_tiled_to_mosaic(block, face, texpos_tiled)
                    yield (v, f.normal, faceno, texpos_tiled, texpos_mosaic)
                    
    def gl_beveled_quads(self, block):
        '''For every edge create a face'''
        if DEBUG:
            if DEBUG_INNER:
                for i in range(3):
                    if block.indices[i] in [0, self.sizes[i]-1]:
                        return
            if DEBUG_NOBEVEL:
                return
        for symbol, f in self.geom.faces.items():
            for symbol2, (vi, vi2) in zip(f.faces, f.edges()):
                if symbol >= symbol2: # find the corners only once, no matter in which order
                    continue
                f2 = self.geom.faces[symbol2]
                # f and f2 have a common edge (vi,vi2)
                # we now need the vertices and normals of the adjacent labels.
                # remember, the face must be counterclockwise!
                bvi1 = f.scaled.vindices[f.vindices.index(vi2)]
                bvi2 = f.scaled.vindices[f.vindices.index(vi)]
                bvi3 = f2.scaled.vindices[f2.vindices.index(vi)]
                bvi4 = f2.scaled.vindices[f2.vindices.index(vi2)]
                if self.gl_never_visible_face(block, [bvi1, bvi2, bvi3, bvi4]):
                    continue
                yield self.geom.beveled_vertices[bvi1], f.normal
                yield self.geom.beveled_vertices[bvi2], f.normal
                yield self.geom.beveled_vertices[bvi3], f2.normal
                yield self.geom.beveled_vertices[bvi1], f.normal
                yield self.geom.beveled_vertices[bvi3], f2.normal
                yield self.geom.beveled_vertices[bvi4], f2.normal
                
    def gl_beveled_triangles(self, block):
        '''For every corner create a face'''
        if DEBUG:
            if DEBUG_INNER:
                for i in range(3):
                    if block.indices[i] in [0, self.sizes[i]-1]:
                        return
            if DEBUG_NOBEVEL:
                return
        for vi in range(len(self.geom.vertices)):
            bf = [] # one beveled face for each vertex
            bn = []
            for face_first in self.geom.faces.values():
                if vi in face_first.vindices:
                    # we now have one adjacent face, this code should be reached if the model is valid
                    break
            else:
                face_first = None
                assert False
            face = face_first
            while True:
                ivi = face.vindices.index(vi)
                bvi = face.scaled.vindices[ivi]
                bf.insert(0, bvi) # we need counterclockwise order
                bn.insert(0, face.normal)
                symbol = face.faces[ivi]
                face = self.geom.faces[symbol]
                ## we now have the clockwise next face
                if face_first.symbol == face.symbol:
                    break
            if self.gl_never_visible_face(block, bf):
                continue
            for vi, n in zip(bf, bn):
                yield self.geom.beveled_vertices[vi], n
            
    def gl_pick_triangles(self, selection_mode):
        def edge_center(v1, v2):
            return [(_v1 + _v2) / 2 for _v1, _v2 in zip(v1, v2)]
        for block in self.blocks:
            cnt_faces = 0
            for i in range(3):
                if block.indices[i] in [0, self.sizes[i]-1]:
                    cnt_faces += 1
            for face, symbol in enumerate(self.faces):
                f = self.geom.faces[symbol]
                assert f.symbol == symbol
                if symbol not in block.visible_faces:
                    continue
                cnt_n = 0
                for _sym in f.faces:
                    if _sym in block.visible_faces:
                        cnt_n += 1
                ft = f.translated(self.geom.vertices, block.coords)
                vc = ft.center()
                if self.mirror_distance is None:
                    ftt = ft
                else:
                    ftt = ft.translated(None, f.normal, self.mirror_distance)
                vct = ftt.center()
                
                if selection_mode == 1 and cnt_n == 0:
                    maxis, mslice, mdir = self.get_selected_move_center(block, f)
                    color = len(self.pick_data)
                    self.pick_data.append((maxis, mslice, mdir, face, True, block, symbol, None, None))
                    for i in (0,1,2, 2,3,0):    # pylint: disable=C0324
                        yield color, ft.vertices[i]
                    if self.mirror_distance is not None:
                        color = len(self.pick_data)
                        self.pick_data.append((maxis, mslice, not mdir, face, True, block, symbol, None, None))
                        for i in (0,2,1, 2,0,3):    # pylint: disable=C0324
                            yield color, ftt.vertices[i]
                elif selection_mode == 1 and cnt_n == 1 and cnt_faces == 2:
                    for edgeno, symboledge in enumerate(f.faces): # find the other face on the block
                        if symboledge in block.visible_faces:
                            break
                    else:
                        edgeno = symboledge = None
                        assert False
                    maxis, mslice, mdir = self.get_selected_move(block, f, edgeno)
                    if DEBUG:
                        assert self.compare_move_to_pick(maxis, mslice, mdir, f, self.geom.faces[symboledge])
                    ec = edge_center(*list(ft.edges())[edgeno])
                    color = len(self.pick_data)
                    self.pick_data.append((maxis, mslice, mdir, face, False, block, symbol, vc, ec))
                    for i in (0,1,2, 2,3,0):    # pylint: disable=C0324
                        yield color, ft.vertices[i]
                    if self.mirror_distance is not None:
                        for i in (0,2,1, 2,0,3):    # pylint: disable=C0324
                            yield color, ftt.vertices[i]
                elif selection_mode == 1 and cnt_n == 2 and cnt_faces == 3:
                    # find the two other faces on the block
                    for i1, symboledge1 in enumerate(f.faces): # find the other face on the block
                        i2 = (i1 + 1) % 4
                        symboledge2 = f.faces[i2]
                        visible_faces = block.visible_faces
                        if symboledge1 in visible_faces and symboledge2 in visible_faces:
                            break
                    else:
                        i1 = symboledge1 = None
                        assert False
                    symboledges = (i1, symboledge1, i1-1), (i2, symboledge2, i2)
                    for edgeno, symboledge, offset in symboledges:
                        maxis, mslice, mdir = self.get_selected_move(block, f, edgeno)
                        if DEBUG:
                            assert self.compare_move_to_pick(maxis, mslice, mdir, f, self.geom.faces[symboledge])
                        ec = edge_center(*list(ft.edges())[edgeno])
                        color = len(self.pick_data)
                        self.pick_data.append((maxis, mslice, mdir, face, False, block, symbol, vc, ec))
                        for i in (0, 1, 2):
                            yield color, ft.vertices[(i+offset)%4]
                        if self.mirror_distance is not None:
                            for i in (0, 2, 1):
                                yield color, ftt.vertices[(i+offset)%4]
                else:
                    for edgeno, (edge, edget) in enumerate(zip(ft.edges(), ftt.edges())):
                        maxis, mslice, mdir = self.get_selected_move(block, f, edgeno)
                        ec = edge_center(*edge)
                        color = len(self.pick_data)
                        self.pick_data.append((maxis, mslice, mdir, face, False, block, symbol, vc, ec))
                        # pylint: disable=C0321
                        yield color, vc; yield color, edge[0]; yield color, edge[1]
                        if self.mirror_distance is not None:
                            yield color, vct; yield color, edget[1]; yield color, edget[0]
                    
    def gl_vertex_data(self, selection_mode, design):
        vertices = []
        normals = []
        colors = []
        texpos = []
        # list of [count vertices, face index]
        labelinfos = [None for unused_b in self.blocks]
        cnts_block = [0 for unused_b in self.blocks]
        
        # label vertices
        for iblock, block in enumerate(self.blocks):
            cnt = 0
            facesinfo = []
            last_faceno = -1
            for v, n, f, tpt, tpm in self.gl_label_quads(block, True):
                vertices += v
                normals += n
                colors += design.label_colors[f]
                texpos += tpm if design.imagemodes[f] else tpt
                if last_faceno != f:
                    cnt = 1
                    facesinfo.append((cnt, f))
                    last_faceno = f
                else:
                    cnt += 1
                    facesinfo[-1] = (cnt, f)
            labelinfos[iblock] = facesinfo
            
        # bevel, hidden labels
        idx_block = len(vertices) // 3
        for iblock, block in enumerate(self.blocks):
            cnt = 0
            for v, n in self.gl_beveled_quads(block):
                vertices += v
                normals += n
                colors += design.bevel_color
                cnt += 1
            for v, n, f, tpt, tpm in self.gl_label_quads(block, False):
                vertices += v
                normals += n
                colors += design.bevel_color
                cnt += 1
            for v, n in self.gl_beveled_triangles(block):
                vertices += v
                normals += n
                colors += design.bevel_color
                cnt += 1
            cnts_block[iblock] = cnt
        
        pickvertices, pickcolors = self.gl_pick_data(selection_mode)
        idx_pick = len(vertices) // 3
        cnt_pick = len(pickvertices) // 3
        
        vertices += pickvertices
        colors += pickcolors
        
        idx_debug = len(vertices) // 3
        if DEBUG_DRAW:
            x = float(-self.sizes[0])
            y = float(-self.sizes[1])
            z = float(-self.sizes[2])
            # axes
            vertices += [x-1, y-1, z,  -x, y-1, z]
            colors += [255, 0, 0,  255, 0, 0]
            vertices += [x-1, y-1, z,  x-1, -y, z]
            colors += [0, 255, 0,  0, 255, 0]
            vertices += [x-1, y-1, z,  x-1, y-1, -z]
            colors += [0, 0, 255,  0, 0, 255]
            # selection (modelview)
            vertices += [0, 0, 0,  1, 1, 1]
            colors += [255, 255, 255,  255, 255, 255]
            # selection (viewport)
            vertices += [0, 0, 0,  1, 1, 0]
            colors += [255, 255, 0,  255, 0, 0]
            
        if DEBUG:
            assert sum([cnt for _bi in labelinfos for cnt, unused_faceno in _bi]) == idx_block
        
        return (    array.array('f', vertices).tobytes(),
                    array.array('f', normals).tobytes(),
                    array.array('B', colors).tobytes(),
                    array.array('f', texpos).tobytes(),
                    labelinfos,
                    idx_block, cnts_block,
                    idx_pick, cnt_pick,
                    idx_debug,
               )
                
    def calc_data(self):
        '''Function to verify that Block.visible_glfaces lists are initialized.'''
        for iblock, block in enumerate(self.blocks):
            for v, n, f, tpt, tpm in self.gl_label_quads(block, True):
                pass
            for v, n in self.gl_beveled_quads(block):
                pass
            for v, n, f, tpt, tpm in self.gl_label_quads(block, False):
                pass
            for v, n in self.gl_beveled_triangles(block):
                pass
                
    def gl_pick_data(self, selection_mode):
        # Pick TRIANGLES vertices
        vertices = []
        colors = []
        for col, v in self.gl_pick_triangles(selection_mode):
            vertices += v
            color = [(col>>4) & 0xf0, (col) & 0xf0, (col<<4) & 0xf0]
            colors += color
        assert len(vertices) == len(colors)
        return vertices, colors
        
        
class TowerModel (BrickModel):
    type = N_('Tower')
    mformat = _('{0}×{1}-Tower')
    symmetry = 180., 90., 180.
    
    def __init__(self, size, mirror_distance):
        BrickModel.__init__(self, size, mirror_distance)
        self.size = size[:2]
        
    @classmethod
    def norm_sizes(cls, sizes):
        x, y, unused_z = sizes
        return x, y, x
        
        
class CubeModel (BrickModel):
    type = N_('Cube')
    mformat = _('{0}×{0}×{0}-Cube')
    symmetry = 90., 90., 90.
    
    def __init__(self, size, mirror_distance):
        BrickModel.__init__(self, size, mirror_distance)
        self.size = size[0],
        
    @classmethod
    def norm_sizes(cls, sizes):
        x, unused_y, unused_z = sizes
        return x, x, x
        
        
empty_model = EmptyModel()
models = [CubeModel, TowerModel, BrickModel]

def from_string(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)
    mtype, width, height, depth, exp = match.group(1, 2, 4, 5, 6)
    for Model in models:
        if mtype == Model.type:
            break
    else:
        Model = None
        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, Model, sizes, exp
    
    
