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

#  Copyright © 2012-2015  B. Clausius <barcc@gmx.de>
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.


import sys, os
sys.path.insert(0, '.')
import pickletools
from multiprocessing import Pool, cpu_count
from collections import defaultdict, OrderedDict
from bisect import bisect_left
from array import array
from itertools import chain
from math import sin, cos, pi

from pybiklib.debug import DEBUG, DEBUG_MSG, DEBUG_NOLABEL, DEBUG_NOBEVEL, DEBUG_INVISIBLE, DEBUG_INDEXONLY
from pybiklib.modelcommon import epsilon, filebyteorder, get_texcoords_range
# executable script needs absolute imports
from buildlib.geom import roundeps, Vector
from buildlib.modeldef import modeldefs


dumps = None

def make_dumps(reproducible):
    global dumps
    import io
    import pickle
    if not reproducible:
        dumps = pickle.dumps
        return
    try:
        class _Pickler (pickle._Pickler):
            def save_dict(self, obj):
                if self.bin:
                    self.write(pickle.EMPTY_DICT)
                else:   # proto 0 -- can't use EMPTY_DICT
                    self.write(pickle.MARK + pickle.DICT)
                self.memoize(obj)
                self._batch_setitems(sorted(obj.items()))
                
            pickle._Pickler.dispatch[dict] = save_dict
            
        def _dumps(obj, protocol=None, *, fix_imports=True):
            f = io.BytesIO()
            try:
                _Pickler(f, protocol, fix_imports=fix_imports).dump(obj)
                return f.getvalue()
            except Exception:
                print('warning: using undocumented interface in module pickle failed, models will not be reproducible:')
                sys.excepthook(*sys.exc_info())
                return pickle.dumps(obj, protocol, fix_imports=fix_imports)
        _dumps({1:2, 3:4})
    except Exception:
        print('warning: using undocumented interface in module pickle failed, models will not be reproducible:')
        sys.excepthook(*sys.exc_info())
        _dumps = pickle.dumps
    dumps = _dumps
        
def equal_vector_fuzzy(v1, v2):
    for v1k, v2k in zip(v1, v2):
        if abs(v1k - v2k) > epsilon:
            return False
    return True
    
    
class VectorData:
    __slots__ = 'vectors indices sorted cnts dups toindex'.split()
    
    def __init__(self, fast):
        self.vectors = []
        self.indices = []
        self.sorted = []
        self.cnts = 0
        self.dups = 0
        self.toindex = self.toindex_fast if fast else self.toindex_dedup
        
    def toindex_dedup(self, vector):
        vector = vector.rounded()
        vector = [(0. if v == 0. else v) for v in vector]  # 0.0 == -0.0
        self.cnts += 1
        si = bisect_left(self.sorted, vector)
        i = len(self.vectors)
        if si < i and self.sorted[si] == vector:
            self.dups += 1
            return self.indices[si]
        self.vectors.append(vector)
        self.sorted.insert(si, vector)
        self.indices.insert(si, i)
        return i
        
    def toindex_fast(self, vector):
        vector = vector.rounded()
        i = len(self.vectors)
        self.vectors.append(vector)
        return i
        
        
class ModelFactory:
    mtype_attributes = (
            'name', 'mformat', 'sizenames', 'defaultsize',
            'symmetries', 'axes', 'symbols', 'symbolsI', 'faces', 'facekeys',
            'normal_rotation_symbols', 'rotation_symbols', 'rotation_matrices',
            'face_permutations', 'default_rotation')
        
    def __init__(self, modeldef):
        self.modeldef = modeldef()
        self.create_rotations()
        
    def __getattr__(self, attrname):
        return getattr(self.modeldef, attrname)
        
    @staticmethod
    def _matrix_equal(m1, m2):
        for line1, line2 in zip(m1, m2):
            for value1, value2 in zip(line1, line2):
                if abs(value1 - value2) > epsilon:
                    return False
        return True
        
    @staticmethod
    def _mult_matrix_vector3(matrix, vector):
        return Vector(sum(matrix[i][k]*vector[k] for k in range(3)) for i in range(3))
        
    @staticmethod
    def _mult_matrix(matrix1, matrix2):
        return [[sum(matrix1[i][k]*matrix2[k][j] for k in range(4)) for j in range(4)] for i in range(4)]
        
    @staticmethod
    def _create_rotation(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 = [
            [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
        for y, line in enumerate(m):
            for x, value in enumerate(line):
                if abs(value) < epsilon:
                    m[y][x] = 0.
        return m
        
    def _create_permutation(self, matrix):
        permutation = {}
        for sym1, symI1, axis1 in zip(self.symbols, self.symbolsI, self.axes):
            for sym2, symI2, axis2 in zip(self.symbols, self.symbolsI, self.axes):
                axisR = self._mult_matrix_vector3(matrix, axis1)
                if axisR.equalfuzzy(axis2):
                    permutation[sym2] = sym1
                    permutation[symI2] = symI1
                elif axisR.inversfuzzy(axis2):
                    permutation[symI2] = sym1
                    permutation[sym2] = symI1
        return permutation
        
    def create_rotations(self):
        prim = []
        for axis, sym, symI, symmetry in zip(self.axes, self.symbols, self.symbolsI, self.symmetries):
            angle = 360. / symmetry
            # for some models (towers, bricks) rotations are equal to the invers rotations,
            # but the symbols are different
            prim.append((sym, self._create_rotation(axis, angle)))
            prim.append((symI, self._create_rotation(axis, -angle)))
        self.normal_rotation_symbols = {'': ''}
        transform = [['', [[1.,0.,0.,0.],[0.,1.,0.,0.],[0.,0.,1.,0.],[0.,0.,0.,1.]]]]
        for sp, p in prim:
            transform.append([sp, p])
        for sm, m in transform:
            for sp, p in prim:
                n = self._mult_matrix(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])
        self.rotation_symbols = [s for s, m in transform]
        self.rotation_matrices = [m for s, m in transform]
        self.face_permutations = {s: self._create_permutation(m) for s, m in transform}
        
    def get_rotated_position(self, cells):
        rotated_position = {}
        centers = [c.center() for c in cells]
        for b, center in enumerate(centers):
            for sym, rotation in zip(self.rotation_symbols, self.rotation_matrices):
                coords = self._mult_matrix_vector3(rotation, center)
                for p, center2 in enumerate(centers):
                    if equal_vector_fuzzy(center2, coords):
                        if sym not in rotated_position:
                            rotated_position[sym] = [0] * len(cells)
                        rotated_position[sym][p] = b
                        break
                else:
                    assert False, 'not a permutation'
        return rotated_position
        
    @staticmethod
    def mark_invisible_faces_bevel(polys):
        for cell in polys.cells:
            for other in ['cut', 'bevel']:
                for hf in cell.halffaces:
                    if hf.face.type != 'bevel':
                        continue
                    other_removed = False
                    for he in hf.halfedges:
                        face_ = he.other.halfface.face
                        if face_.type == 'face':
                            break
                        if face_.type == other:
                            break
                        if face_.type.endswith('_removed'):
                            other_removed = True
                    else:
                        if other_removed:
                            hf.face.type += '_removed'
                            
    def get_data(self, sizes, vectordata):
        polys, slice_centers = self.create_cells(sizes)
        
        normals_polys = OrderedDict()  # for reproducible build
        for cell in polys.cells:
            for hf in cell.halffaces:
                key = (cell.indices, hf.face.id)
                normals_polys[key] = self.normals[hf.face.id] if hf.face.type == 'face' else hf.normal()
        data_cells_visible_faces = [[f.id for f in cell.faces if f.type == 'face'] for cell in polys.cells]
        normals = [self.normals[sym] for sym in self.faces]
        polys_verts = [v.point for v in polys.verts]
        data_texranges_mosaic = [list(get_texcoords_range(polys_verts, normal)) for normal in normals]
        data_normals = [vectordata.toindex(normal) for normal in normals]
        data_rotated_position = self.get_rotated_position(polys.cells)
        data_cell_indices = [cell.indices for cell in polys.cells]
        data_pick_polygons = list(self.gl_pick_polygons(polys.cells, vectordata))
        
        # beveled cells
        normals_polys = {k: vectordata.toindex(n) for k, n in normals_polys.items()}
        polys_beveled = polys
        polys_beveled.bevel(self.bevel_width)
        self.mark_invisible_faces_bevel(polys_beveled)
        if DEBUG_INVISIBLE:
            for face in polys_beveled.faces:
                if face.type.endswith('_removed'):
                    face.type = face.type.rsplit('_', 1)[0]
                else:
                    face.type += '_removed'
        polys_beveled.remove_faces(lambda f: f.type.endswith('_removed'))
        
        data_block_polygons = [[list(self.gl_label_polygons(cell, vectordata)),
                                list(self.gl_black_polygons(cell, normals_polys, vectordata))]
                                    for cell in polys_beveled.cells]
        return {
                'rotated_position': data_rotated_position,
                'cell_indices': data_cell_indices,
                'cells_visible_faces': data_cells_visible_faces,
                'texranges_mosaic': data_texranges_mosaic,
                'normals': data_normals,
                'block_polygons': data_block_polygons,
                'pick_polygons': data_pick_polygons,
                'slice_centers': slice_centers,
            }
        
    def gl_label_polygons(self, cell, vectordata):
        if DEBUG:
            if DEBUG_NOLABEL:
                return
        for halfface in cell.halffaces:
            if halfface.face.type != 'face':
                continue
            symbol = halfface.face.id
            faceno = self.faces.index(symbol)
            cell_vertices = [vectordata.toindex(he.verts[0].point) for he in halfface.halfedges]
            cell_vertices.append(faceno)
            yield cell_vertices
            
    @staticmethod
    def gl_black_polygons(cell, inormals, vectordata):
        if DEBUG:
            if DEBUG_NOBEVEL:
                return
        for halfface in cell.halffaces:
            if halfface.face.type == 'face':
                continue
            vertices = [he.verts[0] for he in halfface.halfedges]
            if halfface.face.type == 'cut':
                inormal = inormals[(cell.indices, halfface.face.id)]
                vnit = ((vectordata.toindex(v.point), inormal) for v in vertices)
                yield list(chain(*vnit))
            elif halfface.face.type == 'bevel':
                vnit = ((vectordata.toindex(v.point), inormals[(cell.indices, v.id)])
                            for v in vertices)
                yield list(chain(*vnit))
                
    def gl_pick_polygons(self, cells, vectordata):
        for cellidx, cell in enumerate(cells):
            for halfface in cell.halffaces:
                if halfface.face.type != 'face':
                    continue
                symbol = halfface.face.id
                face = self.faces.index(symbol)
                visible_face_indices = [i for i, he in enumerate(halfface.halfedges)
                                          if he.other.halfface.face.type == 'face']
                edges_iverts = [vectordata.toindex(he.verts[0].point) for he in halfface.halfedges]
                if len(visible_face_indices) == 0:
                    picktype = 0
                elif len(visible_face_indices) == 1:
                    if len(edges_iverts) == 3:
                        picktype = 1
                    elif len(edges_iverts) == 4:
                        picktype = 2
                    elif len(edges_iverts) == 6:
                        picktype = 4
                    else:
                        assert False, len(edges_iverts)
                    i = visible_face_indices[0]
                    edges_iverts = edges_iverts[i:] + edges_iverts[:i]
                elif len(visible_face_indices) == 2:
                    assert 3 <= len(edges_iverts) <= 5, len(edges_iverts)
                    i = visible_face_indices[0]
                    if len(edges_iverts) < 5:
                        picktype = 3
                    else:
                        picktype = 5
                    if i+1 == visible_face_indices[1]:
                        edges_iverts = edges_iverts[i:] + edges_iverts[:i]
                    elif visible_face_indices == [0, len(edges_iverts)-1]:
                        i = visible_face_indices[1]
                        edges_iverts = edges_iverts[i:] + edges_iverts[:i]
                    else:
                        picktype = -1
                else:
                    picktype = -1
                yield [cellidx, [face, picktype], edges_iverts]
                
        

class Dedup:
    def __init__(self):
        self.dedup_data = defaultdict(list)
        self.cnt_dups = 0
        self.cnt_values = 0
        
    def _float(self, value):
        self.cnt_values += 1
        dddlt = self.dedup_data['f']
        value = roundeps(value)
        if value == 0.0:  # 0.0 == -0.0
            value = 0.0  # for reproducible build
        didx = bisect_left(dddlt, value)
        if didx < len(dddlt) and dddlt[didx] == value:
            self.cnt_dups += 1
            return dddlt[didx], True, 'f'
        else:
            dddlt.insert(didx, value)
            return value, False, 'f'
            
    def _recursion(self, value, iterable):
        dedup = True
        vtypes = []
        for k, v in iterable:
            v, d, t = self.dedup(v)
            dedup = dedup and d
            vtypes.append(t)
            value[k] = v
        return dedup, '('+''.join(vtypes)+')'
        
    def _replace(self, value, dedup, vtype):
        self.cnt_values += 1
        dddlt = self.dedup_data[vtype]
        try:
            return self._replace_bisect(dddlt, value, dedup, vtype)
        except TypeError:
            return self._replace_index(dddlt, value, dedup, vtype)
            
    def _replace_index(self, dddlt, value, dedup, vtype):
        if not dedup:
            #assert value not in dddlt
            dddlt.append(value)
            return value, False, vtype
        try:
            didx = dddlt.index(value)
        except ValueError:
            dddlt.append(value)
            return value, False, vtype
        else:
            dvalue = dddlt[didx]
            self.cnt_dups += 1
            return dvalue, True, vtype
        
    def _replace_bisect(self, dddlt, value, unused_dedup, vtype):
        didx = bisect_left(dddlt, value)
        try:
            dvalue = dddlt[didx]
        except IndexError:
            dddlt.append(value)
            return value, False, vtype
        if value == dvalue:
            self.cnt_dups += 1
            return dvalue, True, vtype
        else:
            dddlt.insert(didx, value)
            return value, False, vtype
            
    def dedup(self, value):
        if isinstance(value, dict):
            value = {(sys.intern(k) if type(k) is str else k):v for k,v in value.items()}
            dedup, vtype = self._recursion(value, value.items())
            return self._replace(value, dedup, 'd' + vtype)
        elif isinstance(value, list):
            dedup, vtype = self._recursion(value, enumerate(value))
            return self._replace(tuple(value), dedup, 'a' + vtype)
        elif isinstance(value, tuple):
            value = list(value)
            dedup, vtype = self._recursion(value, enumerate(value))
            return self._replace(tuple(value), dedup, 'a' + vtype)
        elif isinstance(value, float):
            return self._float(value)
        elif isinstance(value, int):
            return value, True, 'i'
        elif isinstance(value, str):
            return sys.intern(value), True, 's'
        elif isinstance(value, type(None)):
            return value, True, 'n'
        else:
            assert False, type(value)
            
def pool_functions(parallel, reproducible):
    pool = None
    if reproducible:
        print('warning: using undocumented interface in module pickle for reproducible build')
    try:
        if parallel > 1:
            pool = Pool(processes=parallel, initializer=make_dumps, initargs=[reproducible])
    except OSError as e:
        print('process pool not available ({}):'.format(e))
        print('  deactivating multiprocessing')
    sys.stdout.flush() # when Pool(…) fails this line is 
    if pool is not None:
        return pool.imap_unordered
    else:
        make_dumps(reproducible)
        return map
        
#XXX: speedup parallel builds, longer jobs first
fileorder = 'b111 b101 b000 p111111 b010 p101111 b100 b110 p010000 p000000 b011 t1111 b001 t0000 t1011 t0100'.split() 

def get_datafilename(fileclass, sizes, fast):
    if fast:
        return fileclass + ''.join(str(s-1) for s in sizes)
    else:
        sizes = fileclass + ''.join(str((s-1)%2) for s in sizes)
        try:
            priority = fileorder.index(sizes) + 1
        except ValueError:
            priority = 99
        return '{:02}'.format(priority) + sizes
        
def enum_modelfiles(fast, maxsize, Factory):
    if isinstance(maxsize, tuple):
        minsize, maxsize = maxsize
    else:
        minsize = 1
    maxsize += 1
    def tuples(maxlen, part=()):
        if maxlen == len(part):
            yield part
        else:
            for i in range(minsize, maxsize):
                yield from tuples(maxlen, part+(i,))
    for size in tuples(len(Factory.sizenames)):
        norm_size, sizes = Factory.norm_sizes(size)
        if sizes is None:
            continue
        filename = get_datafilename(Factory.fileclass, sizes, fast)
        yield filename, sizes, size, norm_size
            
def pool_enum_modelfiles(dirname, testfunc, fast, maxsize):
    ignored = []
    modelfiles = []
    for Factory in modeldefs:
        for filename, *unused in enum_modelfiles(fast, maxsize, Factory):
            filename = os.path.join(dirname, filename)
            if filename in modelfiles or filename in ignored:
                continue
            if testfunc(filename):
                modelfiles.append(filename)
            else:
                ignored.append(filename)
    if modelfiles:
        for filename in sorted(ignored):
            print('skipping', filename)
    return sorted(modelfiles)
    
def pool_create_modelfiledata(path, fast, maxsize):
    filename = os.path.basename(path)
    savedata = {}
    vectordata = VectorData(fast)
    for Factory in modeldefs:
        sizes_list = []
        factory = None
        for _filename, sizes, *unused in enum_modelfiles(fast, maxsize, Factory):
            if _filename != filename:
                continue
            if sizes in sizes_list:
                continue
            sizes_list.append(sizes)
            if factory is None:
                factory = ModelFactory(Factory)
            savedata.setdefault(factory.type, {})[sizes] = factory.get_data(sizes, vectordata)
    vectors = array('f', chain.from_iterable(vectordata.vectors))
    return savedata, vectors, vectordata.cnts, vectordata.dups
    
def format_seconds(seconds):
    if seconds > 60:
        return '{:.0f}m {:.2f}s'.format(*divmod(seconds, 60))
    else:
        return '{:.2f}s'.format(seconds)
    
def pool_create_modelfile(filename, fast, pickle_protocol, maxsize):
    import time
    seconds = time.process_time()
    
    savedata, vectors, cnt_vectors, dup_vectors = pool_create_modelfiledata(filename, fast, maxsize)
    vals = dups = ''
    if not fast:
        dedup = Dedup()
        savedata = dedup.dedup(savedata)[0]
        if DEBUG_MSG:
            vals = '\n  vals: %6s  vec: %6s' % (dedup.cnt_values, cnt_vectors)
            dups = '\n  dups: %6s       %6s' % (dedup.cnt_dups, dup_vectors)
    
    savedata = dumps(savedata, pickle_protocol)
    datasize = ['{:.1f} kb'.format(len(savedata) / 1000), '---']
    vectorssize = '{:.1f} kb'.format(len(vectors) * vectors.itemsize / 1000)
    if not fast:
        savedata = pickletools.optimize(savedata)
        datasize[1] = '{:.1f} kb'.format(len(savedata) / 1000)
    seconds = time.process_time() - seconds
    
    vlen = array('I', [len(vectors)])
    if sys.byteorder != filebyteorder:
        vlen.byteswap()
        vectors.byteswap()
    with open(filename, 'wb') as datafile:
        datafile.write(savedata)
        vlen.tofile(datafile)
        vectors.tofile(datafile)
    return filename, seconds, 'generated {}, ({}, {}), {}, {} vectors {}{}{}'.format(
                    filename,
                    datasize[0], datasize[1], format_seconds(seconds), len(vectors) / 3, vectorssize,
                    vals, dups)
    
def pool_create_indexdata(fast, maxsize):
    savedata_type = {}
    savedata_types = []
    savedata_size = {}
    savedata_sizes = {}
    savedata_facenames = []
    savedata = {'type': savedata_type, 'types': savedata_types,
                'normsize': savedata_size, 'sizes': savedata_sizes,
                'facenames': savedata_facenames,
               }
    facekeys = []
    for Factory in modeldefs:
        factory = ModelFactory(Factory)
        savedata_type[factory.type] = {attr: getattr(factory, attr) for attr in factory.mtype_attributes}
        savedata_types.append(factory.type)
        savedata_size_type = {}
        savedata_sizes_type = {}
        savedata_size[factory.type] = savedata_size_type
        savedata_sizes[factory.type] = savedata_sizes_type
        for facekey, facename in zip(factory.facekeys, factory.facenames):
            if facekey not in facekeys:
                facekeys.append(facekey)
                savedata_facenames.append((facekey, facename))
        for filename, sizes, size, norm_size in enum_modelfiles(fast, maxsize, Factory):
            savedata_size_type[size] = norm_size
            if norm_size in savedata_sizes_type:
                assert [sizes, filename] == savedata_sizes_type[norm_size]
            else:
                savedata_sizes_type[norm_size] = [sizes, filename]
        size_range = ((min(vals), max(vals)) for vals in zip(*savedata_size_type.keys()))
        defaultsize = savedata_type[factory.type]['defaultsize']
        defaultsize = tuple(max(smin, min(s, smax)) for s, (smin, smax) in zip(defaultsize, size_range))
        savedata_type[factory.type]['defaultsize'] = defaultsize
    return savedata
    
def pool_create_indexfile(filename, fast, pickle_protocol, maxsize):
    import time
    seconds = time.process_time()
    
    savedata = pool_create_indexdata(fast, maxsize)
    vals = dups = ''
    if not fast:
        dedup = Dedup()
        savedata = dedup.dedup(savedata)[0]
        if DEBUG_MSG:
            vals = '\n  vals: %6s' % dedup.cnt_values
            dups = '\n  dups: %6s' % dedup.cnt_dups
    savedata = dumps(savedata, pickle_protocol)
    datasize = ['{:.1f} kb'.format(len(savedata) / 1000), '---']
    if not fast:
        savedata = pickletools.optimize(savedata)
        datasize[1] = '{:.1f} kb'.format(len(savedata) / 1000)
    seconds = time.process_time() - seconds
    
    with open(filename, 'wb') as datafile:
        datafile.write(savedata)
    return filename, seconds, 'generated {}, ({}, {}), {}{}{}'.format(
                    filename,
                    datasize[0], datasize[1], format_seconds(seconds),
                    vals, dups)
    
def pool_run(args):
    func, *args = args
    return func(*args)
    
def get_indexfilename(dirname):
    return os.path.join(dirname, 'index')
    
def create_modeldata(dirname, testfunc=None, parallel=1, fast=False, pickle_protocol=-1, maxsize=10, reproducible=False):
    # prepare jobs
    if testfunc is None:
        testfunc = lambda arg: True
    if DEBUG_INDEXONLY:
        modelfiles = []
    else:
        modelfiles = pool_enum_modelfiles(dirname, testfunc, fast, maxsize)
    jobs = [(pool_create_modelfile, m, fast, pickle_protocol, maxsize) for m in modelfiles]
    indexfilename = get_indexfilename(dirname)
    jobs.append((pool_create_indexfile, indexfilename, fast, pickle_protocol, maxsize))
    # run jobs
    if parallel is True:
        parallel = cpu_count()
    if not parallel or parallel < 1:
        parallel = 1
    realparallel = min(parallel, len(jobs))
    print('using {} / {} processes'.format(realparallel, parallel))
    imap_model = pool_functions(realparallel, reproducible)
    result = []
    for filename, seconds, lines in imap_model(pool_run, jobs):
        result.append((filename, seconds))
        print(lines)
        sys.stdout.flush()
    if DEBUG_MSG and not fast:
        result.sort(key=lambda fs: fs[1], reverse=True)
        if result != sorted(result, key=lambda fs: fs[0]):
            print('warning: reorder jobs to optimize parallel build')
            for filename, seconds in result:
                print(' ', os.path.basename(filename), format_seconds(seconds))
    
    
if __name__ == '__main__':
    create_modeldata('data/models', maxsize=(1,6))
    

