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

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

from collections import deque, OrderedDict
from math import atan2, sin, cos, pi

import pybiklib.modelcommon
from pybiklib.modelcommon import epsdigits, epsilon


def roundeps(val):
    return round(val, epsdigits+1)
    
class Vector (pybiklib.modelcommon.Vector):
    def rounded(self):
        return self.__class__(roundeps(s) for s in self)
        
    def scaled(self, vector):
        return self.__class__(s*v for s,v in zip(self, vector))
        
    def equalfuzzy(self, other): return (self-other).length_squared() < epsilon
    def inversfuzzy(self, other): return (self+other).length_squared() < epsilon
        
        
def distance_axis_vert(axis, vert):
    ''' axis:  vector that defines a line through (0,0,0)
        return: the shortest distance between axis and vert
    '''
    axis = Vector(axis)
    base_point = axis * vert.point.dot(axis) / axis.dot(axis)
    return (vert.point - base_point).length()
    
def distance_axis_edge(axis, verts):
    ''' axis:  vector that defines a line through (0,0,0)
        verts: two points that define a line segment
        return: the shortest distance between axis and edge
    '''
    vec_axis = axis             # S1.P1 - S1.P0, S1.P0 == (0,0,0)
    vec_edge = verts[1].point - verts[0].point  # S2.P1 - S2.P0
    vec_vert0 = -verts[0].point  # S1.P0 - S2.P0
    a = vec_axis.dot(vec_axis)  # always >= 0
    b = vec_axis.dot(vec_edge)
    c = vec_edge.dot(vec_edge)  # always >= 0
    d = vec_axis.dot(vec_vert0)
    e = vec_edge.dot(vec_vert0)
    D = a*c - b*b   # always >= 0
    sD = D      # sc = sN / sD, default sD = D >= 0
    tD = D      # tc = tN / tD, default tD = D >= 0
    
    # compute the line parameters of the two closest points
    if D < epsilon: # the lines are almost parallel
        sN = 0.0    # force using point P0 on segment S1
        sD = 1.0    # to prevent possible division by 0.0 later
        tN = e
        tD = c
    else:   # get the closest points on the infinite lines
        sN = b*e - c*d
        tN = a*e - b*d
        
    if tN < 0.0:  # tc < 0 => the t=0 edge is visible
        tN = 0.0
        # recompute sc for this edge
        sN = -d
        sD = a
    elif tN > tD:   # tc > 1  => the t=1 edge is visible
        tN = tD
        # recompute sc for this edge
        sN = (-d + b)
        sD = a
    # finally do the division to get sc and tc
    sc = 0.0 if abs(sN) < epsilon else sN / sD
    tc = 0.0 if abs(tN) < epsilon else tN / tD
    
    # get the difference of the two closest points
    dP = vec_vert0 + (vec_axis * sc) - (vec_edge * tc)    # =  S1(sc) - S2(tc)
    
    return dP.length()     # return the closest distance
    
    
class Plane:
    def __init__(self, normal):
        self.normal = Vector(normal)
        self.distance = 0
        
    def __repr__(self):
        return '{0}({1.normal}, {1.distance})'.format(self.__class__.__name__, self)
        
    def signed_distance(self, point):
        return self.normal.dot(point) - self.distance
        
    def intersect_segment(self, p, q):
        pvalue = self.signed_distance(p.point)
        qvalue = self.signed_distance(q.point)
        if abs(pvalue - qvalue) < epsilon:
            return -1, None
        if abs(pvalue) < epsilon:
            return 0, p
        if abs(qvalue) < epsilon:
            return 0, q
        if pvalue < 0 < qvalue or qvalue < 0 < pvalue:
            d = pvalue / (pvalue - qvalue)
            return 1, Vert(p.point + (q.point - p.point) * d)
        else:
            return -1, None
            
        
class GeomBase:
    def __repr__(self):
        def args():
            for attr in self.fields_arg:
                yield str(list(getattr(self, attr)))
            for attr in self.fields_kwarg:
                yield '{}={!r}'.format(attr, getattr(self, attr))
        return '{}({})'.format(self.__class__.__name__, ', '.join(args()))
        
    def __euler(self):
        objnames = ['verts', 'edges', 'faces', 'cells']
        strx = ''
        euler = 0
        for i, objname in zip(range(self.dim), objnames):
            count = sum(1 for obj in getattr(self, objname))
            strx += ' {}={}'.format(objname[0], count)
            euler += -count if i % 2 else count
        euler += -1 if self.dim % 2 else 1
        return 'x={}{} {}=1'.format(euler, strx, objnames[self.dim][0])
        
    def __genstr(self, indent=''):
        def args():
            for attr in self.fields_kwarg:
                yield '{}={!r}'.format(attr, getattr(self, attr))
        yield '{}{}: {} {}'.format(indent, self.__class__.__name__, ' '.join(args()), self.__euler())
        for attr in self.fields_arg:
            for v in getattr(self, attr):
                yield from v.__genstr(indent+'  ')
                    
    def __str__(self):
        return '\n'.join(self.__genstr())
        
    def clone(self, clone_data):
        if id(self) in clone_data:
            return
        obj = clone_data[id(self)] = self.__class__()
        vals = [(attr, getattr(self, attr)) for attr in reversed(self.fields_geom)]
        def func(clone_data):
            for attr in self.fields_kwarg:
                if attr not in self.fields_geom:
                    setattr(obj, attr, getattr(self, attr))
            for attr, val in vals:
                newval = [clone_data[id(o)] for o in val] if isinstance(val, list) else clone_data[id(val)]
                setattr(obj, attr, newval)
        for attr, val in vals:
            if isinstance(val, list):
                for o in val:
                    yield o.clone
            else:
                yield val.clone
        yield func
        
    def center(self):
        c = Vector()
        for i, v in enumerate(self.verts):
            c += v.point
        return c / (i+1)
        
        
class Vert (GeomBase):
    dim = 0
    fields_arg = ()
    fields_kwarg = 'point', 'id',
    fields_geom = ()
    
    def __init__(self, point=None, *, id=None):
        self.point = Vector() if point is None else Vector(point)
        self.id = id
        
    def assert_valid(self):
        import numbers
        assert len(self.point) == 3
        assert all(isinstance(i, numbers.Number) for i in self.point)
        return True
        
        
class HalfEdge (GeomBase):
    dim = 1
    fields_arg = 'verts',
    fields_kwarg = 'edge',
    fields_geom = 'verts', 'edge', '_nextatface', 'other', 'halfface'
    
    def __init__(self, verts=None, edge=None):
        if verts is None and edge is None:
            return
        self.verts = list(verts)
        self.edge = edge
        if edge is not None:
            edge.halfedges.append(self)
        self._nextatface = None
        self.other = None # other halfedge of edge in the same cell
        self.halfface = None
        
    @classmethod
    def from_other(cls, halfedge):
        he = cls(reversed(halfedge.verts), halfedge.edge)
        halfedge.other = he
        he.other = halfedge
        return he
        
    def assert_valid(self):
        assert all(isinstance(v, Vert) for v in self.verts)
        assert isinstance(self.edge, Edge)
        assert isinstance(self._nextatface, self.__class__)
        assert isinstance(self.other, self.__class__), self.other.__class__
        assert isinstance(self.halfface, HalfFace)
        assert all(v.assert_valid() for v in self.verts)
        assert len(self.verts) == 2
        assert self.edge.assert_valid()
        assert self._nextatface is not None
        assert self._nextatface is not self # len > 1
        assert self._nextatface._nextatface is not self # len > 2
        assert self._nextatface.edge is not self.edge
        assert self.verts[1] is self._nextatface.verts[0]
        assert self.other.other == self
        assert self.halfface is self._nextatface.halfface
        return True
        
    def __iter__(self):
        for v in self.verts:
            yield v
            
    @property
    def prevatface(self):
        he = self
        while he._nextatface != self:
            he = he._nextatface
        return he
        
    def addtoface(self, halfedge, next=None):
        halfedge._nextatface = next or self._nextatface
        halfedge.halfface = self.halfface
        self._nextatface = halfedge
        
    def bevel_point(self, dist):
        '''
            >>> poly = Polyhedron()
            >>> poly.set_verts([0.,0.,0.], [1.,0.,0.], [1., 2.,0.])
            >>> poly.set_edges([0,1], [1,2], [2,0])
            >>> poly.set_faces([1,2,3])
            >>> poly.edges[0].halfedges[0].bevel_point(.1)
            Vert(0.9, 0.1, 0.0)
        '''
        vert1, vert2 = self.verts
        vert3 = self._nextatface.verts[1]
        nd1 = (vert1.point - vert2.point).normalised()
        nd2 = (vert3.point - vert2.point).normalised()
        return vert2.point + (nd1 + nd2) * dist / nd1.cross(nd2).length()
        
        
class Edge (GeomBase):
    fields_arg = ()
    fields_kwarg = 'id',
    fields_geom = 'halfedges',
    
    def __init__(self, *, id=None):
        self.halfedges = []
        self.id = id
        
    def assert_valid(self):
        assert all(isinstance(he, HalfEdge) for he in self.halfedges)
        assert len(self.halfedges) > 0
        assert all(isinstance(he, HalfEdge) for he in self.halfedges)
        assert all(he.edge is self for he in self.halfedges)
        assert all(he._nextatface is not None for he in self.halfedges)
        return True
        
    def __str__(self):
        verts = len(self.halfedges)
        euler = verts - 1
        return 'χ={} v={} e=1'.format(euler, verts)
        
    def split(self, vert):
        verts = self.halfedges[0].verts
        edge2 = self.__class__()
        for he1 in self.halfedges:
            if he1.verts[0] == verts[0]:
                he2 = HalfEdge([vert, he1.verts[1]], edge2)
                he1.verts[1] = vert
                he1.addtoface(he2)
                he2_other = he1.other.prevatface
            else:
                assert he1.verts[1] == verts[0], (he1.verts, verts)
                he2 = HalfEdge([he1.verts[0], vert], edge2)
                he1.verts[0] = vert
                he1.prevatface.addtoface(he2)
                he2_other = he1.other._nextatface
            if he2_other.edge is edge2:
                he2.other = he2_other
                he2_other.other = he2
                
        
class HalfFace (GeomBase):
    dim = 2
    fields_arg = 'halfedges',
    fields_kwarg = 'face',
    fields_geom = 'halfedge', '_nextatcell', 'face'
    
    def __init__(self, *, halfedges=None, face=None):
        if face is not None:
            self.face = face
            self.face.halffaces.append(self)
        if halfedges:
            self.set_halfedges(halfedges)
        else:
            self.halfedge = None
        self._nextatcell = None
        
    def assert_valid(self):
        assert isinstance(self.face, Face)
        assert self.face.assert_valid()
        assert isinstance(self._nextatcell, self.__class__)
        assert self._nextatcell is not None
        assert self.halfedge.halfface is self
        assert all(isinstance(he, HalfEdge) for he in self.halfedges)
        assert all(he.assert_valid() for he in self.halfedges)
        return True
        
    def __iter__(self):
        if self.halfedge is None:
            return
        he = self.halfedge
        while True:
            yield he
            he = he._nextatface
            if he is self.halfedge:
                return
                
    halfedges = property(__iter__)
        
    def halfedges2(self):
        if self.halfedge is None:
            return
        he = self.halfedge
        while True:
            yield he, he._nextatface
            he = he._nextatface
            if he is self.halfedge:
                return
                
    @property
    def edges(self):
        for he in self.halfedges:
            yield he.edge
            
    @property
    def verts(self):
        for he in self.halfedges:
            yield he.verts[0]
            
    def set_halfedges(self, halfedges):
        ithalfedges = iter(halfedges)
        self.halfedge = phe = next(ithalfedges)
        for he in ithalfedges:
            phe._nextatface = he
            phe.halfface = self
            phe = he
        phe._nextatface = self.halfedge
        phe.halfface = self
        
    def normal(self):
        verts = self.verts
        try:
            v1 = next(verts).point
            v2 = next(verts).point
            v3 = next(verts).point
        except StopIteration:
            raise ValueError('Too few vertices')
        return (v2-v1).cross(v3-v1).normalised()
        
    @staticmethod
    def _sorted_halfedges(halfedges):
        try:
            current = halfedges.pop(0)
        except IndexError:
            return halfedges
        halfedges_new = [current]
        unused, prev_vert = current.verts
        while halfedges:
            for i, he in enumerate(halfedges):
                if prev_vert not in he.verts:
                    continue
                current = halfedges.pop(i)
                assert current is he
                vert1, vert2 = he.verts
                if prev_vert is vert2:
                    current.verts = [vert2, vert1]
                    prev_vert = vert1
                else:
                    prev_vert = vert2
                halfedges_new.append(current)
                break
            else:
                assert False
        halfedges[:] = halfedges_new
        return halfedges
        
    def reverse(self):
        halfedges = list(self.halfedges)
        for he in halfedges:
            he.verts = list(reversed(he.verts))
        self._sorted_halfedges(halfedges)
        self.set_halfedges(halfedges)
        
        
class Face (GeomBase):
    fields_arg = ()
    fields_kwarg = 'type', 'id'
    fields_geom = 'halffaces',
    
    def __init__(self, *, type=None, id=None):
        self.halffaces = []
        self.type = type
        self.id = id
        
    def assert_valid(self):
        assert all(isinstance(f, HalfFace) for f in self.halffaces)
        assert all(hf.face is self for hf in self.halffaces)
        assert 1 <= len(self.halffaces) <= 2
        lenhf0 = len(list(self.halffaces[0].halfedges))
        assert all(lenhf0 == len(list(hf.halfedges)) for hf in self.halffaces)
        return True
        
    def __str__(self):
        verts = len(list(self.verts()))
        edges = len(list(self.edges()))
        euler = verts - edges + 1
        return 'χ={} v={} e={} f=1'.format(euler, verts, edges)
        
    @property
    def edges(self):
        return self.halffaces[0].edges
        
    @property
    def verts(self):
        return self.halffaces[0].verts
        
    def remove_all_edges(self):
        for hf in self.halffaces:
            hf.halfedge = None
            
    def split(self, split_verts):
        assert len(split_verts) == 2, len(split_verts)
        edge = Edge()
        newface = Face(type=self.type, id=self.id)
        edgef1 = None
        for halfface in self.halffaces:
            he = halfface.halfedge
            # make sure to start on the same edge for all halffaces
            if edgef1 is None:
                edgef1 = he.edge
            else:
                while he.edge is not edgef1:
                    he = he._nextatface
                halfface.halfedge = he
            # proceed to the end of the old halfface
            while he.verts[1] not in split_verts:
                he = he._nextatface
            hedge1f1 = he
            he = he._nextatface
            hedge1f2 = he
            # create a new halfface
            newhalfface = HalfFace(face=newface)
            newhalfface.halfedge = he
            newhalfface._nextatcell = halfface._nextatcell
            halfface._nextatcell = newhalfface
            # proceed to the end of the new halfface
            while he.verts[1] not in split_verts:
                he.halfface = newhalfface
                he = he._nextatface
            he.halfface = newhalfface
            hedge2f2 = he
            he = he._nextatface
            hedge2f1 = he
            # insert a new halfedge between the splitted halfface
            hedge1 = HalfEdge(split_verts, edge)
            hedge2 = HalfEdge([split_verts[1], split_verts[0]], edge)
            hedge1.other = hedge2
            hedge2.other = hedge1
            if hedge1f1.verts[1] is split_verts[1]:
                hedge1, hedge2 = hedge2, hedge1
            hedge1f1.addtoface(hedge1, hedge2f1)
            hedge2f2.addtoface(hedge2, hedge1f2)
        return edge
        
    @classmethod
    def polygon(cls, point, n, ids=None):
        x0, y0 = point
        vert0 = Vert(point)
        r = vert0.point.length()
        angle_v0 = atan2(y0, x0)
        angle_diff = 2 * pi / n
        
        vertp = vert0
        halfedges = []
        for k in range(1, n):
            angle = k * angle_diff + angle_v0
            vertc = Vert((r * cos(angle), r * sin(angle)))
            edge = Edge() if ids is None else Edge(id=ids[k-1])
            halfedges.append(HalfEdge((vertp, vertc), edge))
            vertp = vertc
        edge = Edge() if ids is None else Edge(id=ids[n-1])
        halfedges.append(HalfEdge((vertp, vert0), edge))
        return HalfFace(halfedges=halfedges, face=Face()).face
        
    def extrude_y(self, upy, downy, ids=None):
        assert len(self.halffaces) == 1
        halfface_up = self.halffaces[0]
        halfedges_down = []
        halffaces = [halfface_up]
        
        def extrude_vert(vert_up, upy, downy):
            x1, y1 = vert_up.point
            vert_up.point = Vector((x1, upy, -y1))
            vert_down = Vert((x1, downy, -y1))
            return vert_up, vert_down
            
        vp_down = None
        for he_up in halfface_up.halfedges:
            # vertices
            vc_up, vc_down = extrude_vert(he_up.verts[1], upy, downy)
            # edges
            e_down = Edge()
            ec_side = Edge()
            if vp_down is None:
                v1_up, v1_down = vc_up, vc_down
                e0_down = e_down
                e1_side = ec_side
            else:
                # down halfedge
                halfedges_down.append(HalfEdge([vc_down, vp_down], e_down))
                # side face
                he1 = HalfEdge([vp_down, vc_down], e_down)
                he2 = HalfEdge([vc_down, vc_up], ec_side)
                he3 = HalfEdge([vc_up, vp_up], he_up.edge)
                he4 = HalfEdge([vp_up, vp_down], ep_side)
                halfface_side = HalfFace(halfedges=[he1, he2, he3, he4], face=Face(type='face', id=he_up.edge.id))
                halffaces.append(halfface_side)
            # next
            vp_up, vp_down = vc_up, vc_down
            ep_side = ec_side
        he_up = halfface_up.halfedge
        # joining down halfedge
        halfedges_down.append(HalfEdge([v1_down, vp_down], e0_down))
        # joining side face
        he1 = HalfEdge([vp_down, v1_down], e0_down)
        he2 = HalfEdge([v1_down, v1_up], e1_side)
        he3 = HalfEdge([v1_up, vp_up], he_up.edge)
        he4 = HalfEdge([vp_up, vp_down], ep_side)
        halfface_side = HalfFace(halfedges=[he1, he2, he3, he4], face=Face(type='face', id=he_up.edge.id))
        halffaces.append(halfface_side)
        # up and down faces
        face_down = Face()
        if ids is not None:
            self.type = face_down.type = 'face'
            self.id, face_down.id = ids
        halfface_down = HalfFace(halfedges=reversed(halfedges_down), face=face_down)
        halffaces.append(halfface_down)
        return Cell(halffaces=halffaces)
        
    def pyramid(self, upy, downy, id=None):
        self.assert_valid()
        assert len(self.halffaces) == 1
        halfface_down = self.halffaces[0]
        halffaces = [halfface_down]
        vert_up = Vert((0., upy, 0.))
        
        def change_vert(vert_down, downy):
            x1, y1 = vert_down.point
            vert_down.point = Vector((-x1, downy, -y1))
            return vert_down
            
        vp_down = None
        for he in halfface_down.halfedges:
            # vertices
            vc_down = change_vert(he.verts[1], downy)
            # edges
            ec_side = Edge()
            if vp_down is None:
                v1_down = vc_down
                e1_side = ec_side
            else:
                # side face
                he1 = HalfEdge([vc_down, vp_down], he.edge)
                he2 = HalfEdge([vp_down, vert_up], ep_side)
                he3 = HalfEdge([vert_up, vc_down], ec_side)
                halfface_side = HalfFace(halfedges=[he1, he2, he3], face=Face(type='face', id=he.edge.id))
                halffaces.append(halfface_side)
            # next
            vp_down = vc_down
            ep_side = ec_side
        he = halfface_down.halfedge
        # joining side face
        he1 = HalfEdge([v1_down, vp_down], he.edge)
        he2 = HalfEdge([vp_down, vert_up], ep_side)
        he3 = HalfEdge([vert_up, v1_down], e1_side)
        halfface_side = HalfFace(halfedges=[he1, he2, he3], face=Face(type='face', id=he.edge.id))
        halffaces.append(halfface_side)
        # down face
        if id is not None:
            self.type = 'face'
            self.id = id
        return Cell(halffaces=halffaces)
        
        
class Cell (GeomBase):
    dim = 3
    fields_arg = 'halffaces',
    fields_kwarg = 'indices',
    fields_geom = 'halfface',
    
    def __init__(self, *, halffaces=None):
        if halffaces is not None:
            phf = halffaces[-1]
            for hf in halffaces:
                phf._nextatcell = hf
                phf = hf
                for he in hf.halfedges:
                    if len(he.edge.halfedges) == 2: #XXX:
                        he.edge.halfedges[0].other = he.edge.halfedges[1]
                        he.edge.halfedges[1].other = he.edge.halfedges[0]
            self.halfface = halffaces[0]
        self.indices = None
        
    def assert_valid(self):
        assert all(isinstance(f, HalfFace) for f in self.halffaces)
        assert all(hf.assert_valid() for hf in self.halffaces)
        assert self.halfface is not None # len > 0
        assert self.halfface is not self # len > 1
        assert self.halfface._nextatcell is not self # len > 2
        assert self.halfface._nextatcell._nextatcell is not self # len > 3
        assert len(list(self.edges)) > 3
        assert len(list(self.verts)) > 3
        assert all(hf.halfedge.other.halfface in self.halffaces for hf in self.halffaces)
        return True
        
    def __iter__(self):
        if self.halfface is None:
            return
        hf = self.halfface
        while True:
            yield hf
            hf = hf._nextatcell
            if hf is self.halfface:
                return
                
    halffaces = property(__iter__)
        
    @property
    def faces(self):
        for hf in self.halffaces:
            yield hf.face
            
    @property
    def edges(self):
        cache = []
        for f in self.faces:
            for e in f.edges:
                if e not in cache:
                    cache.append(e)
                    yield e
                    
    @property
    def verts(self):
        cache = []
        for hf in self.halffaces:
            for v in hf.verts:
                if v not in cache:
                    cache.append(v)
                    yield v
                    
    def add(self, halfface):
        halfface._nextatcell = self.halfface._nextatcell
        self.halfface._nextatcell = halfface
        
    def split(self, split_edges, id=None):
        halffaces, halffaces1, halffaces2 = [self.halfface], [self.halfface], []
        halfedges1, halfedges2 = [], []
        while halffaces:
            for he in halffaces.pop().halfedges:
                hf = he.other.halfface
                if he.edge in split_edges:
                    halfedges1.append(HalfEdge.from_other(he))
                    if hf not in halffaces2:
                        halffaces2.append(hf)
                else:
                    if hf not in halffaces1:
                        halffaces.append(hf)
                        halffaces1.append(hf)
        assert len(halfedges1) > 2
        halffaces = halffaces2[:]
        while halffaces:
            for he in halffaces.pop().halfedges:
                if he.edge in split_edges:
                    halfedges2.append(HalfEdge.from_other(he))
                else:
                    hf = he.other.halfface
                    if hf not in halffaces2:
                        halffaces.append(hf)
                        halffaces2.append(hf)
        assert len(halfedges2) > 2
        # new face
        face = Face(type='cut', id=id)
        halfface1 = HalfFace(halfedges=HalfFace._sorted_halfedges(halfedges1), face=face)
        halfface2 = HalfFace(halfedges=HalfFace._sorted_halfedges(halfedges2), face=face)
        # update cell
        phf = halfface1
        for hf in halffaces1:
            phf._nextatcell = hf
            phf = hf
        phf._nextatcell = halfface1
        # new cell
        halffaces2.append(halfface2)
        new_cell = Cell(halffaces=halffaces2)
        return new_cell
            
        
class Polyhedron:
    def __init__(self):
        self._cells = []
        
    def clone(self):
        clone_data = {}
        obj = self.__class__()
        funcs = deque(f for o in self._cells for f in o.clone(clone_data))
        obj._cells = [clone_data[id(o)] for o in self._cells]
        while funcs:
            f = funcs.popleft()
            r = f(clone_data)
            if r is not None:
                funcs.extendleft(reversed(list(r)))
        return obj
        
    def assert_valid(self):
        import itertools
        assert all(isinstance(c, Cell) and c.assert_valid() for c in self.cells)
        assert all(isinstance(f, Face) and f.assert_valid() for f in self.faces)
        assert all(isinstance(e, Edge) and e.assert_valid() for e in self.edges)
        assert all(isinstance(v, Vert) and v.assert_valid() for v in self.verts)
        assert all(not v1.equalfuzzy(v2) for v1, v2 in itertools.combinations(self.verts, 2)), self.verts
        return True
        
    def __str__(self):
        verts = len(self.verts)
        edges = len(self.edges)
        faces = len(self.faces)
        cells = len(self.cells)
        euler = verts - edges + faces - cells
        return 'χ={} v={} e={} f={} c={}'.format(euler, verts, edges, faces, cells)
        
    def verts_string(self, verts): return '-'.join(str(self.verts.index(v)) for v in verts)
    
    @property
    def cells(self):
        return self._cells
    @cells.setter
    def cells(self, cells):
        self._cells = cells
        
    @property
    def faces(self):
        cache = []
        for c in self.cells:
            for f in c.faces:
                if f not in cache:
                    cache.append(f)
        return cache
        
    @property
    def edges(self):
        cache = []
        for f in self.faces:
            for e in f.edges:
                if e not in cache:
                    cache.append(e)
        return cache
        
    @property
    def verts(self):
        cache = []
        for f in self.faces:
            for v in f.verts:
                if v not in cache:
                    cache.append(v)
        return cache
        
    def set_verts(self, *args):
        self._polytopes = [Vert(arg) for arg in args]
            
    def set_edges(self, *args):
        halfedges = []
        for arg in args:
            verts = [self._polytopes[i] for i in arg]
            halfedges.append(HalfEdge(verts=verts, edge=Edge()))
        self._polytopes = halfedges
        
    def set_faces(self, *args, ids=None):
        halffaces = []
        if ids is None:
            faces = [Face() for unused in args]
        else:
            assert len(args) == len(ids)
            faces = [Face(type='face', id=fid) for fid in ids]
        for arg, face in zip(args, faces):
            halfedges = []
            for i in arg:
                # this only works if args describe one closed cell
                if i > 0:
                    he = self._polytopes[i-1]
                elif i < 0:
                    he = HalfEdge.from_other(self._polytopes[-i-1])
                else:
                    raise ValueError()
                halfedges.append(he)
            halfface = HalfFace(halfedges=halfedges, face=face)
            halffaces.append(halfface)
        del self._polytopes
        self._cells = [Cell(halffaces=halffaces)]
            
    _next_split_id = 0
        
    def split_plane(self, plane, indexpos=None):
        # split intersected edges
        new_verts = []
        for edge in self.edges[:]:
            pos, vert = plane.intersect_segment(*edge.halfedges[0].verts)
            if pos > 0:
                edge.split(vert)
                new_verts.append(vert)
            elif pos == 0 and vert not in new_verts:
                new_verts.append(vert)
        # split intersected faces
        new_edges = []
        for face in self.faces[:]:
            split_verts = [v for v in face.verts for nv in new_verts if nv is v]
            assert 0 <= len(split_verts) <= 2, split_verts
            if len(split_verts) == 2:
                for he in face.halffaces[0].halfedges:
                    if he.verts == split_verts or he.verts == [split_verts[1], split_verts[0]]:
                        new_edges.append(he.edge)
                        break
                else:
                    new_edge = face.split(split_verts)
                    new_edges.append(new_edge)
        # split cells
        split_id = self._next_split_id
        self.__class__._next_split_id += 1
        for cell in self.cells[:]:
            split_edges = [e for e in cell.edges if e in new_edges]
            if len(split_edges) > 2:
                new_cell = cell.split(split_edges, split_id)
                self.cells.append(new_cell)
            else:
                new_cell = None
            if indexpos is not None:
                for hf in cell.halffaces:
                    for he in hf.halfedges:
                        if he.edge not in split_edges:
                            sd1 = -plane.signed_distance(he.verts[0].point)
                            # the verts of the edge e cannot be both in range epsilon to the plane,
                            # otherwise e would be in split_edges, so only test one vert for epsilon
                            if sd1 > epsilon or sd1 >= -epsilon and plane.signed_distance(he.verts[1].point) < 0:
                                if new_cell is not None:
                                    new_cell.indices = cell.indices
                                new_cell = cell
                            elif new_cell is None:
                                break
                            indices = list(cell.indices)
                            indices[indexpos] += 1
                            new_cell.indices = tuple(indices)
                            break
                    else:
                        continue
                    break
        return split_id
            
    def scale(self, value):
        for v in self.verts:
            v.point = v.point.scaled(value)
            
    def bevel(self, width):
        faces = self.faces
        
        f_vverts_eedges = {}
        for f in faces:
            vverts = {id(he.verts[1]): Vert(he.bevel_point(width), id=f.id) for he in f.halffaces[0].halfedges}
            eedges = {id(e): Edge() for e in f.edges}
            f_vverts_eedges[f] = (vverts, eedges)
        for c in self.cells:
            # create the faces that replace the old edges
            vhalfedges = OrderedDict()  # for reproducible build
            for he1 in (he for hf in c.halffaces for he in hf.halfedges):
                if he1.halfface.face.type == 'bevel':
                    continue
                he3 = he1.other
                if he3.halfface.face.type == 'bevel':
                    continue
                vverts1, eedges1 = f_vverts_eedges[he1.halfface.face]
                vverts3, eedges3 = f_vverts_eedges[he3.halfface.face]
                idhe1v2 = id(he1.verts[1])
                idhe3v2 = id(he3.verts[1])
                # new halfedges for the adjacent vertex-faces
                he2 = HalfEdge([], edge=Edge())
                he4 = HalfEdge([], edge=Edge())
                vhalfedges.setdefault(idhe1v2, []).append(he2)
                vhalfedges.setdefault(idhe3v2, []).append(he4)
                # update halfedges for the adjacent faces
                ide = id(he1.edge)
                edge = eedges1[ide]
                he1.edge = edge
                edge.halfedges.append(he1)
                edge = eedges3[ide]
                he3.edge = edge
                edge.halfedges.append(he3)
                # replace the adjacent verts
                vert1 = vverts1[idhe3v2]
                vert2 = vverts1[idhe1v2]
                vert3 = vverts3[idhe1v2]
                vert4 = vverts3[idhe3v2]
                he1.verts[:] = [vert1, vert2]
                he2.verts[:] = [vert2, vert3]
                he3.verts[:] = [vert3, vert4]
                he4.verts[:] = [vert4, vert1]
                # new halfedges for the new face
                he1 = HalfEdge.from_other(he1)
                he2 = HalfEdge.from_other(he2)
                he3 = HalfEdge.from_other(he3)
                he4 = HalfEdge.from_other(he4)
                c.add(HalfFace(halfedges=[he4, he3, he2, he1], face=Face(type='bevel')))
            # create the faces that replace the old verts
            for halfedges in vhalfedges.values():
                c.add(HalfFace(halfedges=HalfFace._sorted_halfedges(halfedges), face=Face(type='bevel')))
                    
    def remove_faces(self, func):
        for cell in self.cells:
            halffaces = list(cell.halffaces)
            phf = halffaces[-1]
            for hf in halffaces:
                if func(hf.face):
                    phf._nextatcell = hf._nextatcell
                    hf._nextatcell = None
                    hf.face.halffaces.remove(hf)
                    hf.face = None
                    if hf is cell.halfface:
                        cell.halfface = phf._nextatcell
                else:
                    phf = hf
        
    def remove_cells(self, func):
        self._cells = [c for c in self._cells if not func(c)]
        
    

