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

# ##### BEGIN GPL LICENSE BLOCK #####
#
#  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 2
#  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, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

# <pep8 compliant>

# ----------------------------------------------------------
# Author: Stephen Leger (s-leger)
#
# ----------------------------------------------------------
# noinspection PyUnresolvedReferences
import bpy
# noinspection PyUnresolvedReferences
from bpy.types import Operator, PropertyGroup, Mesh, Panel
from bpy.props import (
    FloatProperty, BoolProperty, IntProperty,
    StringProperty, EnumProperty,
    CollectionProperty
    )
import bmesh
from mathutils import Vector, Matrix
from mathutils.geometry import interpolate_bezier
from math import sin, cos, pi, atan2
from .archipack_manipulator import Manipulable, archipack_manipulator
from .archipack_object import ArchipackCreateTool, ArchipackObject
from .archipack_2d import Line, Arc
from .archipack_cutter import (
    CutAblePolygon, CutAbleGenerator,
    ArchipackCutter,
    ArchipackCutterPart
    )


class Slab():

    def __init__(self):
        # self.colour_inactive = (1, 1, 1, 1)
        pass

    def set_offset(self, offset, last=None):
        """
            Offset line and compute intersection point
            between segments
        """
        self.line = self.make_offset(offset, last)

    def straight_slab(self, a0, length):
        s = self.straight(length).rotate(a0)
        return StraightSlab(s.p, s.v)

    def curved_slab(self, a0, da, radius):
        n = self.normal(1).rotate(a0).scale(radius)
        if da < 0:
            n.v = -n.v
        a0 = n.angle
        c = n.p - n.v
        return CurvedSlab(c, radius, a0, da)


class StraightSlab(Slab, Line):

    def __init__(self, p, v):
        Line.__init__(self, p, v)
        Slab.__init__(self)


class CurvedSlab(Slab, Arc):

    def __init__(self, c, radius, a0, da):
        Arc.__init__(self, c, radius, a0, da)
        Slab.__init__(self)


class SlabGenerator(CutAblePolygon, CutAbleGenerator):

    def __init__(self, parts):
        self.parts = parts
        self.segs = []
        self.holes = []
        self.convex = True
        self.xsize = 0

    def add_part(self, part):

        if len(self.segs) < 1:
            s = None
        else:
            s = self.segs[-1]
        # start a new slab
        if s is None:
            if part.type == 'S_SEG':
                p = Vector((0, 0))
                v = part.length * Vector((cos(part.a0), sin(part.a0)))
                s = StraightSlab(p, v)
            elif part.type == 'C_SEG':
                c = -part.radius * Vector((cos(part.a0), sin(part.a0)))
                s = CurvedSlab(c, part.radius, part.a0, part.da)
        else:
            if part.type == 'S_SEG':
                s = s.straight_slab(part.a0, part.length)
            elif part.type == 'C_SEG':
                s = s.curved_slab(part.a0, part.da, part.radius)

        self.segs.append(s)
        self.last_type = part.type

    def set_offset(self):
        last = None
        for i, seg in enumerate(self.segs):
            seg.set_offset(self.parts[i].offset, last)
            last = seg.line

    def close(self, closed):
        # Make last segment implicit closing one
        if closed:
            part = self.parts[-1]
            w = self.segs[-1]
            dp = self.segs[0].p0 - self.segs[-1].p0
            if "C_" in part.type:
                dw = (w.p1 - w.p0)
                w.r = part.radius / dw.length * dp.length
                # angle pt - p0        - angle p0 p1
                da = atan2(dp.y, dp.x) - atan2(dw.y, dw.x)
                a0 = w.a0 + da
                if a0 > pi:
                    a0 -= 2 * pi
                if a0 < -pi:
                    a0 += 2 * pi
                w.a0 = a0
            else:
                w.v = dp

            if len(self.segs) > 1:
                w.line = w.make_offset(self.parts[-1].offset, self.segs[-2].line)

            p1 = self.segs[0].line.p1
            self.segs[0].line = self.segs[0].make_offset(self.parts[0].offset, w.line)
            self.segs[0].line.p1 = p1

    def locate_manipulators(self):
        """
            setup manipulators
        """
        for i, f in enumerate(self.segs):

            manipulators = self.parts[i].manipulators
            p0 = f.p0.to_3d()
            p1 = f.p1.to_3d()
            # angle from last to current segment
            if i > 0:
                v0 = self.segs[i - 1].straight(-1, 1).v.to_3d()
                v1 = f.straight(1, 0).v.to_3d()
                manipulators[0].set_pts([p0, v0, v1])

            if type(f).__name__ == "StraightSlab":
                # segment length
                manipulators[1].type_key = 'SIZE'
                manipulators[1].prop1_name = "length"
                manipulators[1].set_pts([p0, p1, (1, 0, 0)])
            else:
                # segment radius + angle
                v0 = (f.p0 - f.c).to_3d()
                v1 = (f.p1 - f.c).to_3d()
                manipulators[1].type_key = 'ARC_ANGLE_RADIUS'
                manipulators[1].prop1_name = "da"
                manipulators[1].prop2_name = "radius"
                manipulators[1].set_pts([f.c.to_3d(), v0, v1])

            # snap manipulator, don't change index !
            manipulators[2].set_pts([p0, p1, (1, 0, 0)])
            # dumb segment id
            manipulators[3].set_pts([p0, p1, (1, 0, 0)])

    def get_verts(self, verts):
        for s in self.segs:
            if "Curved" in type(s).__name__:
                for i in range(16):
                    # x, y = slab.line.lerp(i / 16)
                    verts.append(s.lerp(i / 16).to_3d())
            else:
                # x, y = s.line.p0
                verts.append(s.p0.to_3d())
            """
            for i in range(33):
                x, y = slab.line.lerp(i / 32)
                verts.append((x, y, 0))
            """

    def rotate(self, idx_from, a):
        """
            apply rotation to all following segs
        """
        self.segs[idx_from].rotate(a)
        ca = cos(a)
        sa = sin(a)
        rM = Matrix([
            [ca, -sa],
            [sa, ca]
            ])
        # rotation center
        p0 = self.segs[idx_from].p0
        for i in range(idx_from + 1, len(self.segs)):
            seg = self.segs[i]
            # rotate seg
            seg.rotate(a)
            # rotate delta from rotation center to segment start
            dp = rM @ (seg.p0 - p0)
            seg.translate(dp)

    def translate(self, idx_from, dp):
        """
            apply translation to all following segs
        """
        self.segs[idx_from].p1 += dp
        for i in range(idx_from + 1, len(self.segs)):
            self.segs[i].translate(dp)

    def draw(self, context):
        """
            draw generator using gl
        """
        for seg in self.segs:
            seg.draw(context, render=False)

    def limits(self):
        x_size = [s.p0.x for s in self.segs]
        self.xsize = max(x_size) - min(x_size)

    def cut(self, context, o):
        """
            either external or holes cuts
        """
        self.limits()

        self.as_lines(step_angle=0.0502)
        # self.segs = [s.line for s in self.segs]

        for b in o.children:
            d = archipack_slab_cutter.datablock(b)
            if d is not None:
                g = d.ensure_direction()
                g.change_coordsys(b.matrix_world, o.matrix_world)
                self.slice(g)

    def slab(self, context, o, d):

        verts = []
        self.get_verts(verts)
        if len(verts) > 2:

            bm = bmesh.new()

            for v in verts:
                bm.verts.new(v)
            bm.verts.ensure_lookup_table()
            for i in range(1, len(verts)):
                bm.edges.new((bm.verts[i - 1], bm.verts[i]))
            bm.edges.new((bm.verts[-1], bm.verts[0]))
            bm.edges.ensure_lookup_table()
            bmesh.ops.contextual_create(bm, geom=bm.edges)

            self.cut_holes(bm, self)

            bmesh.ops.dissolve_limit(bm,
                        angle_limit=0.01,
                        use_dissolve_boundaries=False,
                        verts=bm.verts,
                        edges=bm.edges,
                        delimit={'MATERIAL'})

            bm.to_mesh(o.data)
            bm.free()
            # geom = bm.faces[:]
            # verts = bm.verts[:]
            # bmesh.ops.solidify(bm, geom=geom, thickness=d.z)

            # merge with object
            # bmed.bmesh_join(context, o, [bm], normal_update=True)

            bpy.ops.object.mode_set(mode='OBJECT')


def update(self, context):
    self.update(context)


def update_manipulators(self, context):
    self.update(context, manipulable_refresh=True)


def update_path(self, context):
    self.update_path(context)


materials_enum = (
            ('0', 'Ceiling', '', 0),
            ('1', 'White', '', 1),
            ('2', 'Concrete', '', 2),
            ('3', 'Wood', '', 3),
            ('4', 'Metal', '', 4),
            ('5', 'Glass', '', 5)
            )


class archipack_slab_material(PropertyGroup):
    index : EnumProperty(
        items=materials_enum,
        default='4',
        update=update
        )

    def find_in_selection(self, context):
        """
            find witch selected object this instance belongs to
            provide support for "copy to selected"
        """
        selected = context.selected_objects[:]
        for o in selected:
            props = archipack_slab.datablock(o)
            if props:
                for part in props.rail_mat:
                    if part == self:
                        return props
        return None

    def update(self, context):
        props = self.find_in_selection(context)
        if props is not None:
            props.update(context)


class archipack_slab_child(PropertyGroup):
    """
        Store child fences to be able to sync
    """
    child_name : StringProperty()
    idx : IntProperty()

    def get_child(self, context):
        d = None
        child = context.scene.objects.get(self.child_name.strip())
        if child is not None and child.data is not None:
            if 'archipack_fence' in child.data:
                d = child.data.archipack_fence[0]
        return child, d


def update_type(self, context):

    d = self.find_in_selection(context)

    if d is not None and d.auto_update:

        d.auto_update = False
        # find part index
        idx = 0
        for i, part in enumerate(d.parts):
            if part == self:
                idx = i
                break

        part = d.parts[idx]
        a0 = 0
        if idx > 0:
            g = d.get_generator()
            w0 = g.segs[idx - 1]
            a0 = w0.straight(1).angle
            if "C_" in self.type:
                w = w0.straight_slab(part.a0, part.length)
            else:
                w = w0.curved_slab(part.a0, part.da, part.radius)
        else:
            if "C_" in self.type:
                p = Vector((0, 0))
                v = self.length * Vector((cos(self.a0), sin(self.a0)))
                w = StraightSlab(p, v)
                a0 = pi / 2
            else:
                c = -self.radius * Vector((cos(self.a0), sin(self.a0)))
                w = CurvedSlab(c, self.radius, self.a0, pi)

        # w0 - w - w1
        if idx + 1 == d.n_parts:
            dp = - w.p0
        else:
            dp = w.p1 - w.p0

        if "C_" in self.type:
            part.radius = 0.5 * dp.length
            part.da = pi
            a0 = atan2(dp.y, dp.x) - pi / 2 - a0
        else:
            part.length = dp.length
            a0 = atan2(dp.y, dp.x) - a0

        if a0 > pi:
            a0 -= 2 * pi
        if a0 < -pi:
            a0 += 2 * pi
        part.a0 = a0

        if idx + 1 < d.n_parts:
            # adjust rotation of next part
            part1 = d.parts[idx + 1]
            if "C_" in part.type:
                a0 = part1.a0 - pi / 2
            else:
                a0 = part1.a0 + w.straight(1).angle - atan2(dp.y, dp.x)

            if a0 > pi:
                a0 -= 2 * pi
            if a0 < -pi:
                a0 += 2 * pi
            part1.a0 = a0

        d.auto_update = True


class ArchipackSegment():
    """
        A single manipulable polyline like segment
        polyline like segment line or arc based
        @TODO: share this base class with
        stair, wall, fence, slab
    """
    type : EnumProperty(
            items=(
                ('S_SEG', 'Straight', '', 0),
                ('C_SEG', 'Curved', '', 1),
                ),
            default='S_SEG',
            update=update_type
            )
    length : FloatProperty(
            name="Length",
            min=0.01,
            default=2.0,
            update=update
            )
    radius : FloatProperty(
            name="Radius",
            min=0.5,
            default=0.7,
            update=update
            )
    da : FloatProperty(
            name="Angle",
            min=-pi,
            max=pi,
            default=pi / 2,
            subtype='ANGLE', unit='ROTATION',
            update=update
            )
    a0 : FloatProperty(
            name="Start angle",
            min=-2 * pi,
            max=2 * pi,
            default=0,
            subtype='ANGLE', unit='ROTATION',
            update=update
            )
    offset : FloatProperty(
            name="Offset",
            description="Add to current segment offset",
            default=0,
            unit='LENGTH', subtype='DISTANCE',
            update=update
            )
    linked_idx : IntProperty(default=-1)

    # @TODO:
    # flag to handle wall's  x_offset
    # when set add wall offset value to segment offset
    # pay attention at allowing per wall segment offset

    manipulators : CollectionProperty(type=archipack_manipulator)

    def find_in_selection(self, context):
        raise NotImplementedError

    def update(self, context, manipulable_refresh=False):
        props = self.find_in_selection(context)
        if props is not None:
            props.update(context, manipulable_refresh)

    def draw_insert(self, context, layout, index):
        """
            May implement draw for insert / remove segment operators
        """
        pass

    def draw(self, context, layout, index):
        box = layout.box()
        box.prop(self, "type", text=str(index + 1))
        self.draw_insert(context, box, index)
        if self.type in ['C_SEG']:
            box.prop(self, "radius")
            box.prop(self, "da")
        else:
            box.prop(self, "length")
        box.prop(self, "a0")
        # box.prop(self, "offset")


class archipack_slab_part(ArchipackSegment, PropertyGroup):

    def draw_insert(self, context, layout, index):
        row = layout.row(align=True)
        row.operator("archipack.slab_insert", text="Split").index = index
        row.operator("archipack.slab_balcony", text="Balcony").index = index
        row.operator("archipack.slab_remove", text="Remove").index = index

    def find_in_selection(self, context):
        """
            find witch selected object this instance belongs to
            provide support for "copy to selected"
        """
        selected = context.selected_objects[:]
        for o in selected:
            props = archipack_slab.datablock(o)
            if props:
                for part in props.parts:
                    if part == self:
                        return props
        return None


class archipack_slab(ArchipackObject, Manipulable, PropertyGroup):
    # boundary
    n_parts : IntProperty(
            name="Parts",
            min=1,
            default=1, update=update_manipulators
            )
    parts : CollectionProperty(type=archipack_slab_part)
    closed : BoolProperty(
            default=True,
            name="Close",
            options={'SKIP_SAVE'},
            update=update_manipulators
            )
    # UI layout related
    parts_expand : BoolProperty(
            options={'SKIP_SAVE'},
            default=False
            )

    x_offset : FloatProperty(
            name="Offset",
            min=-1000, max=1000,
            default=0.0, precision=2, step=1,
            unit='LENGTH', subtype='DISTANCE',
            update=update
            )
    z : FloatProperty(
            name="Thickness",
            default=0.3, precision=2, step=1,
            unit='LENGTH', subtype='DISTANCE',
            update=update
            )
    auto_synch : BoolProperty(
            name="Auto-Synch",
            description="Keep wall in synch when editing",
            default=True,
            update=update_manipulators
            )
    # @TODO:
    # Global slab offset
    # will only affect slab parts sharing a wall

    childs : CollectionProperty(type=archipack_slab_child)
    # Flag to prevent mesh update while making bulk changes over variables
    # use :
    # .auto_update = False
    # bulk changes
    # .auto_update = True
    auto_update : BoolProperty(
            options={'SKIP_SAVE'},
            default=True,
            update=update_manipulators
            )

    def get_generator(self):
        g = SlabGenerator(self.parts)
        for part in self.parts:
            # type, radius, da, length
            g.add_part(part)

        g.set_offset()

        g.close(self.closed)
        g.locate_manipulators()
        return g

    def insert_part(self, context, where):
        self.manipulable_disable(context)
        self.auto_update = False
        # the part we do split
        part_0 = self.parts[where]
        part_0.length /= 2
        part_0.da /= 2
        self.parts.add()
        part_1 = self.parts[len(self.parts) - 1]
        part_1.type = part_0.type
        part_1.length = part_0.length
        part_1.offset = part_0.offset
        part_1.da = part_0.da
        part_1.a0 = 0
        # move after current one
        self.parts.move(len(self.parts) - 1, where + 1)
        self.n_parts += 1
        for c in self.childs:
            if c.idx > where:
                c.idx += 1
        self.setup_manipulators()
        self.auto_update = True

    def insert_balcony(self, context, where):
        self.manipulable_disable(context)
        self.auto_update = False

        # the part we do split
        part_0 = self.parts[where]
        part_0.length /= 3
        part_0.da /= 3

        # 1st part 90deg
        self.parts.add()
        part_1 = self.parts[len(self.parts) - 1]
        part_1.type = "S_SEG"
        part_1.length = 1.5
        part_1.da = part_0.da
        part_1.a0 = -pi / 2
        # move after current one
        self.parts.move(len(self.parts) - 1, where + 1)

        # 2nd part -90deg
        self.parts.add()
        part_1 = self.parts[len(self.parts) - 1]
        part_1.type = part_0.type
        part_1.length = part_0.length
        part_1.radius = part_0.radius + 1.5
        part_1.da = part_0.da
        part_1.a0 = pi / 2
        # move after current one
        self.parts.move(len(self.parts) - 1, where + 2)

        # 3nd part -90deg
        self.parts.add()
        part_1 = self.parts[len(self.parts) - 1]
        part_1.type = "S_SEG"
        part_1.length = 1.5
        part_1.da = part_0.da
        part_1.a0 = pi / 2
        # move after current one
        self.parts.move(len(self.parts) - 1, where + 3)

        # 4nd part -90deg
        self.parts.add()
        part_1 = self.parts[len(self.parts) - 1]
        part_1.type = part_0.type
        part_1.length = part_0.length
        part_1.radius = part_0.radius
        part_1.offset = part_0.offset
        part_1.da = part_0.da
        part_1.a0 = -pi / 2
        # move after current one
        self.parts.move(len(self.parts) - 1, where + 4)

        self.n_parts += 4
        self.setup_manipulators()

        for c in self.childs:
            if c.idx > where:
                c.idx += 4

        self.auto_update = True
        g = self.get_generator()

        o = context.active_object
        bpy.ops.archipack.fence(auto_manipulate=False)
        c = context.active_object
        c.select_set(state=True)
        c.data.archipack_fence[0].n_parts = 3
        c.select_set(state=False)
        # link to o
        c.location = Vector((0, 0, 0))
        c.parent = o
        c.location = g.segs[where + 1].p0.to_3d()
        self.add_child(c.name, where + 1)
        # c.matrix_world.translation = g.segs[where].p1.to_3d()
        o.select_set(state=True)
        context.view_layer.objects.active = o
        self.relocate_childs(context, o, g)

    def add_part(self, context, length):
        self.manipulable_disable(context)
        self.auto_update = False
        p = self.parts.add()
        p.length = length
        self.n_parts += 1
        self.setup_manipulators()
        self.auto_update = True
        return p

    def add_child(self, name, idx):
        c = self.childs.add()
        c.child_name = name
        c.idx = idx

    def setup_childs(self, o, g):
        """
            Store childs
            call after a boolean oop
        """
        # print("setup_childs")
        self.childs.clear()
        itM = o.matrix_world.inverted()

        dmax = 0.2
        for c in o.children:
            if (c.data and 'archipack_fence' in c.data):
                pt = (itM @ c.matrix_world.translation).to_2d()
                for idx, seg in enumerate(g.segs):
                    # may be optimized with a bound check
                    res, d, t = seg.point_sur_segment(pt)
                    #  p1
                    #  |-- x
                    #  p0
                    dist = abs(t) * seg.length
                    if dist < dmax and abs(d) < dmax:
                        # print("%s %s %s %s" % (idx, dist, d, c.name))
                        self.add_child(c.name, idx)

        # synch wall
        # store index of segments with p0 match
        if self.auto_synch:

            if o.parent is not None:

                for i, part in enumerate(self.parts):
                    part.linked_idx = -1

                # find first child wall
                d = None
                for c in o.parent.children:
                    if c.data and "archipack_wall2" in c.data:
                        d = c.data.archipack_wall2[0]
                        break

                if d is not None:
                    og = d.get_generator()
                    j = 0
                    for i, part in enumerate(self.parts):
                        ji = j
                        while ji < d.n_parts + 1:
                            if (g.segs[i].p0 - og.segs[ji].p0).length < 0.005:
                                j = ji + 1
                                part.linked_idx = ji
                                # print("link: %s to %s" % (i, ji))
                                break
                            ji += 1

    def relocate_childs(self, context, o, g):
        """
            Move and resize childs after edition
        """
        # print("relocate_childs")

        # Wall child syncro
        # must store - idx of shared segs
        # -> store this in parts provide 1:1 map
        # share type: full, start only, end only
        # -> may compute on the fly with idx stored
        # when full segment does match
        #  -update type, radius, length, a0, and da
        # when start only does match
        #  -update type, radius, a0
        # when end only does match
        #  -compute length/radius
        # @TODO:
        # handle p0 and p1 changes right in Generator (archipack_2d)
        # and retrieve params from there
        if self.auto_synch:
            if o.parent is not None:
                wall = None

                for child in o.parent.children:
                    if child.data and "archipack_wall2" in child.data:
                        wall = child
                        break

                if wall is not None:
                    d = wall.data.archipack_wall2[0]
                    d.auto_update = False
                    w = d.get_generator()

                    last_idx = -1

                    # update og from g
                    for i, part in enumerate(self.parts):
                        idx = part.linked_idx
                        seg = g.segs[i]

                        if i + 1 < self.n_parts:
                            next_idx = self.parts[i + 1].linked_idx
                        elif d.closed:
                            next_idx = self.parts[0].linked_idx
                        else:
                            next_idx = -1

                        if idx > -1:

                            # start and shared: update rotation
                            a = seg.angle - w.segs[idx].angle

                            if abs(a) > 0.00001:
                                w.rotate(idx, a)

                            if last_idx > -1:
                                w.segs[last_idx].p1 = seg.p0

                            if next_idx > -1:

                                if (idx + 1 == next_idx) or (next_idx == 0 and i + 1 == self.n_parts):
                                    # shared: should move last point
                                    # and apply to next segments
                                    # this is overridden for common segs
                                    # but translate non common ones
                                    dp = seg.p1 - w.segs[idx].p1
                                    w.translate(idx, dp)

                                    # shared: transfer type too
                                    if "C_" in part.type:
                                        d.parts[idx].type = 'C_WALL'
                                        w.segs[idx] = CurvedSlab(seg.c, seg.r, seg.a0, seg.da)
                                    else:
                                        d.parts[idx].type = 'S_WALL'
                                        w.segs[idx] = StraightSlab(seg.p.copy(), seg.v.copy())
                            last_idx = -1

                        elif next_idx > -1:
                            # only last is shared
                            # note: on next run will be part of start
                            last_idx = next_idx - 1

                    # update d from og
                    last_seg = None
                    for i, seg in enumerate(w.segs):

                        d.parts[i].a0 = seg.delta_angle(last_seg)

                        last_seg = seg

                        if "C_" in d.parts[i].type:
                            d.parts[i].radius = seg.r
                            d.parts[i].da = seg.da
                        else:
                            d.parts[i].length = max(0.01, seg.length)

                    wall.select_set(state=True)
                    context.view_layer.objects.active = wall

                    d.auto_update = True
                    wall.select_set(state=False)

                    o.select_set(state=True)
                    context.view_layer.objects.active = o

                    wall.matrix_world = o.matrix_world.copy()

        tM = o.matrix_world
        for child in self.childs:
            c, d = child.get_child(context)
            if c is None:
                continue

            a = g.segs[child.idx].angle
            x, y = g.segs[child.idx].p0
            sa = sin(a)
            ca = cos(a)

            if d is not None:
                c.select_set(state=True)

                # auto_update need object to be active to
                # setup manipulators on the right object
                context.view_layer.objects.active = c

                d.auto_update = False
                for i, part in enumerate(d.parts):
                    if "C_" in self.parts[i + child.idx].type:
                        part.type = "C_FENCE"
                    else:
                        part.type = "S_FENCE"
                    part.a0 = self.parts[i + child.idx].a0
                    part.da = self.parts[i + child.idx].da
                    part.length = self.parts[i + child.idx].length
                    part.radius = self.parts[i + child.idx].radius
                d.parts[0].a0 = pi / 2
                d.auto_update = True
                c.select_set(state=False)

                context.view_layer.objects.active = o
                # preTranslate
                c.matrix_world = tM @ Matrix([
                    [sa, ca, 0, x],
                    [-ca, sa, 0, y],
                    [0, 0, 1, 0],
                    [0, 0, 0, 1]
                ])

    def remove_part(self, context, where):
        self.manipulable_disable(context)
        self.auto_update = False

        # preserve shape
        # using generator
        if where > 0:

            g = self.get_generator()
            w = g.segs[where - 1]
            w.p1 = g.segs[where].p1

            if where + 1 < self.n_parts:
                self.parts[where + 1].a0 = g.segs[where + 1].delta_angle(w)

            part = self.parts[where - 1]

            if "C_" in part.type:
                part.radius = w.r
            else:
                part.length = w.length

            if where > 1:
                part.a0 = w.delta_angle(g.segs[where - 2])
            else:
                part.a0 = w.straight(1, 0).angle

        for c in self.childs:
            if c.idx >= where:
                c.idx -= 1
        self.parts.remove(where)
        self.n_parts -= 1
        # fix snap manipulators index
        self.setup_manipulators()
        self.auto_update = True

    def update_parts(self, o, update_childs=False):
        # print("update_parts")
        # remove rows
        # NOTE:
        # n_parts+1
        # as last one is end point of last segment or closing one
        row_change = False
        for i in range(len(self.parts), self.n_parts, -1):
            row_change = True
            self.parts.remove(i - 1)

        # add rows
        for i in range(len(self.parts), self.n_parts):
            row_change = True
            self.parts.add()

        self.setup_manipulators()

        g = self.get_generator()

        if o is not None and (row_change or update_childs):
            self.setup_childs(o, g)

        return g

    def setup_manipulators(self):

        if len(self.manipulators) < 1:
            s = self.manipulators.add()
            s.type_key = "SIZE"
            s.prop1_name = "z"
            s.normal = Vector((0, 1, 0))

        for i in range(self.n_parts):
            p = self.parts[i]
            n_manips = len(p.manipulators)
            if n_manips < 1:
                s = p.manipulators.add()
                s.type_key = "ANGLE"
                s.prop1_name = "a0"
            p.manipulators[0].type_key = 'ANGLE'
            if n_manips < 2:
                s = p.manipulators.add()
                s.type_key = "SIZE"
                s.prop1_name = "length"
            if n_manips < 3:
                s = p.manipulators.add()
                s.type_key = 'WALL_SNAP'
                s.prop1_name = str(i)
                s.prop2_name = 'z'
            if n_manips < 4:
                s = p.manipulators.add()
                s.type_key = 'DUMB_STRING'
                s.prop1_name = str(i + 1)
            p.manipulators[2].prop1_name = str(i)
            p.manipulators[3].prop1_name = str(i + 1)

        self.parts[-1].manipulators[0].type_key = 'DUMB_ANGLE'

    def is_cw(self, pts):
        p0 = pts[0]
        d = 0
        for p in pts[1:]:
            d += (p.x * p0.y - p.y * p0.x)
            p0 = p
        return d > 0

    def interpolate_bezier(self, pts, wM, p0, p1, resolution):
        # straight segment, worth testing here
        # since this can lower points count by a resolution factor
        # use normalized to handle non linear t
        if resolution == 0:
            pts.append(wM @ p0.co.to_3d())
        else:
            v = (p1.co - p0.co).normalized()
            d1 = (p0.handle_right - p0.co).normalized()
            d2 = (p1.co - p1.handle_left).normalized()
            if d1 == v and d2 == v:
                pts.append(wM @ p0.co.to_3d())
            else:
                seg = interpolate_bezier(wM @ p0.co,
                    wM @ p0.handle_right,
                    wM @ p1.handle_left,
                    wM @ p1.co,
                    resolution + 1)
                for i in range(resolution):
                    pts.append(seg[i].to_3d())

    def from_spline(self, wM, resolution, spline):
        pts = []
        if spline.type == 'POLY':
            pts = [wM @ p.co.to_3d() for p in spline.points]
            if spline.use_cyclic_u:
                pts.append(pts[0])
        elif spline.type == 'BEZIER':
            points = spline.bezier_points
            for i in range(1, len(points)):
                p0 = points[i - 1]
                p1 = points[i]
                self.interpolate_bezier(pts, wM, p0, p1, resolution)
            if spline.use_cyclic_u:
                p0 = points[-1]
                p1 = points[0]
                self.interpolate_bezier(pts, wM, p0, p1, resolution)
                pts.append(pts[0])
            else:
                pts.append(wM @ points[-1].co)

        self.from_points(pts, spline.use_cyclic_u)

    def from_points(self, pts, closed):

        if self.is_cw(pts):
            pts = list(reversed(pts))

        self.auto_update = False

        self.n_parts = len(pts) - 1

        self.update_parts(None)

        p0 = pts.pop(0)
        a0 = 0
        for i, p1 in enumerate(pts):
            dp = p1 - p0
            da = atan2(dp.y, dp.x) - a0
            if da > pi:
                da -= 2 * pi
            if da < -pi:
                da += 2 * pi
            if i >= len(self.parts):
                break
            p = self.parts[i]
            p.length = dp.to_2d().length
            p.dz = dp.z
            p.a0 = da
            a0 += da
            p0 = p1

        self.closed = closed
        self.auto_update = True

    def make_surface(self, o, verts):
        bm = bmesh.new()
        for v in verts:
            bm.verts.new(v)
        bm.verts.ensure_lookup_table()
        for i in range(1, len(verts)):
            bm.edges.new((bm.verts[i - 1], bm.verts[i]))
        bm.edges.new((bm.verts[-1], bm.verts[0]))
        bm.edges.ensure_lookup_table()
        bmesh.ops.contextual_create(bm, geom=bm.edges)
        bm.to_mesh(o.data)
        bm.free()

    def unwrap_uv(self, o):
        bm = bmesh.new()
        bm.from_mesh(o.data)
        for face in bm.faces:
            face.select = face.material_index > 0
        bm.to_mesh(o.data)
        bpy.ops.uv.cube_project(scale_to_bounds=False, correct_aspect=True)

        for face in bm.faces:
            face.select = face.material_index < 1
        bm.to_mesh(o.data)
        bpy.ops.uv.smart_project(use_aspect=True, stretch_to_bounds=False)
        bm.free()

    def update(self, context, manipulable_refresh=False, update_childs=False):

        o = self.find_in_selection(context, self.auto_update)

        if o is None:
            return

        # clean up manipulators before any data model change
        if manipulable_refresh:
            self.manipulable_disable(context)

        g = self.update_parts(o, update_childs)

        # relocate before cutting segs
        self.relocate_childs(context, o, g)

        o.select_set(state=True)
        context.view_layer.objects.active = o

        g.cut(context, o)

        g.slab(context, o, self)

        modif = o.modifiers.get('Slab')
        if modif is None:
            modif = o.modifiers.new('Slab', 'SOLIDIFY')
            modif.use_quality_normals = True
            modif.use_even_offset = True
            modif.material_offset_rim = 2
            modif.material_offset = 1

        modif.thickness = self.z
        modif.offset = 1.0
        o.data.use_auto_smooth = True
        bpy.ops.object.shade_smooth()

        # Height
        self.manipulators[0].set_pts([
            (0, 0, 0),
            (0, 0, -self.z),
            (-1, 0, 0)
            ], normal=g.segs[0].straight(-1, 0).v.to_3d())

        # enable manipulators rebuild
        if manipulable_refresh:
            self.manipulable_refresh = True

        # restore context
        self.restore_context(context)

    def manipulable_setup(self, context):
        """
            NOTE:
            this one assume context.active_object is the instance this
            data belongs to, failing to do so will result in wrong
            manipulators set on active object
        """
        self.manipulable_disable(context)

        o = context.active_object

        self.setup_manipulators()

        for i, part in enumerate(self.parts):
            if i >= self.n_parts:
                break

            if i > 0:
                # start angle
                self.manip_stack.append(part.manipulators[0].setup(context, o, part))

            # length / radius + angle
            self.manip_stack.append(part.manipulators[1].setup(context, o, part))

            # snap point
            self.manip_stack.append(part.manipulators[2].setup(context, o, self))
            # index
            self.manip_stack.append(part.manipulators[3].setup(context, o, self))

        for m in self.manipulators:
            self.manip_stack.append(m.setup(context, o, self))

    def manipulable_invoke(self, context):
        """
            call this in operator invoke()
        """
        # print("manipulable_invoke")
        if self.manipulate_mode:
            self.manipulable_disable(context)
            return False

        o = context.active_object
        g = self.get_generator()
        # setup childs manipulators
        self.setup_childs(o, g)
        self.manipulable_setup(context)
        self.manipulate_mode = True

        self._manipulable_invoke(context)

        return True


def update_hole(self, context):
    # update parent's roof only when manipulated
    self.update(context, update_parent=True)


def update_operation(self, context):
    self.reverse(context, make_ccw=(self.operation == 'INTERSECTION'))


class archipack_slab_cutter_segment(ArchipackCutterPart, PropertyGroup):
    manipulators : CollectionProperty(type=archipack_manipulator)
    type : EnumProperty(
        name="Type",
        items=(
            ('DEFAULT', 'Side', 'Side with rake', 0),
            ('BOTTOM', 'Bottom', 'Bottom with gutter', 1),
            ('LINK', 'Side link', 'Side without decoration', 2),
            ('AXIS', 'Top', 'Top part with hip and beam', 3)
            # ('LINK_VALLEY', 'Side valley', 'Side with valley', 3),
            # ('LINK_HIP', 'Side hip', 'Side with hip', 4)
            ),
        default='DEFAULT',
        update=update_hole
        )

    def find_in_selection(self, context):
        selected = context.selected_objects[:]
        for o in selected:
            d = archipack_slab_cutter.datablock(o)
            if d:
                for part in d.parts:
                    if part == self:
                        return d
        return None

    def draw(self, layout, context, index):
        box = layout.box()
        box.label(text="Part:" + str(index + 1))
        # box.prop(self, "type", text=str(index + 1))
        box.prop(self, "length")
        box.prop(self, "a0")


class archipack_slab_cutter(ArchipackCutter, ArchipackObject, Manipulable, PropertyGroup):
    parts : CollectionProperty(type=archipack_slab_cutter_segment)

    def update_points(self, context, o, pts, update_parent=False):
        self.auto_update = False
        self.from_points(pts)
        self.auto_update = True
        if update_parent:
            self.update_parent(context, o)

    def update_parent(self, context, o):

        d = archipack_slab.datablock(o.parent)
        if d is not None:
            o.parent.select_set(state=True)
            context.view_layer.objects.active = o.parent
            d.update(context)
        o.parent.select_set(state=False)
        context.view_layer.objects.active = o


class ARCHIPACK_PT_slab(Panel):
    """Archipack Slab"""
    bl_idname = "ARCHIPACK_PT_slab"
    bl_label = "Slab"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    # bl_context = 'object'
    bl_category = 'Archipack'

    @classmethod
    def poll(cls, context):
        return archipack_slab.filter(context.active_object)

    def draw(self, context):
        o = context.active_object
        prop = archipack_slab.datablock(o)
        if prop is None:
            return
        layout = self.layout
        layout.operator('archipack.slab_manipulate', icon='VIEW_PAN')
        box = layout.box()
        box.operator('archipack.slab_cutter').parent = o.name
        box = layout.box()
        box.prop(prop, 'z')
        box = layout.box()
        box.prop(prop, 'auto_synch')
        box = layout.box()
        row = box.row()
        if prop.parts_expand:
            row.prop(prop, 'parts_expand', icon="TRIA_DOWN", text="Parts", emboss=False)
            box.prop(prop, 'n_parts')
            # box.prop(prop, 'closed')
            for i, part in enumerate(prop.parts):
                part.draw(context, layout, i)
        else:
            row.prop(prop, 'parts_expand', icon="TRIA_RIGHT", text="Parts", emboss=False)


class ARCHIPACK_PT_slab_cutter(Panel):
    bl_idname = "ARCHIPACK_PT_slab_cutter"
    bl_label = "Slab Cutter"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'Archipack'

    @classmethod
    def poll(cls, context):
        return archipack_slab_cutter.filter(context.active_object)

    def draw(self, context):
        prop = archipack_slab_cutter.datablock(context.active_object)
        if prop is None:
            return
        layout = self.layout
        scene = context.scene
        box = layout.box()
        box.operator('archipack.slab_cutter_manipulate', icon='VIEW_PAN')
        box.prop(prop, 'operation', text="")
        box = layout.box()
        box.label(text="From curve")
        box.prop_search(prop, "user_defined_path", scene, "objects", text="", icon='OUTLINER_OB_CURVE')
        if prop.user_defined_path != "":
            box.prop(prop, 'user_defined_resolution')
            # box.prop(prop, 'x_offset')
            # box.prop(prop, 'angle_limit')
        """
        box.prop_search(prop, "boundary", scene, "objects", text="", icon='OUTLINER_OB_CURVE')
        """
        prop.draw(layout, context)


# ------------------------------------------------------------------
# Define operator class to create object
# ------------------------------------------------------------------


class ARCHIPACK_OT_slab_insert(Operator):
    bl_idname = "archipack.slab_insert"
    bl_label = "Insert"
    bl_description = "Insert part"
    bl_category = 'Archipack'
    bl_options = {'REGISTER', 'UNDO'}
    index : IntProperty(default=0)

    def execute(self, context):
        if context.mode == "OBJECT":
            d = archipack_slab.datablock(context.active_object)
            if d is None:
                return {'CANCELLED'}
            d.insert_part(context, self.index)
            return {'FINISHED'}
        else:
            self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
            return {'CANCELLED'}


class ARCHIPACK_OT_slab_balcony(Operator):
    bl_idname = "archipack.slab_balcony"
    bl_label = "Insert"
    bl_description = "Insert part"
    bl_category = 'Archipack'
    bl_options = {'REGISTER', 'UNDO'}
    index : IntProperty(default=0)

    def execute(self, context):
        if context.mode == "OBJECT":
            d = archipack_slab.datablock(context.active_object)
            if d is None:
                return {'CANCELLED'}
            d.insert_balcony(context, self.index)
            return {'FINISHED'}
        else:
            self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
            return {'CANCELLED'}


class ARCHIPACK_OT_slab_remove(Operator):
    bl_idname = "archipack.slab_remove"
    bl_label = "Remove"
    bl_description = "Remove part"
    bl_category = 'Archipack'
    bl_options = {'REGISTER', 'UNDO'}
    index : IntProperty(default=0)

    def execute(self, context):
        if context.mode == "OBJECT":
            d = archipack_slab.datablock(context.active_object)
            if d is None:
                return {'CANCELLED'}
            d.remove_part(context, self.index)
            return {'FINISHED'}
        else:
            self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
            return {'CANCELLED'}


# ------------------------------------------------------------------
# Define operator class to create object
# ------------------------------------------------------------------


class ARCHIPACK_OT_slab(ArchipackCreateTool, Operator):
    bl_idname = "archipack.slab"
    bl_label = "Slab"
    bl_description = "Slab"
    bl_category = 'Archipack'
    bl_options = {'REGISTER', 'UNDO'}

    def create(self, context):
        m = bpy.data.meshes.new("Slab")
        o = bpy.data.objects.new("Slab", m)
        d = m.archipack_slab.add()
        # make manipulators selectable
        d.manipulable_selectable = True
        self.link_object_to_scene(context, o)
        o.select_set(state=True)
        context.view_layer.objects.active = o
        self.load_preset(d)
        self.add_material(o)
        return o

    # -----------------------------------------------------
    # Execute
    # -----------------------------------------------------
    def execute(self, context):
        if context.mode == "OBJECT":
            bpy.ops.object.select_all(action="DESELECT")
            o = self.create(context)
            o.location = bpy.context.scene.cursor.location
            o.select_set(state=True)
            context.view_layer.objects.active = o
            self.manipulate()
            return {'FINISHED'}
        else:
            self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
            return {'CANCELLED'}


class ARCHIPACK_OT_slab_from_curve(Operator):
    bl_idname = "archipack.slab_from_curve"
    bl_label = "Slab curve"
    bl_description = "Create a slab from a curve"
    bl_category = 'Archipack'
    bl_options = {'REGISTER', 'UNDO'}

    auto_manipulate : BoolProperty(default=True)

    @classmethod
    def poll(self, context):
        return context.active_object is not None and context.active_object.type == 'CURVE'
    # -----------------------------------------------------
    # Draw (create UI interface)
    # -----------------------------------------------------
    # noinspection PyUnusedLocal

    def draw(self, context):
        layout = self.layout
        row = layout.row()
        row.label(text="Use Properties panel (N) to define parms", icon='INFO')

    def create(self, context):
        curve = context.active_object
        bpy.ops.archipack.slab(auto_manipulate=self.auto_manipulate)
        o = context.view_layer.objects.active
        d = archipack_slab.datablock(o)
        spline = curve.data.splines[0]
        d.from_spline(curve.matrix_world, 12, spline)
        if spline.type == 'POLY':
            pt = spline.points[0].co
        elif spline.type == 'BEZIER':
            pt = spline.bezier_points[0].co
        else:
            pt = Vector((0, 0, 0))
        # pretranslate
        o.matrix_world = curve.matrix_world @ Matrix.Translation(pt)
        return o

    # -----------------------------------------------------
    # Execute
    # -----------------------------------------------------
    def execute(self, context):
        if context.mode == "OBJECT":
            bpy.ops.object.select_all(action="DESELECT")
            self.create(context)
            return {'FINISHED'}
        else:
            self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
            return {'CANCELLED'}


class ARCHIPACK_OT_slab_from_wall(Operator):
    bl_idname = "archipack.slab_from_wall"
    bl_label = "->Slab"
    bl_description = "Create a slab from a wall"
    bl_category = 'Archipack'
    bl_options = {'REGISTER', 'UNDO'}

    auto_manipulate : BoolProperty(default=True)
    ceiling : BoolProperty(default=False)

    @classmethod
    def poll(self, context):
        o = context.active_object
        return o is not None and o.data is not None and 'archipack_wall2' in o.data

    def create(self, context):
        wall = context.active_object
        wd = wall.data.archipack_wall2[0]
        bpy.ops.archipack.slab(auto_manipulate=False)
        o = context.view_layer.objects.active
        d = archipack_slab.datablock(o)
        d.auto_update = False
        d.closed = True
        d.parts.clear()
        d.n_parts = wd.n_parts + 1
        for part in wd.parts:
            p = d.parts.add()
            if "S_" in part.type:
                p.type = "S_SEG"
            else:
                p.type = "C_SEG"
            p.length = part.length
            p.radius = part.radius
            p.da = part.da
            p.a0 = part.a0
        d.auto_update = True
        # pretranslate
        if self.ceiling:
            o.matrix_world = Matrix([
                [1, 0, 0, 0],
                [0, 1, 0, 0],
                [0, 0, 1, wd.z + d.z],
                [0, 0, 0, 1],
                ]) @ wall.matrix_world
        else:
            o.matrix_world = wall.matrix_world.copy()
            bpy.ops.object.select_all(action='DESELECT')
            # parenting childs to wall reference point
            if wall.parent is None:
                x, y, z = wall.bound_box[0]
                context.scene.cursor.location = wall.matrix_world @ Vector((x, y, z))
                # fix issue #9
                context.view_layer.objects.active = wall
                bpy.ops.archipack.reference_point()
            else:
                wall.parent.select_set(state=True)
                context.view_layer.objects.active = wall.parent
            wall.select_set(state=True)
            o.select_set(state=True)
            bpy.ops.archipack.parent_to_reference()
            wall.parent.select_set(state=False)

        return o

    # -----------------------------------------------------
    # Execute
    # -----------------------------------------------------
    def execute(self, context):
        if context.mode == "OBJECT":
            bpy.ops.object.select_all(action="DESELECT")
            o = self.create(context)
            o.select_set(state=True)
            context.view_layer.objects.active = o
            if self.auto_manipulate:
                bpy.ops.archipack.slab_manipulate('INVOKE_DEFAULT')
            return {'FINISHED'}
        else:
            self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
            return {'CANCELLED'}


class ARCHIPACK_OT_slab_cutter(ArchipackCreateTool, Operator):
    bl_idname = "archipack.slab_cutter"
    bl_label = "Slab Cutter"
    bl_description = "Slab Cutter"
    bl_category = 'Archipack'
    bl_options = {'REGISTER', 'UNDO'}

    parent : StringProperty("")

    def create(self, context):
        m = bpy.data.meshes.new("Slab Cutter")
        o = bpy.data.objects.new("Slab Cutter", m)
        d = m.archipack_slab_cutter.add()
        parent = context.scene.objects.get(self.parent.strip())
        if parent is not None:
            o.parent = parent
            bbox = parent.bound_box
            angle_90 = pi / 2
            x0, y0, z = bbox[0]
            x1, y1, z = bbox[6]
            x = 0.2 * (x1 - x0)
            y = 0.2 * (y1 - y0)
            o.matrix_world = parent.matrix_world @ Matrix([
                [1, 0, 0, -3 * x],
                [0, 1, 0, 0],
                [0, 0, 1, 0],
                [0, 0, 0, 1]
                ])
            p = d.parts.add()
            p.a0 = - angle_90
            p.length = y
            p = d.parts.add()
            p.a0 = angle_90
            p.length = x
            p = d.parts.add()
            p.a0 = angle_90
            p.length = y
            d.n_parts = 3
            # d.close = True
            pd = archipack_slab.datablock(parent)
            pd.boundary = o.name
        else:
            o.location = context.scene.cursor.location
        # make manipulators selectable
        d.manipulable_selectable = True
        self.link_object_to_scene(context, o)
        o.select_set(state=True)
        context.view_layer.objects.active = o
        # self.add_material(o)
        self.load_preset(d)
        update_operation(d, context)
        return o

    # -----------------------------------------------------
    # Execute
    # -----------------------------------------------------
    def execute(self, context):
        if context.mode == "OBJECT":
            bpy.ops.object.select_all(action="DESELECT")
            o = self.create(context)
            o.select_set(state=True)
            context.view_layer.objects.active = o
            self.manipulate()
            return {'FINISHED'}
        else:
            self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
            return {'CANCELLED'}


# ------------------------------------------------------------------
# Define operator class to manipulate object
# ------------------------------------------------------------------


class ARCHIPACK_OT_slab_manipulate(Operator):
    bl_idname = "archipack.slab_manipulate"
    bl_label = "Manipulate"
    bl_description = "Manipulate"
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(self, context):
        return archipack_slab.filter(context.active_object)

    def invoke(self, context, event):
        d = archipack_slab.datablock(context.active_object)
        d.manipulable_invoke(context)
        return {'FINISHED'}


class ARCHIPACK_OT_slab_cutter_manipulate(Operator):
    bl_idname = "archipack.slab_cutter_manipulate"
    bl_label = "Manipulate"
    bl_description = "Manipulate"
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(self, context):
        return archipack_slab_cutter.filter(context.active_object)

    def invoke(self, context, event):
        d = archipack_slab_cutter.datablock(context.active_object)
        d.manipulable_invoke(context)
        return {'FINISHED'}


def register():
    bpy.utils.register_class(archipack_slab_cutter_segment)
    bpy.utils.register_class(archipack_slab_cutter)
    Mesh.archipack_slab_cutter = CollectionProperty(type=archipack_slab_cutter)
    bpy.utils.register_class(ARCHIPACK_OT_slab_cutter)
    bpy.utils.register_class(ARCHIPACK_PT_slab_cutter)
    bpy.utils.register_class(ARCHIPACK_OT_slab_cutter_manipulate)

    bpy.utils.register_class(archipack_slab_material)
    bpy.utils.register_class(archipack_slab_child)
    bpy.utils.register_class(archipack_slab_part)
    bpy.utils.register_class(archipack_slab)
    Mesh.archipack_slab = CollectionProperty(type=archipack_slab)
    bpy.utils.register_class(ARCHIPACK_PT_slab)
    bpy.utils.register_class(ARCHIPACK_OT_slab)
    bpy.utils.register_class(ARCHIPACK_OT_slab_insert)
    bpy.utils.register_class(ARCHIPACK_OT_slab_balcony)
    bpy.utils.register_class(ARCHIPACK_OT_slab_remove)
    # bpy.utils.register_class(ARCHIPACK_OT_slab_manipulate_ctx)
    bpy.utils.register_class(ARCHIPACK_OT_slab_manipulate)
    bpy.utils.register_class(ARCHIPACK_OT_slab_from_curve)
    bpy.utils.register_class(ARCHIPACK_OT_slab_from_wall)


def unregister():
    bpy.utils.unregister_class(archipack_slab_material)
    bpy.utils.unregister_class(archipack_slab_child)
    bpy.utils.unregister_class(archipack_slab_part)
    bpy.utils.unregister_class(archipack_slab)
    del Mesh.archipack_slab
    bpy.utils.unregister_class(ARCHIPACK_PT_slab)
    bpy.utils.unregister_class(ARCHIPACK_OT_slab)
    bpy.utils.unregister_class(ARCHIPACK_OT_slab_insert)
    bpy.utils.unregister_class(ARCHIPACK_OT_slab_balcony)
    bpy.utils.unregister_class(ARCHIPACK_OT_slab_remove)
    # bpy.utils.unregister_class(ARCHIPACK_OT_slab_manipulate_ctx)
    bpy.utils.unregister_class(ARCHIPACK_OT_slab_manipulate)
    bpy.utils.unregister_class(ARCHIPACK_OT_slab_from_curve)
    bpy.utils.unregister_class(ARCHIPACK_OT_slab_from_wall)
    del Mesh.archipack_slab_cutter
    bpy.utils.unregister_class(archipack_slab_cutter_segment)
    bpy.utils.unregister_class(archipack_slab_cutter)
    bpy.utils.unregister_class(ARCHIPACK_OT_slab_cutter)
    bpy.utils.unregister_class(ARCHIPACK_PT_slab_cutter)
    bpy.utils.unregister_class(ARCHIPACK_OT_slab_cutter_manipulate)
