# Integer geometry and explicit local-to-world transforms.
from dataclasses import dataclass
from itertools import product
from typing import Iterable

Pos = tuple[int, int, int]


@dataclass(frozen=True)
class Box:
    lo: Pos
    hi: Pos

    def __post_init__(self):
        object.__setattr__(self, 'lo', tuple(self.lo))
        object.__setattr__(self, 'hi', tuple(self.hi))
        if any(not isinstance(v, int) for v in (*self.lo, *self.hi)):
            raise TypeError('Box coordinates must be integers')
        if any(a > b for a, b in zip(self.lo, self.hi)):
            raise ValueError('Inverted box')

    def contains(self, p: Pos) -> bool:
        return all(a <= v <= b for a, v, b in zip(self.lo, p, self.hi))

    def cells(self) -> Iterable[Pos]:
        return product(*(range(a, b + 1) for a, b in zip(self.lo, self.hi)))

    def corners(self):
        return product(*zip(self.lo, self.hi))

    def intersection(self, other):
        low = tuple(max(a, b) for a, b in zip(self.lo, other.lo))
        high = tuple(min(a, b) for a, b in zip(self.hi, other.hi))
        return None if any(a > b for a, b in zip(low, high)) else Box(low, high)

    def expand(self, n: int):
        return Box(tuple(v - n for v in self.lo), tuple(v + n for v in self.hi))

    @property
    def size(self):
        return tuple(b - a + 1 for a, b in zip(self.lo, self.hi))

    @property
    def volume(self):
        x, y, z = self.size
        return x * y * z

    @classmethod
    def enclosing(cls, points):
        points = list(points)
        if not points:
            raise ValueError('No realized geometry')
        return cls(tuple(min(p[i] for p in points) for i in range(3)), tuple(max(p[i] for p in points) for i in range(3)))


@dataclass(frozen=True)
class Frame:
    origin: Pos = (0, 0, 0)
    turn: int = 0

    def __post_init__(self):
        object.__setattr__(self, 'origin', tuple(self.origin))
        if not isinstance(self.turn, int) or any(not isinstance(v, int) for v in self.origin):
            raise TypeError('Frames use integer coordinates and quarter turns')
        object.__setattr__(self, 'turn', self.turn % 4)

    def vector(self, p: Pos) -> Pos:
        x, y, z = p
        for _ in range(self.turn % 4):
            x, z = -z, x
        return x, y, z

    def point(self, p: Pos) -> Pos:
        return tuple(a + b for a, b in zip(self.origin, self.vector(p)))

    def inverse(self, p: Pos) -> Pos:
        return Frame(turn=-self.turn).vector(tuple(a - b for a, b in zip(p, self.origin)))

    def box(self, box: Box) -> Box:
        return Box.enclosing(self.point(p) for p in box.corners())

    def compose(self, other):
        return Frame(self.point(other.origin), (self.turn + other.turn) % 4)

    def state(self, state: str) -> str:
        from .blocks import parse, state_of
        name, props = parse(state)
        dirs = ['north', 'east', 'south', 'west']
        rotated = {}
        for key, value in props.items():
            if key in dirs:
                key = dirs[(dirs.index(key) + self.turn) % 4]
            if key == 'facing' and value in dirs:
                value = dirs[(dirs.index(value) + self.turn) % 4]
            if key in ('shape', 'orientation') and any(v in dirs for v in value.split('_')):
                from .blocks import SCHEMA
                tokens = [dirs[(dirs.index(v) + self.turn) % 4] if v in dirs else v for v in value.split('_')]
                value = '_'.join(tokens)
                allowed = SCHEMA[name.removeprefix('minecraft:')][0][key]
                if value not in allowed:
                    matches = [candidate for candidate in allowed if sorted(candidate.split('_')) == sorted(tokens)]
                    if matches:
                        value = sorted(matches)[0]
            if key == 'axis' and value in ('x', 'z') and self.turn % 2:
                value = 'z' if value == 'x' else 'x'
            if key == 'rotation':
                value = str((int(value) + 4 * self.turn) % 16)
            rotated[key] = value
        return state_of(name, **rotated)
