# SPDX-License-Identifier: MIT
# Copyright 2015 Sugiany

import bpy

from bpy_extras.object_utils import (
        AddObjectHelper,
        object_data_add,
        )

from bpy.props import (
        IntProperty,
        BoolProperty,
        BoolVectorProperty,
        FloatVectorProperty,
        FloatProperty,
        )

import mathutils
import copy


class MengerSponge(object):
    FACE_INDICES = [
        [3, 7, 4, 0],
        [5, 6, 2, 1],
        [1, 2, 3, 0],
        [7, 6, 5, 4],
        [4, 5, 1, 0],
        [2, 6, 7, 3],
    ]

    def __init__(self, level):
        self.__level = level
        self.__max_point_number = 3 ** level
        self.__vertices_map = {}
        self.__indices = []
        self.__face_visibility = {}
        self.__faces = []

        for x in range(3):
            for y in range(3):
                for z in range(3):
                    self.__face_visibility[(x, y, z)] = [
                        x == 0 or x == 2 and (y == 1 or z == 1),
                        x == 2 or x == 0 and (y == 1 or z == 1),
                        y == 0 or y == 2 and (x == 1 or z == 1),
                        y == 2 or y == 0 and (x == 1 or z == 1),
                        z == 0 or z == 2 and (y == 1 or x == 1),
                        z == 2 or z == 0 and (y == 1 or x == 1),
                    ]

    def create(self, width, height):
        m = self.__max_point_number
        points = [
            (0, 0, 0),
            (m, 0, 0),
            (m, 0, m),
            (0, 0, m),
            (0, m, 0),
            (m, m, 0),
            (m, m, m),
            (0, m, m),
            ]
        self.__make_sub_sponge(points, None, self.__level)
        vertices = self.__make_vertices(width, height)
        return vertices, self.__faces

    def __get_vindex(self, p):
        if p in self.__vertices_map:
            return self.__vertices_map[p]
        index = len(self.__vertices_map)
        self.__vertices_map[p] = index
        return index

    def __make_vertices(self, width, height):
        vertices = [None] * len(self.__vertices_map)
        w2 = width / 2
        h2 = height / 2
        w_step = width / self.__max_point_number
        h_step = height / self.__max_point_number
        for p, i in sorted(self.__vertices_map.items(), key=lambda x: x[1]):
            vertices[i] = mathutils.Vector([
                    p[0] * w_step - w2,
                    p[1] * w_step - w2,
                    p[2] * h_step - h2,
                    ])
        return vertices

    def __make_sub_sponge(self, cur_points, face_vis, depth):
        if depth <= 0:
            if not face_vis:
                face_vis = [True] * 6
            cur_point_indices = []
            for p in cur_points:
                cur_point_indices.append(self.__get_vindex(p))
            for i, vis in enumerate(face_vis):
                if vis:
                    f = []
                    for vi in self.FACE_INDICES[i]:
                        f.append(cur_point_indices[vi])
                    self.__faces.append(f)
            return

        base = cur_points[0]
        width = (cur_points[1][0] - base[0]) / 3
        local_vert_map = {}
        for z in range(4):
            for y in range(4):
                for x in range(4):
                    local_vert_map[(x, y, z)] = (
                        width * x + base[0],
                        width * y + base[1],
                        width * z + base[2],
                        )

        for x in range(3):
            for y in range(3):
                for z in range(3):
                    if [x, y, z].count(1) > 1:
                        continue
                    next_points = [
                        local_vert_map[(x, y, z)],
                        local_vert_map[(x + 1, y, z)],
                        local_vert_map[(x + 1, y, z + 1)],
                        local_vert_map[(x, y, z + 1)],
                        local_vert_map[(x, y + 1, z)],
                        local_vert_map[(x + 1, y + 1, z)],
                        local_vert_map[(x + 1, y + 1, z + 1)],
                        local_vert_map[(x, y + 1, z + 1)],
                    ]
                    visibility = copy.copy(self.__face_visibility[(x, y, z)])
                    if face_vis:
                        visibility[0] = visibility[0] and (face_vis[0] or x != 0)
                        visibility[1] = visibility[1] and (face_vis[1] or x != 2)
                        visibility[2] = visibility[2] and (face_vis[2] or y != 0)
                        visibility[3] = visibility[3] and (face_vis[3] or y != 2)
                        visibility[4] = visibility[4] and (face_vis[4] or z != 0)
                        visibility[5] = visibility[5] and (face_vis[5] or z != 2)
                    self.__make_sub_sponge(
                        next_points,
                        visibility,
                        depth - 1)


class AddMengerSponge(bpy.types.Operator, AddObjectHelper):
    bl_idname = "mesh.menger_sponge_add"
    bl_label = "Menger Sponge"
    bl_description = "Construct a menger sponge mesh"
    bl_options = {'REGISTER', 'UNDO'}

    level: IntProperty(
            name="Level",
            description="Sponge Level",
            min=0, max=4,
            default=1,
            )
    radius: FloatProperty(
            name="Width",
            description="Sponge Radius",
            min=0.01, max=100.0,
            default=1.0,
            )
    layers: BoolVectorProperty(
            name="Layers",
            size=20,
            subtype='LAYER',
            options={'HIDDEN', 'SKIP_SAVE'},
            )

    def execute(self, context):
        sponger = MengerSponge(self.level)
        vertices, faces = sponger.create(self.radius * 2, self.radius * 2)
        del sponger

        mesh = bpy.data.meshes.new(name='Sponge')
        mesh.from_pydata(vertices, [], faces)
        uvs = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)]
        mesh.uv_layers.new()
        for i, uvloop in enumerate(mesh.uv_layers.active.data):
            uvloop.uv = uvs[i % 4]

        object_data_add(context, mesh, operator=self)

        return {'FINISHED'}