# Portable design geometry and deterministic voxelization shared by extension components.
from dataclasses import dataclass
import math
from hearth.space import Box

CARDINAL = ((1, 0), (-1, 0), (0, 1), (0, -1))


def lattice(value):
    """Round consistently across native Python and browser Python."""
    return math.floor(value + 0.5)


@dataclass(frozen=True)
class Polyline:
    points: tuple[tuple[float, float], ...]

    def __post_init__(self):
        object.__setattr__(self, 'points', tuple(tuple(p) for p in self.points))
        if len(self.points) < 2 or any(len(p) != 2 or not all(math.isfinite(v) for v in p) for p in self.points):
            raise ValueError('A finite polyline needs at least two two-dimensional points')
        if any(a == b for a, b in zip(self.points, self.points[1:])):
            raise ValueError('Repeated consecutive control points')

    @classmethod
    def bezier(cls, controls, segments=32):
        if len(controls) != 4 or not 4 <= segments <= 256:
            raise ValueError('Cubic curves require four controls and 4..256 samples')
        points = []
        for i in range(segments + 1):
            t = i / segments
            weights = ((1 - t)**3, 3 * t * (1 - t)**2, 3 * t * t * (1 - t), t**3)
            points.append(tuple(sum(w * p[k] for w, p in zip(weights, controls)) for k in (0, 1)))
        return cls(tuple(points))

    @property
    def length(self):
        return sum(math.dist(a, b) for a, b in zip(self.points, self.points[1:]))

    def nearest(self, point):
        """Return distance, arc length, tangent and projection with stable tie breaks."""
        best, walked = None, 0.0
        for index, (a, b) in enumerate(zip(self.points, self.points[1:])):
            dx, dz = b[0] - a[0], b[1] - a[1]
            length = math.hypot(dx, dz)
            t = max(0, min(1, ((point[0] - a[0]) * dx + (point[1] - a[1]) * dz) / length**2))
            q = (a[0] + t * dx, a[1] + t * dz)
            item = (math.dist(point, q), walked + t * length, index, (dx / length, dz / length), q)
            if best is None or item[:3] < best[:3]:
                best = item
            walked += length
        return best[0], best[1], best[3], best[4]

    def cells(self):
        """Rasterize a connected cardinal chain, retaining explicit diagonal turns."""
        result = []
        for a, b in zip(self.points, self.points[1:]):
            count = max(1, math.ceil(math.dist(a, b) * 4))
            for i in range(count + 1):
                target = tuple(lattice(a[k] + (b[k] - a[k]) * i / count) for k in (0, 1))
                if not result:
                    result.append(target)
                while result[-1] != target:
                    x, z = result[-1]
                    tx, tz = target
                    candidates = []
                    if x != tx:
                        candidates.append((x + (1 if tx > x else -1), z))
                    if z != tz:
                        candidates.append((x, z + (1 if tz > z else -1)))
                    result.append(min(candidates, key=lambda p: (self.nearest(p)[0], p)))
        return tuple(result)

    def ribbon(self, radius, caps=False):
        xmin = math.floor(min(p[0] for p in self.points) - radius)
        xmax = math.ceil(max(p[0] for p in self.points) + radius)
        zmin = math.floor(min(p[1] for p in self.points) - radius)
        zmax = math.ceil(max(p[1] for p in self.points) + radius)
        a, b = self.points[0], self.points[-1]
        start = (self.points[1][0] - a[0], self.points[1][1] - a[1])
        end = (b[0] - self.points[-2][0], b[1] - self.points[-2][1])
        result = []
        for x in range(xmin, xmax + 1):
            for z in range(zmin, zmax + 1):
                distance, station, _, _ = self.nearest((x, z))
                if not caps and ((station <= 1e-9 and (x - a[0]) * start[0] + (z - a[1]) * start[1] < 0) or (station >= self.length - 1e-9 and (x - b[0]) * end[0] + (z - b[1]) * end[1] > 0)):
                    continue
                if distance <= radius + 0.05:
                    result.append((x, z))
        return tuple(result)


@dataclass(frozen=True)
class Polygon:
    vertices: tuple[tuple[int, int], ...]

    def __post_init__(self):
        object.__setattr__(self, 'vertices', tuple(tuple(p) for p in self.vertices))
        if len(self.vertices) < 3:
            raise ValueError('At least three polygon vertices required')

    @classmethod
    def chamfered(cls, width, depth, corner=0):
        if not 9 <= width <= 27 or not 9 <= depth <= 27 or not 0 <= corner <= min(width, depth) // 3:
            raise ValueError('Footprint domain: dimensions 9..27, corner 0..one third')
        x, z, c = width - 1, depth - 1, corner
        return cls(((0, 0), (x, 0), (x, z), (0, z)) if not c else ((c, 0), (x - c, 0), (x, c), (x, z - c), (x - c, z), (c, z), (0, z - c), (0, c)))

    def cells(self):
        result = []
        for x in range(min(p[0] for p in self.vertices), max(p[0] for p in self.vertices) + 1):
            for z in range(min(p[1] for p in self.vertices), max(p[1] for p in self.vertices) + 1):
                inside = False
                edge = False
                for a, b in zip(self.vertices, self.vertices[1:] + self.vertices[:1]):
                    cross = (x - a[0]) * (b[1] - a[1]) - (z - a[1]) * (b[0] - a[0])
                    if cross == 0 and min(a[0], b[0]) <= x <= max(a[0], b[0]) and min(a[1], b[1]) <= z <= max(a[1], b[1]):
                        edge = True
                    if (a[1] > z) != (b[1] > z) and x < (b[0] - a[0]) * (z - a[1]) / (b[1] - a[1]) + a[0]:
                        inside = not inside
                if edge or inside:
                    result.append((x, z))
        return tuple(result)


def perimeter(cells, diagonal=False):
    points = set(cells)
    steps = tuple((x, z) for x in (-1, 0, 1) for z in (-1, 0, 1) if x or z) if diagonal else CARDINAL
    return tuple(sorted(p for p in points if any((p[0] + dx, p[1] + dz) not in points for dx, dz in steps)))


def bounds2(points, low, high):
    return Box((min(p[0] for p in points), low, min(p[1] for p in points)), (max(p[0] for p in points), high, max(p[1] for p in points)))
