# GPL # Author: Alain Ducharme (phymec)

import bpy
from bpy_extras import object_utils
from itertools import permutations
from math import (
        copysign, pi,
        sqrt,
        )
from bpy.types import Operator
from bpy.props import (
        BoolProperty,
        EnumProperty,
        FloatProperty,
        FloatVectorProperty,
        IntProperty,
        StringProperty,
        )


def round_cube(radius=1.0, arcdiv=4, lindiv=0., size=(0., 0., 0.),
               div_type='CORNERS', odd_axis_align=False, info_only=False):
    # subdiv bitmasks
    CORNERS, EDGES, ALL = 0, 1, 2
    try:
        subdiv = ('CORNERS', 'EDGES', 'ALL').index(div_type)
    except ValueError:
        subdiv = CORNERS  # fallback

    radius = max(radius, 0.)
    if not radius:
        # No sphere
        arcdiv = 1
        odd_axis_align = False

    if arcdiv <= 0:
        arcdiv = max(round(pi * radius * lindiv * 0.5), 1)
    arcdiv = max(round(arcdiv), 1)
    if lindiv <= 0. and radius:
        lindiv = 1. / (pi / (arcdiv * 2.) * radius)
    lindiv = max(lindiv, 0.)
    if not lindiv:
        subdiv = CORNERS

    odd = arcdiv % 2  # even = arcdiv % 2 ^ 1
    step_size = 2. / arcdiv

    odd_aligned = 0
    vi = -1.
    steps = arcdiv + 1
    if odd_axis_align and odd:
        odd_aligned = 1
        vi += 0.5 * step_size
        steps = arcdiv
    axis_aligned = not odd or odd_aligned

    if arcdiv == 1 and not odd_aligned and subdiv == EDGES:
        subdiv = CORNERS

    half_chord = 0.  # ~ spherical cap base radius
    sagitta = 0.     # ~ spherical cap height
    if not axis_aligned:
        half_chord = sqrt(3.) * radius / (3. * arcdiv)
        id2 = 1. / (arcdiv * arcdiv)
        sagitta = radius - radius * sqrt(id2 * id2 / 3. - id2 + 1.)

    # Extrusion per axis
    exyz = [0. if s < 2. * (radius - sagitta) else (s - 2. * (radius - sagitta)) * 0.5 for s in size]
    ex, ey, ez = exyz

    dxyz = [0, 0, 0]       # extrusion divisions per axis
    dssxyz = [0., 0., 0.]  # extrusion division step sizes per axis

    for i in range(3):
        sc = 2. * (exyz[i] + half_chord)
        dxyz[i] = round(sc * lindiv) if subdiv else 0
        if dxyz[i]:
            dssxyz[i] = sc / dxyz[i]
            dxyz[i] -= 1
        else:
            dssxyz[i] = sc

    if info_only:
        ec = sum(1 for n in exyz if n)
        if subdiv:
            fxyz = [d + (e and axis_aligned) for d, e in zip(dxyz, exyz)]
            dvc = arcdiv * 4 * sum(fxyz)
            if subdiv == ALL:
                dvc += sum(p1 * p2 for p1, p2 in permutations(fxyz, 2))
            elif subdiv == EDGES and axis_aligned:
                #      (0, 0, 2, 4) * sum(dxyz) + (0, 0, 2, 6)
                dvc += ec * ec // 2 * sum(dxyz) + ec * (ec - 1)
        else:
            dvc = (arcdiv * 4) * ec + ec * (ec - 1) if axis_aligned else 0
        vert_count = int(6 * arcdiv * arcdiv + (0 if odd_aligned else 2) + dvc)
        if not radius and not max(size) > 0:
            vert_count = 1
        return arcdiv, lindiv, vert_count

    if not radius and not max(size) > 0:
        # Single vertex
        return [(0, 0, 0)], []

    # uv lookup table
    uvlt = []
    v = vi
    for j in range(1, steps + 1):
        v2 = v * v
        uvlt.append((v, v2, radius * sqrt(18. - 6. * v2) / 6.))
        v = vi + j * step_size  # v += step_size # instead of accumulating errors
        # clear fp errors / signs at axis
        if abs(v) < 1e-10:
            v = 0.0

    # Sides built left to right bottom up
    #         xp yp zp  xd  yd  zd
    sides = ((0, 2, 1, (-1, 1, 1)),    # Y+ Front
             (1, 2, 0, (-1, -1, 1)),   # X- Left
             (0, 2, 1, (1, -1, 1)),    # Y- Back
             (1, 2, 0, (1, 1, 1)),     # X+ Right
             (0, 1, 2, (-1, 1, -1)),   # Z- Bottom
             (0, 1, 2, (-1, -1, 1)))   # Z+ Top

    # side vertex index table (for sphere)
    svit = [[[] for i in range(steps)] for i in range(6)]
    # Extend svit rows for extrusion
    yer = zer = 0
    if ey:
        yer = axis_aligned + (dxyz[1] if subdiv else 0)
        svit[4].extend([[] for i in range(yer)])
        svit[5].extend([[] for i in range(yer)])
    if ez:
        zer = axis_aligned + (dxyz[2] if subdiv else 0)
        for side in range(4):
            svit[side].extend([[] for i in range(zer)])
    # Extend svit rows for odd_aligned
    if odd_aligned:
        for side in range(4):
            svit[side].append([])

    hemi = steps // 2

    # Create vertices and svit without dups
    vert = [0., 0., 0.]
    verts = []

    if arcdiv == 1 and not odd_aligned and subdiv == ALL:
        # Special case: Grid Cuboid
        for side, (xp, yp, zp, dir) in enumerate(sides):
            svitc = svit[side]
            rows = len(svitc)
            if rows < dxyz[yp] + 2:
                svitc.extend([[] for i in range(dxyz[yp] + 2 - rows)])
            vert[zp] = (half_chord + exyz[zp]) * dir[zp]
            for j in range(dxyz[yp] + 2):
                vert[yp] = (j * dssxyz[yp] - half_chord - exyz[yp]) * dir[yp]
                for i in range(dxyz[xp] + 2):
                    vert[xp] = (i * dssxyz[xp] - half_chord - exyz[xp]) * dir[xp]
                    if (side == 5) or ((i < dxyz[xp] + 1 and j < dxyz[yp] + 1) and (side < 4 or (i and j))):
                        svitc[j].append(len(verts))
                        verts.append(tuple(vert))
    else:
        for side, (xp, yp, zp, dir) in enumerate(sides):
            svitc = svit[side]
            exr = exyz[xp]
            eyr = exyz[yp]
            ri = 0  # row index
            rij = zer if side < 4 else yer

            if side == 5:
                span = range(steps)
            elif side < 4 or odd_aligned:
                span = range(arcdiv)
            else:
                span = range(1, arcdiv)
                ri = 1

            for j in span:  # rows
                v, v2, mv2 = uvlt[j]
                tv2mh = 1. / 3. * v2 - 0.5
                hv2 = 0.5 * v2

                if j == hemi and rij:
                    # Jump over non-edge row indices
                    ri += rij

                for i in span:  # columns
                    u, u2, mu2 = uvlt[i]
                    vert[xp] = u * mv2
                    vert[yp] = v * mu2
                    vert[zp] = radius * sqrt(u2 * tv2mh - hv2 + 1.)

                    vert[0] = (vert[0] + copysign(ex, vert[0])) * dir[0]
                    vert[1] = (vert[1] + copysign(ey, vert[1])) * dir[1]
                    vert[2] = (vert[2] + copysign(ez, vert[2])) * dir[2]
                    rv = tuple(vert)

                    if exr and i == hemi:
                        rx = vert[xp]  # save rotated x
                        vert[xp] = rxi = (-exr - half_chord) * dir[xp]
                        if axis_aligned:
                            svitc[ri].append(len(verts))
                            verts.append(tuple(vert))
                        if subdiv:
                            offsetx = dssxyz[xp] * dir[xp]
                            for k in range(dxyz[xp]):
                                vert[xp] += offsetx
                                svitc[ri].append(len(verts))
                                verts.append(tuple(vert))
                        if eyr and j == hemi and axis_aligned:
                            vert[xp] = rxi
                            vert[yp] = -eyr * dir[yp]
                            svitc[hemi].append(len(verts))
                            verts.append(tuple(vert))
                            if subdiv:
                                offsety = dssxyz[yp] * dir[yp]
                                ry = vert[yp]
                                for k in range(dxyz[yp]):
                                    vert[yp] += offsety
                                    svitc[hemi + axis_aligned + k].append(len(verts))
                                    verts.append(tuple(vert))
                                vert[yp] = ry
                                for k in range(dxyz[xp]):
                                    vert[xp] += offsetx
                                    svitc[hemi].append(len(verts))
                                    verts.append(tuple(vert))
                                    if subdiv & ALL:
                                        for l in range(dxyz[yp]):
                                            vert[yp] += offsety
                                            svitc[hemi + axis_aligned + l].append(len(verts))
                                            verts.append(tuple(vert))
                                        vert[yp] = ry
                        vert[xp] = rx  # restore

                    if eyr and j == hemi:
                        vert[yp] = (-eyr - half_chord) * dir[yp]
                        if axis_aligned:
                            svitc[hemi].append(len(verts))
                            verts.append(tuple(vert))
                        if subdiv:
                            offsety = dssxyz[yp] * dir[yp]
                            for k in range(dxyz[yp]):
                                vert[yp] += offsety
                                if exr and i == hemi and not axis_aligned and subdiv & ALL:
                                    vert[xp] = rxi
                                    for l in range(dxyz[xp]):
                                        vert[xp] += offsetx
                                        svitc[hemi + k].append(len(verts))
                                        verts.append(tuple(vert))
                                    vert[xp] = rx
                                svitc[hemi + axis_aligned + k].append(len(verts))
                                verts.append(tuple(vert))

                    svitc[ri].append(len(verts))
                    verts.append(rv)
                ri += 1

    # Complete svit edges (shared vertices)
    # Sides' right edge
    for side, rows in enumerate(svit[:4]):
        for j, row in enumerate(rows[:-1]):
            svit[3 if not side else side - 1][j].append(row[0])
    # Sides' top edge
    svit[0][-1].extend(svit[5][0])
    svit[2][-1].extend(svit[5][-1][::-1])
    for row in svit[5]:
        svit[3][-1].insert(0, row[0])
        svit[1][-1].append(row[-1])
    if odd_aligned:
        for side in svit[:4]:
            side[-1].append(-1)
    # Bottom edges
    if odd_aligned:
        svit[4].insert(0, [-1] + svit[2][0][-2::-1] + [-1])
        for i, col in enumerate(svit[3][0][:-1]):
            svit[4][i + 1].insert(0, col)
            svit[4][i + 1].append(svit[1][0][-i - 2])
        svit[4].append([-1] + svit[0][0][:-1] + [-1])
    else:
        svit[4][0].extend(svit[2][0][::-1])
        for i, col in enumerate(svit[3][0][1:-1]):
            svit[4][i + 1].insert(0, col)
            svit[4][i + 1].append(svit[1][0][-i - 2])
        svit[4][-1].extend(svit[0][0])

    # Build faces
    faces = []
    if not axis_aligned:
        hemi -= 1
    for side, rows in enumerate(svit):
        xp, yp = sides[side][:2]
        oa4 = odd_aligned and side == 4
        if oa4:  # special case
            hemi += 1
        for j, row in enumerate(rows[:-1]):
            tri = odd_aligned and (oa4 and not j or rows[j + 1][-1] < 0)
            for i, vi in enumerate(row[:-1]):
                # odd_aligned triangle corners
                if vi < 0:
                    if not j and not i:
                        faces.append((row[i + 1], rows[j + 1][i + 1], rows[j + 1][i]))
                elif oa4 and not i and j == len(rows) - 2:
                    faces.append((vi, row[i + 1], rows[j + 1][i + 1]))
                elif tri and i == len(row) - 2:
                    if j:
                        faces.append((vi, row[i + 1], rows[j + 1][i]))
                    else:
                        if oa4 or arcdiv > 1:
                            faces.append((vi, rows[j + 1][i + 1], rows[j + 1][i]))
                        else:
                            faces.append((vi, row[i + 1], rows[j + 1][i]))
                # subdiv = EDGES (not ALL)
                elif subdiv and len(rows[j + 1]) < len(row) and (i >= hemi):
                    if (i == hemi):
                        faces.append((vi, row[i + 1 + dxyz[xp]], rows[j + 1 + dxyz[yp]][i + 1 + dxyz[xp]],
                                     rows[j + 1 + dxyz[yp]][i]))
                    elif i > hemi + dxyz[xp]:
                        faces.append((vi, row[i + 1], rows[j + 1][i + 1 - dxyz[xp]], rows[j + 1][i - dxyz[xp]]))
                elif subdiv and len(rows[j + 1]) > len(row) and (i >= hemi):
                    if (i > hemi):
                        faces.append((vi, row[i + 1], rows[j + 1][i + 1 + dxyz[xp]], rows[j + 1][i + dxyz[xp]]))
                elif subdiv and len(row) < len(rows[0]) and i == hemi:
                    pass
                else:
                    # Most faces...
                    faces.append((vi, row[i + 1], rows[j + 1][i + 1], rows[j + 1][i]))
        if oa4:
            hemi -= 1

    return verts, faces


class AddRoundCube(Operator, object_utils.AddObjectHelper):
    bl_idname = "mesh.primitive_round_cube_add"
    bl_label = "Add Round Cube"
    bl_description = ("Create mesh primitives: Quadspheres, "
                      "Capsules, Rounded Cuboids, 3D Grids etc")
    bl_options = {"REGISTER", "UNDO", "PRESET"}

    sanity_check_verts = 200000
    vert_count = 0

    Roundcube : BoolProperty(name = "Roundcube",
                default = True,
                description = "Roundcube")
    change : BoolProperty(name = "Change",
                default = False,
                description = "change Roundcube")

    radius: FloatProperty(
            name="Radius",
            description="Radius of vertices for sphere, capsule or cuboid bevel",
            default=0.2, min=0.0, soft_min=0.01, step=10
            )
    size: FloatVectorProperty(
            name="Size",
            description="Size",
            subtype='XYZ',
            default=(2.0, 2.0, 2.0),
            )
    arc_div: IntProperty(
            name="Arc Divisions",
            description="Arc curve divisions, per quadrant, 0=derive from Linear",
            default=4, min=1
            )
    lin_div: FloatProperty(
            name="Linear Divisions",
            description="Linear unit divisions (Edges/Faces), 0=derive from Arc",
            default=0.0, min=0.0, step=100, precision=1
            )
    div_type: EnumProperty(
            name='Type',
            description='Division type',
            items=(
                ('CORNERS', 'Corners', 'Sphere / Corners'),
                ('EDGES', 'Edges', 'Sphere / Corners and extruded edges (size)'),
                ('ALL', 'All', 'Sphere / Corners, extruded edges and faces (size)')),
            default='CORNERS',
            )
    odd_axis_align: BoolProperty(
            name='Odd Axis Align',
            description='Align odd arc divisions with axes (Note: triangle corners!)',
            )
    no_limit: BoolProperty(
            name='No Limit',
            description='Do not limit to ' + str(sanity_check_verts) + ' vertices (sanity check)',
            options={'HIDDEN'}
            )

    def execute(self, context):
        # turn off 'Enter Edit Mode'
        use_enter_edit_mode = bpy.context.preferences.edit.use_enter_edit_mode
        bpy.context.preferences.edit.use_enter_edit_mode = False

        if self.arc_div <= 0 and self.lin_div <= 0:
            self.report({'ERROR'},
                        "Either Arc Divisions or Linear Divisions must be greater than zero")
            return {'CANCELLED'}

        if not self.no_limit:
            if self.vert_count > self.sanity_check_verts:
                self.report({'ERROR'}, 'More than ' + str(self.sanity_check_verts) +
                            ' vertices!  Check "No Limit" to proceed')
                return {'CANCELLED'}

        if bpy.context.mode == "OBJECT":
            if context.selected_objects != [] and context.active_object and \
                (context.active_object.data is not None) and ('Roundcube' in context.active_object.data.keys()) and \
                (self.change == True):
                obj = context.active_object
                oldmesh = obj.data
                oldmeshname = obj.data.name
                verts, faces = round_cube(self.radius, self.arc_div, self.lin_div,
                                  self.size, self.div_type, self.odd_axis_align)
                mesh = bpy.data.meshes.new('Roundcube')
                mesh.from_pydata(verts, [], faces)
                obj.data = mesh
                for material in oldmesh.materials:
                    obj.data.materials.append(material)
                bpy.data.meshes.remove(oldmesh)
                obj.data.name = oldmeshname
            else:
                verts, faces = round_cube(self.radius, self.arc_div, self.lin_div,
                                  self.size, self.div_type, self.odd_axis_align)
                mesh = bpy.data.meshes.new('Roundcube')
                mesh.from_pydata(verts, [], faces)
                obj = object_utils.object_data_add(context, mesh, operator=self)

            obj.data["Roundcube"] = True
            obj.data["change"] = False
            for prm in RoundCubeParameters():
                obj.data[prm] = getattr(self, prm)

        if bpy.context.mode == "EDIT_MESH":
            active_object = context.active_object
            name_active_object = active_object.name
            bpy.ops.object.mode_set(mode='OBJECT')
            verts, faces = round_cube(self.radius, self.arc_div, self.lin_div,
                                  self.size, self.div_type, self.odd_axis_align)
            mesh = bpy.data.meshes.new('Roundcube')
            mesh.from_pydata(verts, [], faces)
            obj = object_utils.object_data_add(context, mesh, operator=self)
            obj.select_set(True)
            active_object.select_set(True)
            bpy.context.view_layer.objects.active = active_object
            bpy.ops.object.join()
            context.active_object.name = name_active_object
            bpy.ops.object.mode_set(mode='EDIT')

        if use_enter_edit_mode:
            bpy.ops.object.mode_set(mode = 'EDIT')

        # restore pre operator state
        bpy.context.preferences.edit.use_enter_edit_mode = use_enter_edit_mode

        return {'FINISHED'}

    def check(self, context):
        self.arcdiv, self.lindiv, self.vert_count = round_cube(
                                                        self.radius, self.arc_div, self.lin_div,
                                                        self.size, self.div_type, self.odd_axis_align,
                                                        True
                                                        )
        return True

    def invoke(self, context, event):
        self.check(context)
        return self.execute(context)

    def draw(self, context):
        self.check(context)
        layout = self.layout

        layout.prop(self, 'radius')
        layout.column().prop(self, 'size', expand=True)

        box = layout.box()
        row = box.row()
        row.alignment = 'CENTER'
        row.scale_y = 0.1
        row.label(text='Divisions')
        row = box.row()
        col = row.column()
        col.alignment = 'RIGHT'
        col.label(text='Arc:')
        col.prop(self, 'arc_div', text='')
        col.label(text='[ {} ]'.format(self.arcdiv))
        col = row.column()
        col.alignment = 'RIGHT'
        col.label(text='Linear:')
        col.prop(self, 'lin_div', text='')
        col.label(text='[ {:.3g} ]'.format(self.lindiv))
        box.row().prop(self, 'div_type')
        row = box.row()
        row.active = self.arcdiv % 2
        row.prop(self, 'odd_axis_align')

        row = layout.row()
        row.alert = self.vert_count > self.sanity_check_verts
        row.prop(self, 'no_limit', text='No limit ({})'.format(self.vert_count))

        if self.change == False:
            col = layout.column(align=True)
            col.prop(self, 'align', expand=True)
            col = layout.column(align=True)
            col.prop(self, 'location', expand=True)
            col = layout.column(align=True)
            col.prop(self, 'rotation', expand=True)

def RoundCubeParameters():
    RoundCubeParameters = [
        "radius",
        "size",
        "arc_div",
        "lin_div",
        "div_type",
        "odd_axis_align",
        "no_limit",
        ]
    return RoundCubeParameters
