# -*- 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: Jacob Morris - Stephen Leger (s-leger)
# ----------------------------------------------------------

import bpy
from bpy.types import Operator, PropertyGroup, Mesh, Panel
from bpy.props import (
    FloatProperty, CollectionProperty, StringProperty,
    BoolProperty, IntProperty, EnumProperty
    )
from mathutils import Vector, Matrix
from mathutils.geometry import interpolate_bezier
from random import uniform
from math import radians, cos, sin, pi, atan2, sqrt
import bmesh
from .bmesh_utils import BmeshEdit as bmed
from .archipack_2d import Line, Arc
from .archipack_manipulator import Manipulable, archipack_manipulator
from .archipack_preset import ArchipackPreset, PresetMenuOperator
from .archipack_object import ArchipackCreateTool, ArchipackObject
from .archipack_cutter import (
    CutAblePolygon, CutAbleGenerator,
    ArchipackCutter,
    ArchipackCutterPart
    )


# ------------------------------------------------------------------
# Define property class to store object parameters and update mesh
# ------------------------------------------------------------------


class Floor():

    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_floor(self, a0, length):
        s = self.straight(length).rotate(a0)
        return StraightFloor(s.p, s.v)

    def curved_floor(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 CurvedFloor(c, radius, a0, da)


class StraightFloor(Floor, Line):

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


class CurvedFloor(Floor, Arc):

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


class FloorGenerator(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 floor
        if s is None:
            if part.type == 'S_SEG':
                p = Vector((0, 0))
                v = part.length * Vector((cos(part.a0), sin(part.a0)))
                s = StraightFloor(p, v)
            elif part.type == 'C_SEG':
                c = -part.radius * Vector((cos(part.a0), sin(part.a0)))
                s = CurvedFloor(c, part.radius, part.a0, part.da)
        else:
            if part.type == 'S_SEG':
                s = s.straight_floor(part.a0, part.length)
            elif part.type == 'C_SEG':
                s = s.curved_floor(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__ == "StraightFloor":
                # 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, dont 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 = floor.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 = floor.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]
        y_size = [s.p0.y for s in self.segs]
        for s in self.segs:
            if "Curved" in type(s).__name__:
                x_size.append(s.c.x + s.r)
                x_size.append(s.c.x - s.r)
                y_size.append(s.c.y + s.r)
                y_size.append(s.c.y - s.r)

        self.xmin = min(x_size)
        self.xmax = max(x_size)
        self.xsize = self.xmax - self.xmin
        self.ymin = min(y_size)
        self.ymax = max(y_size)
        self.ysize = self.ymax - self.ymin

    def cut(self, context, o):
        """
            either external or holes cuts
        """
        self.limits()
        self.as_lines()
        self.is_convex()
        for b in o.children:
            d = archipack_floor_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 floor(self, context, o, d):

        verts, faces, matids, uvs = [], [], [], []

        if d.bevel:
            bevel = d.bevel_amount
        else:
            bevel = 0

        if d.add_grout:
            thickness = min(d.thickness - d.mortar_depth, d.thickness - 0.0001)
            bottom = min(d.thickness - (d.mortar_depth + bevel), d.thickness - 0.0001)
        else:
            thickness = d.thickness
            bottom = 0

        self.top = d.thickness

        self.generate_pattern(d, verts, faces, matids, uvs)
        bm = bmed.buildmesh(
                context, o, verts, faces, matids=matids, uvs=uvs,
                weld=False, clean=False, auto_smooth=True, temporary=True)

        self.cut_holes(bm, self)
        self.cut_boundary(bm, self)

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

        bm.verts.ensure_lookup_table()

        if d.solidify:
            # solidify and floor bottom
            geom = bm.faces[:]
            verts = bm.verts[:]
            edges = bm.edges[:]
            bmesh.ops.solidify(bm, geom=geom, thickness=0.0001)
            for v in verts:
                v.co.z = bottom

            bm.normal_update()

            # bevel
            if d.bevel:
                for v in bm.verts:
                    v.select = True
                for v in verts:
                    v.select = False
                for v in bm.edges:
                    v.select = True
                for v in edges:
                    v.select = False
                geom = [v for v in bm.verts if v.select]
                geom.extend([v for v in bm.edges if v.select])
                bmesh.ops.bevel(bm,
                    geom=geom,
                    offset=d.bevel_amount,
                    offset_type='OFFSET',
                    segments=1,     # d.bevel_res
                    profile=0.5,
                    # vertex_only=False,
                    clamp_overlap=False,
                    material=-1)

        bm.to_mesh(o.data)
        bm.free()

        # Grout
        if d.add_grout:
            verts = []
            self.get_verts(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)

            self.cut_holes(bm, self)
            self.cut_boundary(bm, self)

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

            bm.verts.ensure_lookup_table()

            geom = bm.faces[:]
            bmesh.ops.solidify(bm, geom=geom, thickness=thickness)
            bmed.bmesh_join(context, o, [bm], normal_update=True)

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

    # ---------------------------------------------------
    # Patterns
    # ---------------------------------------------------

    def regular_tile(self, d, verts, faces, matids, uvs):
        """
         ____  ____  ____
        |    ||    ||    | Regular tile, rows can be offset, either manually or randomly
        |____||____||____|
           ____  ____  ____
          |    ||    ||    |
          |____||____||____|
        """
        off = False
        o = 1 / (100 / d.offset) if d.offset != 0 else 0
        y = self.ymin

        while y < self.ymax:
            x = self.xmin
            tl2 = d.tile_length
            if y < self.ymax < y + d.tile_length:
                tl2 = self.ymax - y

            while x < self.xmax:
                tw2 = d.tile_width

                if x < self.xmax < x + d.tile_width:
                    tw2 = self.xmax - x
                elif x == self.xmin and off and not d.random_offset:
                    tw2 = d.tile_width * o
                elif x == self.xmin and d.random_offset:
                    v = d.tile_width * d.offset_variance * 0.0049
                    tw2 = (d.tile_width / 2) + uniform(-v, v)

                self.add_plane(d, verts, faces, matids, uvs, x, y, tw2, tl2)
                x += tw2 + d.spacing

            y += tl2 + d.spacing
            off = not off

    def hopscotch(self, d, verts, faces, matids, uvs):
        """
         ____  _  Large tile, plus small one on top right corner
        |    ||_|
        |____| ____  _  But shifted up so next large one is right below previous small one
              |    ||_|
              |____|
        """
        sp = d.spacing

        # movement variables
        row = 0

        tw = d.tile_width
        tl = d.tile_length
        s_tw = (tw - sp) / 2  # small tile width
        s_tl = (tl - sp) / 2  # small tile length
        y = self.ymin - s_tl

        pre_y = y
        while y < self.ymax + s_tl or (row == 2 and y - sp < self.ymax):
            x = self.xmin
            step_back = True

            if row == 1:  # row start indented slightly
                x = self.xmin + s_tw + sp

            while x < self.xmax:
                if row == 0 or row == 1:
                    # adjust for if there is a need to cut off the bottom of the tile
                    if y < self.ymin - s_tl:
                        self.add_plane(d, verts, faces, matids, uvs, x, y, tw, tl + y - self.ymin)  # large one
                    else:
                        self.add_plane(d, verts, faces, matids, uvs, x, y, tw, tl)  # large one

                    self.add_plane(d, verts, faces, matids, uvs, x + tw + sp, y + s_tl + sp, s_tw, s_tl)  # small one

                    if step_back:
                        x += tw + sp
                        y -= s_tl + sp
                    else:
                        x += tw + s_tw + 2 * sp
                        y += s_tl + sp

                    step_back = not step_back
                else:
                    if x == self.xmin:  # half width for starting position
                        self.add_plane(d, verts, faces, matids, uvs, x, y, s_tw, tl)  # large one
                        # small one on right
                        self.add_plane(d, verts, faces, matids, uvs, x + s_tw + sp, y + s_tl + sp, s_tw, s_tl)
                        # small one on bottom
                        self.add_plane(d, verts, faces, matids, uvs, x, y - sp - s_tl, s_tw, s_tl)
                        x += (2 * s_tw) + tw + (3 * sp)
                    else:
                        self.add_plane(d, verts, faces, matids, uvs, x, y, tw, tl)  # large one
                        # small one on right
                        self.add_plane(d, verts, faces, matids, uvs, x + tw + sp, y + s_tl + sp, s_tw, s_tl)
                        x += (2 * tw) + (3 * sp) + s_tw

            if row == 0 or row == 2:
                y = pre_y + tl + sp
            else:
                y = pre_y + s_tl + sp
            pre_y = y

            row = (row + 1) % 3  # keep wrapping rows

    def stepping_stone(self, d, verts, faces, matids, uvs):
        """
         ____  __  ____
        |    ||__||    | Row of large one, then two small ones stacked beside it
        |    | __ |    |
        |____||__||____|
         __  __  __  __
        |__||__||__||__| Row of smalls
        """
        sp = d.spacing
        y = self.ymin
        row = 0

        tw = d.tile_width
        tl = d.tile_length
        s_tw = (tw - sp) / 2
        s_tl = (tl - sp) / 2

        while y < self.ymax:
            x = self.xmin

            while x < self.xmax:
                if row == 0:  # large one then two small ones stacked beside it
                    self.add_plane(d, verts, faces, matids, uvs, x, y, tw, tl)
                    self.add_plane(d, verts, faces, matids, uvs, x + tw + sp, y, s_tw, s_tl,)
                    self.add_plane(d, verts, faces, matids, uvs, x + tw + sp, y + s_tl + sp, s_tw, s_tl)
                    x += tw + s_tw + (2 * sp)
                else:  # row of small ones
                    self.add_plane(d, verts, faces, matids, uvs, x, y, s_tw, s_tl)
                    self.add_plane(d, verts, faces, matids, uvs, x + s_tw + sp, y, s_tw, s_tl)
                    x += tw + sp

            if row == 0:
                y += tl + sp
            else:
                y += s_tl + sp

            row = (row + 1) % 2

    def hexagon(self, d, verts, faces, matids, uvs):
        """
          __  Hexagon tiles
        /   \
        \___/
        """
        sp = d.spacing
        width = d.tile_width
        dia = (width / 2) / cos(radians(30))
        #               top of current, half way up next,    vertical spacing component
        vertical_spacing = dia * (1 + sin(radians(30))) + (sp * sin(radians(60)))  # center of one row to next row
        da = pi / 3
        base_points = [(sin(i * da), cos(i * da)) for i in range(6)]

        y = self.ymin
        offset = False
        while y - width / 2 < self.ymax:  # place tile as long as bottom is still within bounds
            if offset:
                x = self.xmin + width / 2
            else:
                x = self.xmin - sp / 2

            while x - width / 2 < self.xmax:  # place tile as long as left is still within bounds
                f = len(verts)

                if d.vary_thickness and d.thickness_variance > 0:
                    v = d.thickness / 100 * d.thickness_variance
                    z = uniform(self.top, self.top + v)
                else:
                    z = self.top

                for pt in base_points:
                    verts.append((dia * pt[0] + x, dia * pt[1] + y, z))

                faces.append([f] + [i for i in range(f + 1, len(verts))])
                uvs.append(base_points)
                self.add_matid(d, matids)

                x += width + sp

            y += vertical_spacing
            offset = not offset

    def windmill(self, d, verts, faces, matids, uvs):
        """
         __  ____
        |  ||____| This also has a square one in the middle, totaling 5 tiles per pattern
        |__|   __
         ____ |  |
        |____||__|
        """
        sp = d.spacing

        tw = d.tile_width
        tl = d.tile_length
        s_tw = (tw - sp) / 2
        s_tl = (tl - sp) / 2

        y = self.ymin
        while y < self.ymax:
            x = self.xmin

            while x < self.xmax:
                self.add_plane(d, verts, faces, matids, uvs, x, y, tw, s_tl)  # bottom
                self.add_plane(d, verts, faces, matids, uvs, x + tw + sp, y, s_tw, tl, rotate_uv=True)  # right
                self.add_plane(d, verts, faces, matids, uvs, x + s_tw + sp, y + tl + sp, tw, s_tl)  # top
                self.add_plane(d, verts, faces, matids, uvs, x, y + s_tl + sp, s_tw, tl, rotate_uv=True)  # left
                self.add_plane(d, verts, faces, matids, uvs, x + s_tw + sp, y + s_tl + sp, s_tw, s_tl)  # center

                x += tw + s_tw + (2 * sp)
            y += tl + s_tl + (2 * sp)

    def boards(self, d, verts, faces, matids, uvs):
        """
        ||| Typical wood boards
        |||
        """
        x = self.xmin
        bw, bl = d.board_width, d.board_length
        off = False
        o = 1 / (100 / d.offset) if d.offset != 0 else 0

        while x < self.xmax:
            if d.vary_width:
                v = bw * (d.width_variance / 100) * 0.99
                bw2 = bw + uniform(-v, v)
            else:
                bw2 = bw

            if bw2 + x > self.xmax:
                bw2 = self.xmax - x
            y = self.ymin

            counter = 1
            while y < self.ymax:
                bl2 = bl
                if d.vary_length:
                    v = bl * (d.length_variance / 100) * 0.99
                    bl2 = bl + uniform(-v, v)
                elif y == self.ymin and off and not d.random_offset:
                    bl2 = bl * o
                elif y == self.ymin and d.random_offset:
                    v = bl * d.offset_variance * 0.0049
                    bl2 = (bl / 2) + uniform(-v, v)

                if (counter >= d.max_boards and d.vary_length) or y + bl2 > self.ymax:
                    bl2 = self.ymax - y

                self.add_plane(d, verts, faces, matids, uvs, x, y, bw2, bl2, rotate_uv=True)
                y += bl2 + d.length_spacing
                counter += 1
            off = not off
            x += bw2 + d.width_spacing

    def square_parquet(self, d, verts, faces, matids, uvs):
        """
        ||--||-- Alternating groups oriented either horizontally, or forwards and backwards.
        ||--||-- self.spacing is used because it is the same spacing for width and length
        --||--|| Board width is calculated using number of boards and the length.
        --||--||
        """
        x = self.xmin
        start_orient_length = True

        # figure board width
        bl = d.short_board_length
        bw = (bl - (d.boards_in_group - 1) * d.spacing) / d.boards_in_group
        while x < self.xmax:
            y = self.ymin
            orient_length = start_orient_length
            while y < self.ymax:

                if orient_length:
                    start_x = x

                    for i in range(d.boards_in_group):
                        if x < self.xmax and y < self.ymax:
                            self.add_plane(d, verts, faces, matids, uvs, x, y, bw, bl, rotate_uv=True)
                            x += bw + d.spacing

                    x = start_x
                    y += bl + d.spacing

                else:
                    for i in range(d.boards_in_group):
                        if x < self.xmax and y < self.ymax:
                            self.add_plane(d, verts, faces, matids, uvs, x, y, bl, bw)
                            y += bw + d.spacing

                orient_length = not orient_length

            start_orient_length = not start_orient_length
            x += bl + d.spacing

    def herringbone(self, d, verts, faces, matids, uvs):
        """
        Boards are at 45 degree angle, in chevron pattern, ends are angled
        """
        width_dif = d.board_width / cos(radians(45))
        x_dif = d.short_board_length * cos(radians(45))
        y_dif = d.short_board_length * sin(radians(45))
        total_y_dif = width_dif + y_dif
        sp_dif = d.spacing / cos(radians(45))

        y = self.ymin - y_dif
        while y < self.ymax:
            x = self.xmin

            while x < self.xmax:
                # left side

                self.add_face(d, verts, faces, matids, uvs,
                    (x, y, 0), (x + x_dif, y + y_dif, 0),
                    (x + x_dif, y + total_y_dif, 0), (x, y + width_dif, 0))

                x += x_dif + d.spacing

                # right side
                if x < self.xmax:
                    self.add_face(d, verts, faces, matids, uvs,
                        (x, y + y_dif, 0), (x + x_dif, y, 0),
                        (x + x_dif, y + width_dif, 0), (x, y + total_y_dif, 0))
                    x += x_dif + d.spacing

            y += width_dif + sp_dif  # adjust spacing amount for 45 degree angle

    def herringbone_parquet(self, d, verts, faces, matids, uvs):
        """
        Boards are at 45 degree angle, in chevron pattern, ends are square, not angled
        """

        an_45 = 0.5 * sqrt(2)

        x_dif = d.short_board_length * an_45
        y_dif = d.short_board_length * an_45
        y_dif_45 = d.board_width * an_45
        x_dif_45 = d.board_width * an_45
        total_y_dif = y_dif + y_dif_45

        sp_dif = (d.spacing / an_45) / 2  # divide by two since it is used for both x and y
        width_dif = d.board_width / an_45

        y = self.ymin - y_dif
        while y - y_dif_45 < self.ymax:  # continue as long as bottom left corner is still good
            x = self.xmin

            while x - x_dif_45 < self.xmax:  # continue as long as top left corner is still good
                # left side

                self.add_face(d, verts, faces, matids, uvs,
                    (x, y, 0),
                    (x + x_dif, y + y_dif, 0),
                    (x + x_dif - x_dif_45, y + total_y_dif, 0),
                    (x - x_dif_45, y + y_dif_45, 0))

                x += x_dif - x_dif_45 + sp_dif
                y0 = y + y_dif - y_dif_45 - sp_dif

                if x < self.xmax:
                    self.add_face(d, verts, faces, matids, uvs,
                        (x, y0, 0),
                        (x + x_dif, y0 - y_dif, 0),
                        (x + x_dif + x_dif_45, y0 - y_dif + y_dif_45, 0),
                        (x + x_dif_45, y0 + y_dif_45, 0))

                    x += x_dif + x_dif_45 + sp_dif

                else:  # we didn't place the right board, so step ahead far enough the the while loop for x breaks
                    break

            y += width_dif + (2 * sp_dif)

    def add_matid(self, d, matids):
        if d.vary_materials:
            matid = uniform(1, d.matid)
        else:
            matid = d.matid
        matids.append(matid)

    def add_plane(self, d, verts, faces, matids, uvs, x, y, w, l, rotate_uv=False):
        """
        Adds vertices and faces for a place, clip to outer boundaries if clip is True
        :param x: start x position
        :param y: start y position
        :param w: width (in x direction)
        :param l: length (in y direction)
        """

        x1 = x + w
        y1 = y + l

        if d.vary_thickness and d.thickness_variance > 0:
            v = d.thickness / 100 * d.thickness_variance
            z = uniform(self.top, self.top + v)
        else:
            z = self.top

        p = len(verts)
        verts.extend([(x, y, z), (x1, y, z), (x1, y1, z), (x, y1, z)])
        faces.append([p + 3, p + 2, p + 1, p])
        if rotate_uv:
            uvs.append([(0, 0), (0, 1), (1, 1), (1, 0)])
        else:
            uvs.append([(0, 0), (1, 0), (1, 1), (0, 1)])
        self.add_matid(d, matids)

    def add_face(self, d, verts, faces, matids, uvs, p0, p1, p2, p3):
        """
        Adds vertices and faces for a place, clip to outer boundaries if clip is True
        :param x: start x position
        :param y: start y position
        :param w: width (in x direction)
        :param l: length (in y direction)
        """

        if d.vary_thickness and d.thickness_variance > 0:
            v = d.thickness / 100 * d.thickness_variance
            z = uniform(self.top, self.top + v)
        else:
            z = self.top

        p = len(verts)
        verts.extend([(v[0], v[1], z) for v in [p0, p1, p2, p3]])
        faces.append([p + 3, p + 2, p + 1, p])
        uvs.append([(0, 0), (1, 0), (1, 1), (0, 1)])
        self.add_matid(d, matids)

    def add_manipulator(self, name, pt1, pt2, pt3):
        m = self.manipulators.add()
        m.prop1_name = name
        m.set_pts([pt1, pt2, pt3])

    def generate_pattern(self, d, verts, faces, matids, uvs):

        if d.pattern == "boards":
            self.boards(d, verts, faces, matids, uvs)
        elif d.pattern == "square_parquet":
            self.square_parquet(d, verts, faces, matids, uvs)
        elif d.pattern == "herringbone":
            self.herringbone(d, verts, faces, matids, uvs)
        elif d.pattern == "herringbone_parquet":
            self.herringbone_parquet(d, verts, faces, matids, uvs)
        elif d.pattern == "regular_tile":
            self.regular_tile(d, verts, faces, matids, uvs)
        elif d.pattern == "hopscotch":
            self.hopscotch(d, verts, faces, matids, uvs)
        elif d.pattern == "stepping_stone":
            self.stepping_stone(d, verts, faces, matids, uvs)
        elif d.pattern == "hexagon":
            self.hexagon(d, verts, faces, matids, uvs)
        elif d.pattern == "windmill":
            self.windmill(d, verts, faces, matids, uvs)


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


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_floor(part.a0, part.length)
            else:
                w = w0.curved_floor(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 = StraightFloor(p, v)
                a0 = pi / 2
            else:
                c = -self.radius * Vector((cos(self.a0), sin(self.a0)))
                w = CurvedFloor(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


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


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


class archipack_floor_part(PropertyGroup):

    """
        A single manipulable polyline like segment
        polyline like segment line or arc based
    """
    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="Side offset of segment",
            default=0,
            unit='LENGTH', subtype='DISTANCE',
            update=update
            )
    manipulators : CollectionProperty(type=archipack_manipulator)

    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_floor.datablock(o)
            if props:
                for part in props.parts:
                    if part == self:
                        return props
        return None

    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(self, context, layout, index):
        box = layout.box()
        # box.prop(self, "type", text=str(index + 1))
        box.label(text="#" + str(index + 1))
        if self.type in ['C_SEG']:
            box.prop(self, "radius")
            box.prop(self, "da")
        else:
            box.prop(self, "length")
        box.prop(self, "a0")


class archipack_floor(ArchipackObject, Manipulable, PropertyGroup):
    n_parts : IntProperty(
            name="Parts",
            min=1,
            default=1, update=update_manipulators
            )
    parts : CollectionProperty(type=archipack_floor_part)
    user_defined_path : StringProperty(
            name="User defined",
            update=update_path
            )
    user_defined_resolution : IntProperty(
            name="Resolution",
            min=1,
            max=128,
            default=12, update=update_path
            )
    closed : BoolProperty(
            default=True,
            name="Close",
            options={'SKIP_SAVE'},
            update=update_manipulators
            )
    # UI layout related
    parts_expand : BoolProperty(
            options={'SKIP_SAVE'},
            default=False
            )

    pattern : EnumProperty(
            name='Floor Pattern',
            items=(("boards", "Boards", ""),
                    ("square_parquet", "Square Parquet", ""),
                    ("herringbone_parquet", "Herringbone Parquet", ""),
                    ("herringbone", "Herringbone", ""),
                    ("regular_tile", "Regular Tile", ""),
                    ("hopscotch", "Hopscotch", ""),
                    ("stepping_stone", "Stepping Stone", ""),
                    ("hexagon", "Hexagon", ""),
                    ("windmill", "Windmill", "")),
            default="boards",
            update=update
            )
    spacing : FloatProperty(
            name='Spacing',
            description='The amount of space between boards or tiles in both directions',
            unit='LENGTH', subtype='DISTANCE',
            min=0,
            default=0.005,
            precision=2,
            update=update
            )
    thickness : FloatProperty(
            name='Thickness',
            description='Thickness',
            unit='LENGTH', subtype='DISTANCE',
            min=0.0,
            default=0.005,
            precision=2,
            update=update
            )
    vary_thickness : BoolProperty(
            name='Random Thickness',
            description='Vary thickness',
            default=False,
            update=update
            )
    thickness_variance : FloatProperty(
            name='Variance',
            description='How much vary by',
            min=0, max=100,
            default=25,
            precision=2,
            subtype='PERCENTAGE',
            update=update
            )

    board_width : FloatProperty(
            name='Width',
            description='The width',
            unit='LENGTH', subtype='DISTANCE',
            min=0.02,
            default=0.2,
            precision=2,
            update=update
            )
    vary_width : BoolProperty(
            name='Random Width',
            description='Vary width',
            default=False,
            update=update
            )
    width_variance : FloatProperty(
            name='Variance',
            description='How much vary by',
            subtype='PERCENTAGE',
            min=1, max=100, default=50,
            precision=2,
            update=update
            )
    width_spacing : FloatProperty(
            name='Width Spacing',
            description='The amount of space between boards in the width direction',
            unit='LENGTH', subtype='DISTANCE',
            min=0,
            default=0.002,
            precision=2,
            update=update
            )

    board_length : FloatProperty(
            name='Length',
            description='The length of the boards',
            unit='LENGTH', subtype='DISTANCE',
            precision=2,
            min=0.02,
            default=2,
            update=update
            )
    short_board_length : FloatProperty(
            name='Length',
            description='The length of the boards',
            unit='LENGTH', subtype='DISTANCE',
            precision=2,
            min=0.02,
            default=2,
            update=update
            )
    vary_length : BoolProperty(
            name='Random Length',
            description='Vary board length',
            default=False,
            update=update
            )
    length_variance : FloatProperty(
            name='Variance',
            description='How much board length can vary by',
            subtype='PERCENTAGE',
            min=1, max=100, default=50,
            precision=2, update=update
            )
    max_boards : IntProperty(
            name='Max Boards',
            description='Max number of boards in one row',
            min=1,
            default=20,
            update=update
            )
    length_spacing : FloatProperty(
            name='Length Spacing',
            description='The amount of space between boards in the length direction',
            unit='LENGTH', subtype='DISTANCE',
            min=0,
            default=0.002,
            precision=2,
            update=update
            )

    # parquet specific
    boards_in_group : IntProperty(
            name='Boards in Group',
            description='Number of boards in a group',
            min=1, default=4,
            update=update
            )

    # tile specific
    tile_width : FloatProperty(
            name='Width',
            description='Width of the tiles',
            unit='LENGTH', subtype='DISTANCE',
            min=0.002,
            default=0.2,
            precision=2,
            update=update
            )
    tile_length : FloatProperty(
            name='Length',
            description='Length of the tiles',
            unit='LENGTH', subtype='DISTANCE',
            precision=2,
            min=0.02,
            default=0.3,
            update=update
            )

    # grout
    add_grout : BoolProperty(
            name='Add Grout',
            description='Add grout',
            default=False,
            update=update
            )
    mortar_depth : FloatProperty(
            name='Depth',
            description='The depth of the mortar from the surface of the tile',
            unit='LENGTH', subtype='DISTANCE',
            precision=2,
            step=0.005,
            min=0,
            default=0.001,
            update=update
            )

    # regular tile
    random_offset : BoolProperty(
            name='Random Offset',
            description='Random amount of offset for each row of tiles',
            update=update, default=False
            )
    offset : FloatProperty(
            name='Offset',
            description='How much to offset each row of tiles',
            min=0, max=100, default=0,
            precision=2,
            update=update
            )
    offset_variance : FloatProperty(
            name='Variance',
            description='How much to vary the offset each row of tiles',
            min=0.001, max=100, default=50,
            precision=2,
            update=update
            )

    # bevel
    bevel : BoolProperty(
            name='Bevel',
            update=update,
            default=False,
            description='Bevel upper faces'
            )
    bevel_amount : FloatProperty(
            name='Bevel',
            description='Bevel amount',
            unit='LENGTH', subtype='DISTANCE',
            min=0.0001, default=0.001,
            precision=2, step=0.05,
            update=update
            )
    solidify : BoolProperty(
            name="Solidify",
            default=True,
            update=update
            )
    vary_materials : BoolProperty(
            name="Random Material",
            default=True,
            description="Vary Material indexes",
            update=update)
    matid : IntProperty(
            name="#variations",
            min=1,
            max=10,
            default=7,
            description="Material index maxi",
            update=update)
    auto_update : BoolProperty(
            options={'SKIP_SAVE'},
            default=True,
            update=update_manipulators
            )
    z : FloatProperty(
            name="dumb z",
            description="Dumb z for manipulator placeholder",
            default=0.01,
            options={'SKIP_SAVE'}
            )

    def get_generator(self):
        g = FloorGenerator(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 update_parts(self, o):

        for i in range(len(self.parts), self.n_parts, -1):
            self.parts.remove(i - 1)

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

        self.setup_manipulators()

        g = self.get_generator()

        return g

    @staticmethod
    def create_uv_seams(bm):
        handled = set()
        for edge in bm.edges:
            if edge.verts[0].co.z == 0 and edge.verts[1].co.z == 0:  # bottom
                # make sure both vertices on the edge haven't been handled, this forces one edge to not be made a seam
                # leaving the bottom face still attached
                if not (edge.verts[0].index in handled and edge.verts[1].index in handled):
                    edge.seam = True
                    handled.add(edge.verts[0].index)
                    handled.add(edge.verts[1].index)
            elif edge.verts[0].co.z != edge.verts[1].co.z:  # not horizontal, so they are vertical seams
                edge.seam = True

    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, context, 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)

        pt = wM.inverted() @ pts[0]

        # pretranslate
        o = self.find_in_selection(context, self.auto_update)
        o.matrix_world = wM @ Matrix.Translation(pt)
        self.from_points(pts)

    def from_points(self, pts):

        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 = True
        self.auto_update = True

    def update_path(self, context):
        user_def_path = context.scene.objects.get(self.user_defined_path.strip())
        if user_def_path is not None and user_def_path.type == 'CURVE':
            self.from_spline(
                context,
                user_def_path.matrix_world,
                self.user_defined_resolution,
                user_def_path.data.splines[0])

    def add_manipulator(self, name, pt1, pt2, pt3):
        m = self.manipulators.add()
        m.prop1_name = name
        m.set_pts([pt1, pt2, pt3])

    def update_manipulators(self):
        self.manipulators.clear()  # clear every time, add new ones
        self.add_manipulator("length", (0, 0, 0), (0, self.length, 0), (-0.4, 0, 0))
        self.add_manipulator("width", (0, 0, 0), (self.width, 0, 0), (0.4, 0, 0))

        z = self.thickness

        if self.pattern == "boards":
            self.add_manipulator("board_length", (0, 0, z), (0, self.board_length, z), (0.1, 0, z))
            self.add_manipulator("board_width", (0, 0, z), (self.board_width, 0, z), (-0.2, 0, z))
        elif self.pattern == "square_parquet":
            self.add_manipulator("short_board_length", (0, 0, z), (0, self.short_board_length, z), (-0.2, 0, z))
        elif self.pattern in ("herringbone", "herringbone_parquet"):
            dia = self.short_board_length * cos(radians(45))
            dia2 = self.board_width * cos(radians(45))
            self.add_manipulator("short_board_length", (0, 0, z), (dia, dia, z), (0, 0, z))
            self.add_manipulator("board_width", (dia, 0, z), (dia - dia2, dia2, z), (0, 0, z))
        else:
            tl = self.tile_length
            tw = self.tile_width

            if self.pattern in ("regular_tile", "hopscotch", "stepping_stone"):
                self.add_manipulator("tile_width", (0, tl, z), (tw, tl, z), (0, 0, z))
                self.add_manipulator("tile_length", (0, 0, z), (0, tl, z), (0, 0, z))
            elif self.pattern == "hexagon":
                self.add_manipulator("tile_width", (tw / 2 + self.spacing, 0, z), (tw * 1.5 + self.spacing, 0, z),
                                     (0, 0, 0))
            elif self.pattern == "windmill":
                self.add_manipulator("tile_width", (0, 0, z), (tw, 0, 0), (0, 0, z))
                self.add_manipulator("tile_length", (0, tl / 2 + self.spacing, z), (0, tl * 1.5 + self.spacing, z),
                                     (0, 0, z))

    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 update(self, context, manipulable_refresh=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)

        g.cut(context, o)
        g.floor(context, o, self)

        # 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

        self.manipulable_setup(context)
        self.manipulate_mode = True

        self._manipulable_invoke(context)

        return True


def update_hole(self, context):
    self.update(context, update_parent=True)


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


class archipack_floor_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_floor_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_floor_cutter(ArchipackCutter, ArchipackObject, Manipulable, PropertyGroup):
    parts : CollectionProperty(type=archipack_floor_cutter_segment)

    def update_points(self, context, o, pts, update_parent=False):
        """
            Create boundary from roof
        """
        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_floor.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


# ------------------------------------------------------------------
# Define panel class to show object parameters in ui panel (N)
# ------------------------------------------------------------------


class ARCHIPACK_PT_floor(Panel):
    bl_idname = "ARCHIPACK_PT_floor"
    bl_label = "Flooring"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = 'Archipack'

    @classmethod
    def poll(cls, context):
        # ensure your object panel only show when active object is the right one
        return archipack_floor.filter(context.active_object)

    def draw(self, context):
        o = context.active_object
        if not archipack_floor.filter(o):
            return
        layout = self.layout
        scene = context.scene
        # retrieve datablock of your object
        props = archipack_floor.datablock(o)
        # manipulate
        layout.operator("archipack.floor_manipulate", icon="VIEW_PAN")
        layout.separator()
        box = layout.box()
        row = box.row(align=True)

        # Presets operators
        row.operator("archipack.floor_preset_menu",
                     text=bpy.types.ARCHIPACK_OT_floor_preset_menu.bl_label)
        row.operator("archipack.floor_preset",
                      text="",
                      icon='ADD')
        row.operator("archipack.floor_preset",
                      text="",
                      icon='REMOVE').remove_active = True

        box = layout.box()
        box.operator('archipack.floor_cutter').parent = o.name

        box = layout.box()
        box.label(text="From curve")
        box.prop_search(props, "user_defined_path", scene, "objects", text="", icon='OUTLINER_OB_CURVE')
        if props.user_defined_path != "":
            box.prop(props, 'user_defined_resolution')

        box = layout.box()
        row = box.row()
        if props.parts_expand:
            row.prop(props, 'parts_expand', icon="TRIA_DOWN", text="Parts", emboss=False)
            box.prop(props, 'n_parts')
            # box.prop(prop, 'closed')
            for i, part in enumerate(props.parts):
                part.draw(context, layout, i)
        else:
            row.prop(props, 'parts_expand', icon="TRIA_RIGHT", text="Parts", emboss=False)
        layout.separator()
        box = layout.box()
        box.prop(props, 'pattern', text="")
        # thickness
        box.separator()
        box.prop(props, 'thickness')
        box.prop(props, 'vary_thickness', icon='RNDCURVE')
        if props.vary_thickness:
            box.prop(props, 'thickness_variance')
        box.separator()
        box.prop(props, 'solidify', icon='MOD_SOLIDIFY')
        box.separator()
        if props.pattern == 'boards':
            box.prop(props, 'board_length')
            box.prop(props, 'vary_length', icon='RNDCURVE')
            if props.vary_length:
                box.prop(props, 'length_variance')
                box.prop(props, 'max_boards')
            box.separator()

            # width
            box.prop(props, 'board_width')
            # vary width
            box.prop(props, 'vary_width', icon='RNDCURVE')
            if props.vary_width:
                box.prop(props, 'width_variance')
            box.separator()
            box.prop(props, 'length_spacing')
            box.prop(props, 'width_spacing')

        elif props.pattern in {'square_parquet', 'herringbone_parquet', 'herringbone'}:
            box.prop(props, 'short_board_length')

            if props.pattern != "square_parquet":
                box.prop(props, "board_width")
            box.prop(props, "spacing")

            if props.pattern == 'square_parquet':
                box.prop(props, 'boards_in_group')
        elif props.pattern in {'regular_tile', 'hopscotch', 'stepping_stone', 'hexagon', 'windmill'}:
            # width and length and mortar
            if props.pattern != "hexagon":
                box.prop(props, "tile_length")
            box.prop(props, "tile_width")
            box.prop(props, "spacing")

        if props.pattern in {"regular_tile", "boards"}:
            box.separator()
            box.prop(props, "random_offset", icon="RNDCURVE")
            if props.random_offset:
                box.prop(props, "offset_variance")
            else:
                box.prop(props, "offset")

        # grout
        box.separator()
        box.prop(props, 'add_grout', icon='MESH_GRID')
        if props.add_grout:
            box.prop(props, 'mortar_depth')

        # bevel
        box.separator()
        box.prop(props, 'bevel', icon='MOD_BEVEL')
        if props.bevel:
            box.prop(props, 'bevel_amount')

        box.separator()
        box.prop(props, "vary_materials", icon="MATERIAL")
        if props.vary_materials:
            box.prop(props, "matid")


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

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

    def draw(self, context):
        prop = archipack_floor_cutter.datablock(context.active_object)
        if prop is None:
            return
        layout = self.layout
        scene = context.scene
        box = layout.box()
        box.operator('archipack.floor_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_floor(ArchipackCreateTool, Operator):
    bl_idname = "archipack.floor"
    bl_label = "Floor"
    bl_description = "Floor"
    bl_category = 'Archipack'
    bl_options = {'REGISTER', 'UNDO'}

    def create(self, context):
        """
            expose only basic params in operator
            use object property for other params
        """
        m = bpy.data.meshes.new("Floor")
        o = bpy.data.objects.new("Floor", m)
        d = m.archipack_floor.add()
        # make manipulators selectable
        d.manipulable_selectable = True
        angle_90 = pi / 2
        x, y, = 4, 4
        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
        p = d.parts.add()
        p.a0 = angle_90
        p.length = x
        d.n_parts = 4
        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

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


class ARCHIPACK_OT_floor_from_curve(ArchipackCreateTool, Operator):
    bl_idname = "archipack.floor_from_curve"
    bl_label = "Floor curve"
    bl_description = "Create a floor from a curve"
    bl_category = 'Archipack'
    bl_options = {'REGISTER', 'UNDO'}

    @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.floor(auto_manipulate=self.auto_manipulate, filepath=self.filepath)
        o = context.active_object
        d = archipack_floor.datablock(o)
        d.user_defined_path = curve.name
        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_floor_from_wall(ArchipackCreateTool, Operator):
    bl_idname = "archipack.floor_from_wall"
    bl_label = "->Floor"
    bl_description = "Create a floor from a wall"
    bl_category = 'Archipack'
    bl_options = {'REGISTER', 'UNDO'}

    @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.floor(auto_manipulate=False, filepath=self.filepath)
        o = context.view_layer.objects.active
        d = archipack_floor.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
        o.matrix_world = wall.matrix_world.copy()
        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.floor_manipulate('INVOKE_DEFAULT')
            return {'FINISHED'}
        else:
            self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
            return {'CANCELLED'}


class ARCHIPACK_OT_floor_cutter(ArchipackCreateTool, Operator):
    bl_idname = "archipack.floor_cutter"
    bl_label = "Floor Cutter"
    bl_description = "Floor Cutter"
    bl_category = 'Archipack'
    bl_options = {'REGISTER', 'UNDO'}

    parent : StringProperty("")

    def create(self, context):
        m = bpy.data.meshes.new("Floor Cutter")
        o = bpy.data.objects.new("Floor Cutter", m)
        d = m.archipack_floor_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_floor.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_floor_preset_menu(PresetMenuOperator, Operator):
    bl_description = "Show Floor presets"
    bl_idname = "archipack.floor_preset_menu"
    bl_label = "Floor preset"
    preset_subdir = "archipack_floor"


class ARCHIPACK_OT_floor_preset(ArchipackPreset, Operator):
    """Add a Floor Preset"""
    bl_idname = "archipack.floor_preset"
    bl_label = "Add Floor preset"
    preset_menu = "ARCHIPACK_OT_floor_preset_menu"

    @property
    def blacklist(self):
        return ['manipulators', 'parts', 'n_parts', 'user_defined_path', 'user_defined_resolution']


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

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

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


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

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

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


def register():
    bpy.utils.register_class(archipack_floor_cutter_segment)
    bpy.utils.register_class(archipack_floor_cutter)
    Mesh.archipack_floor_cutter = CollectionProperty(type=archipack_floor_cutter)
    bpy.utils.register_class(ARCHIPACK_OT_floor_cutter)
    bpy.utils.register_class(ARCHIPACK_PT_floor_cutter)
    bpy.utils.register_class(ARCHIPACK_OT_floor_cutter_manipulate)

    bpy.utils.register_class(archipack_floor_part)
    bpy.utils.register_class(archipack_floor)
    Mesh.archipack_floor = CollectionProperty(type=archipack_floor)
    bpy.utils.register_class(ARCHIPACK_PT_floor)
    bpy.utils.register_class(ARCHIPACK_OT_floor)
    bpy.utils.register_class(ARCHIPACK_OT_floor_preset_menu)
    bpy.utils.register_class(ARCHIPACK_OT_floor_preset)
    bpy.utils.register_class(ARCHIPACK_OT_floor_manipulate)
    bpy.utils.register_class(ARCHIPACK_OT_floor_from_curve)
    bpy.utils.register_class(ARCHIPACK_OT_floor_from_wall)


def unregister():
    bpy.utils.unregister_class(archipack_floor_cutter_segment)
    bpy.utils.unregister_class(archipack_floor_cutter)
    del Mesh.archipack_floor_cutter
    bpy.utils.unregister_class(ARCHIPACK_OT_floor_cutter)
    bpy.utils.unregister_class(ARCHIPACK_PT_floor_cutter)
    bpy.utils.unregister_class(ARCHIPACK_OT_floor_cutter_manipulate)

    bpy.utils.unregister_class(archipack_floor_part)
    bpy.utils.unregister_class(archipack_floor)
    del Mesh.archipack_floor
    bpy.utils.unregister_class(ARCHIPACK_PT_floor)
    bpy.utils.unregister_class(ARCHIPACK_OT_floor)
    bpy.utils.unregister_class(ARCHIPACK_OT_floor_preset_menu)
    bpy.utils.unregister_class(ARCHIPACK_OT_floor_preset)
    bpy.utils.unregister_class(ARCHIPACK_OT_floor_manipulate)
    bpy.utils.unregister_class(ARCHIPACK_OT_floor_from_curve)
    bpy.utils.unregister_class(ARCHIPACK_OT_floor_from_wall)
