# Purpose: create sierpinski pyramid geometry
# Copyright (c) 2016-2022 Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Iterable, Sequence, Iterator, Optional
import math
from ezdxf.math import Vec3, UVec, Matrix44, UCS
from ezdxf.render.mesh import MeshVertexMerger, MeshTransformer

if TYPE_CHECKING:
    from ezdxf.eztypes import GenericLayoutType

HEIGHT4 = 1.0 / math.sqrt(2.0)  # pyramid4 height (* length)
HEIGHT3 = math.sqrt(6.0) / 3.0  # pyramid3 height (* length)

DY1_FACTOR = math.tan(math.pi / 6.0) / 2.0  # inner circle radius
DY2_FACTOR = 0.5 / math.cos(math.pi / 6.0)  # outer circle radius


class SierpinskyPyramid:
    """
    Args:
        location: location of base center as (x, y, z) tuple
        length: side length
        level: subdivide level
        sides: sides of base geometry

    """

    def __init__(
        self,
        location: UVec = (0.0, 0.0, 0.0),
        length: float = 1.0,
        level: int = 1,
        sides: int = 4,
    ):
        self.sides = sides
        self.pyramid_definitions = sierpinsky_pyramid(
            location=location, length=length, level=level, sides=sides
        )

    def vertices(self) -> Iterator[UVec]:
        """Yields the pyramid vertices as list of (x, y, z) tuples."""
        for location, length in self.pyramid_definitions:
            yield self._calc_vertices(location, length)

    __iter__ = vertices

    def _calc_vertices(
        self, location: UVec, length: float
    ) -> list[UVec]:
        """
        Calculates the pyramid vertices.

        Args:
            location: location of the pyramid as center point of the base
            length: pyramid side length

        Returns: list of (x, y, z) tuples

        """
        len2 = length / 2.0
        x, y, z = location
        if self.sides == 4:
            return [
                Vec3(x - len2, y - len2, z),
                Vec3(x + len2, y - len2, z),
                Vec3(x + len2, y + len2, z),
                Vec3(x - len2, y + len2, z),
                Vec3(x, y, z + length * HEIGHT4),
            ]
        elif self.sides == 3:
            dy1 = length * DY1_FACTOR
            dy2 = length * DY2_FACTOR
            return [
                Vec3(x - len2, y - dy1, z),
                Vec3(x + len2, y - dy1, z),
                Vec3(x, y + dy2, z),
                Vec3(x, y, z + length * HEIGHT3),
            ]
        else:
            raise ValueError("sides has to be 3 or 4.")

    def faces(self) -> list[Sequence[int]]:
        """Returns list of pyramid faces. All pyramid vertices have the same
        order, so one faces list fits them all.

        """
        if self.sides == 4:
            return [(3, 2, 1, 0), (0, 1, 4), (1, 2, 4), (2, 3, 4), (3, 0, 4)]
        elif self.sides == 3:
            return [(2, 1, 0), (0, 1, 3), (1, 2, 3), (2, 0, 3)]
        else:
            raise ValueError("sides has to be 3 or 4.")

    def render(
        self,
        layout: GenericLayoutType,
        merge: bool = False,
        dxfattribs=None,
        matrix: Optional[Matrix44] = None,
        ucs: Optional[UCS] = None,
    ) -> None:
        """Renders the sierpinsky pyramid into layout, set `merge` to ``True``
        for rendering the whole sierpinsky pyramid into one MESH entity, set
        `merge` to ``False`` for individual pyramids as MESH entities.

        Args:
            layout: DXF target layout
            merge: ``True`` for one MESH entity, ``False`` for individual MESH
                entities per pyramid
            dxfattribs: DXF attributes for the MESH entities
            matrix: apply transformation matrix at rendering
            ucs: apply UCS at rendering

        """
        if merge:
            mesh = self.mesh()
            mesh.render_mesh(
                layout, dxfattribs=dxfattribs, matrix=matrix, ucs=ucs
            )
        else:
            for pyramid in self.pyramids():
                pyramid.render_mesh(layout, dxfattribs, matrix=matrix, ucs=ucs)

    def pyramids(self) -> Iterable[MeshTransformer]:
        """Yields all pyramids of the sierpinsky pyramid as individual
        :class:`MeshTransformer` objects.
        """
        faces = self.faces()
        for vertices in self:
            mesh = MeshTransformer()
            mesh.add_mesh(vertices=vertices, faces=faces)  # type: ignore
            yield mesh

    def mesh(self) -> MeshTransformer:
        """Returns geometry as one :class:`MeshTransformer` object."""
        faces = self.faces()
        mesh = MeshVertexMerger()
        for vertices in self:
            mesh.add_mesh(vertices=vertices, faces=faces)  # type: ignore
        return MeshTransformer.from_builder(mesh)


def sierpinsky_pyramid(
    location=(0.0, 0.0, 0.0),
    length: float = 1.0,
    level: int = 1,
    sides: int = 4,
) -> list[tuple[Vec3, float]]:
    """Build a Sierpinski pyramid.

    Args:
        location: base center point of the pyramid
        length: base length of the pyramid
        level: recursive building levels, has to 1 or bigger
        sides: 3 or 4 sided pyramids supported

    Returns: list of pyramid vertices

    """
    location = Vec3(location)
    level = int(level)
    if level < 1:
        raise ValueError("level has to be 1 or bigger.")
    pyramids = _sierpinsky_pyramid(location, length, sides)
    for _ in range(level - 1):
        next_level_pyramids = []
        for location, length in pyramids:
            next_level_pyramids.extend(
                _sierpinsky_pyramid(location, length, sides)
            )
        pyramids = next_level_pyramids
    return pyramids


def _sierpinsky_pyramid(
    location: Vec3, length: float = 1.0, sides: int = 4
) -> list[tuple[Vec3, float]]:
    if sides == 3:
        return sierpinsky_pyramid_3(location, length)
    elif sides == 4:
        return sierpinsky_pyramid_4(location, length)
    else:
        raise ValueError("sides has to be 3 or 4.")


def sierpinsky_pyramid_4(
    location: Vec3, length: float = 1.0
) -> list[tuple[Vec3, float]]:
    """Build a 4-sided Sierpinski pyramid. Pyramid height = length of the base
    square!

    Args:
        location: base center point of the pyramid
        length: base length of the pyramid

    Returns: list of (location, length) tuples, representing the sierpinski pyramid

    """
    len2 = length / 2
    len4 = length / 4
    x, y, z = location
    return [
        (Vec3(x - len4, y - len4, z), len2),
        (Vec3(x + len4, y - len4, z), len2),
        (Vec3(x - len4, y + len4, z), len2),
        (Vec3(x + len4, y + len4, z), len2),
        (Vec3(x, y, z + len2 * HEIGHT4), len2),
    ]


def sierpinsky_pyramid_3(
    location: Vec3, length: float = 1.0
) -> list[tuple[Vec3, float]]:
    """Build a 3-sided Sierpinski pyramid (tetraeder).

    Args:
        location: base center point of the pyramid
        length: base length of the pyramid

    Returns: list of (location, length) tuples, representing the sierpinski pyramid

    """
    dy1 = length * DY1_FACTOR * 0.5
    dy2 = length * DY2_FACTOR * 0.5
    len2 = length / 2
    len4 = length / 4
    x, y, z = location
    return [
        (Vec3(x - len4, y - dy1, z), len2),
        (Vec3(x + len4, y - dy1, z), len2),
        (Vec3(x, y + dy2, z), len2),
        (Vec3(x, y, z + len2 * HEIGHT3), len2),
    ]
