# Public ridge mounts and fitted glazed lightwells composed from real wall windows.
from dataclasses import dataclass
from hearth import Box, Frame, ContractError
from hearth.kernel import Capability, Contract, Domain, Port, Grant, Rule, Plan, Child, Binding
from hearth.blocks import AIR, name_of, solid, state_of, log, stairs


@dataclass(frozen=True)
class RidgeMount:
    """Describe a mount relative to the ridge center at the first footprint row.

    X crosses the ridge, Z follows it and Y is relative to the roof peak. The
    five-cell bearing footprint excludes the one-cell cap overhang. Hosts can
    fit this description without inspecting a particular attachment's geometry.
    """
    length: int = 5
    lower: int = -4
    upper: int = 6

    def __post_init__(self):
        if any(type(v) is not int for v in (self.length, self.lower, self.upper)) or self.length not in (5, 7, 9) or not -8 <= self.lower <= 0 or not 2 <= self.upper <= 12:
            raise ValueError('Ridge mount length is 5/7/9; lower -8..0; upper 2..12')

    @property
    def envelope(self):
        return Box((-3, self.lower, -1), (3, self.upper, self.length))

    @property
    def footprint(self):
        return Box((-2, 0, 0), (2, 0, self.length - 1))

    @property
    def occupied_envelopes(self):
        """Conservative body/cut and high-cap regions for neighboring fittings."""
        return (Box((-2, self.lower, 0), (2, self.upper, self.length - 1)), Box((-3, 2, -1), (3, self.upper, self.length)))


@dataclass(frozen=True)
class RidgeLantern:
    """Fit a timber-and-glass lightwell through an aligned ridge installation."""
    length: int = 5
    material: str = 'deepslate_tile'
    timber: str = 'dark_oak'

    def mounting(self):
        return RidgeMount(self.length)

    def capability(self):
        return Capability('roof.ridge-lantern', ('roof', 'enclosure', 'ridge-attachment', 'composite'), inputs={'length': Domain(choices=(5, 7, 9)), 'timber': Domain(choices=('spruce', 'dark_oak'))}, offers=('daylight',), assumptions=('aligned ridge-installation with measured roof bearings',), adaptations=('fit ring beams to actual roof profile',), guarantees=('open daylight well', 'sealed glazing and hipped cap', 'bounded roof replacement'))

    def negotiate(self, ctx, parameters):
        if len(ctx.bindings) != 1:
            raise ContractError('ridge-host', ctx.path)
        port = ctx.view.port(ctx.bindings[0].host, ctx.bindings[0].port)
        if port.kind != 'ridge-installation' or port.frame != ctx.frame or port.facts.get('length') != self.length:
            raise ContractError('ridge-interface', ctx.path, ctx.frame.origin, 'Require aligned ridge mount of the requested length')
        box = self.mounting().envelope
        if not all(port.region.contains(ctx.world(q)) for q in box.corners()):
            raise ContractError('ridge-envelope', ctx.path)
        surface = tuple(tuple(q) for q in port.facts['surface'])
        anchors = tuple(tuple(q) for q in port.facts['anchors'])
        expected = {(x, z) for x in range(-2, 3) for z in range(self.length)}
        if {(x, z) for x, _, z in surface} != expected or any(y < box.lo[1] or y > 0 for _, y, _ in surface):
            raise ContractError('ridge-surface', ctx.path, conditions='Complete bounded local roof surface required')
        if len(anchors) != 4 or {(x, z) for x, _, z in anchors} != {(x, z) for x in (-2, 2) for z in (-1, self.length)} or not all(box.contains(q) for q in anchors):
            raise ContractError('ridge-anchors', ctx.path, conditions='Four bounded end bearings are required')
        for q in (*surface, *anchors):
            if not solid(ctx.state(q)):
                raise ContractError('ridge-bearing', ctx.path, ctx.world(q), 'Measured roof contact is not an actual solid cell')
        heights = {(x, z): y for x, y, z in surface}
        ring = tuple((x, y, z) for x in range(-2, 3) for z in range(self.length) if x in (-2, 2) or z in (0, self.length - 1) for y in range(min(heights[x, z], -3), -2))
        posts = tuple((x, y, z) for x in (-2, 2) for z in (0, self.length - 1) for y in range(heights[x, z], 3))
        cap = tuple((x, 2 + min(x + 3, 3 - x, z + 1, self.length - z, 3), z) for x in range(-3, 4) for z in range(-1, self.length + 1))
        targets = ((0, -3, self.length // 2), (0, 0, 0), (0, 0, self.length - 1), (-2, 0, 1), (2, 0, 1))
        rules = (Rule('support', anchors), Rule('expected', ring + posts, data={'names': [self.timber + '_log']}), Rule('sealed', cap), Rule('daylight-path', targets, Box((-2, -4, 0), (2, 2, self.length - 1)), phase='complete'), Rule('expected', ((0, 5, self.length // 2),), data={'names': [self.material + 's']}), Rule('expected', ((0, 6, self.length // 2),), data={'names': [self.timber + '_fence']}))
        well = Box((-1, -4, 1), (1, 1, self.length - 2))
        port = Port('well', 'daylight', Frame((0, -3, self.length // 2)), well, 32, facts={'coordinate_space': 'component-local', 'player_access': False, 'transmission': 'connected air and glass; thin light hangers allowed'})
        return Contract(box, ports=(port,), rules=rules, decisions={'length': self.length, 'surface': surface, 'anchors': anchors, 'ring': ring, 'posts': posts, 'cap': cap, 'well': (well.lo, well.hi)})

    def realize(self, ctx, contract):
        from .roofcraft import FlushWindow
        p = Plan()
        # Only this bound component cuts the host. Descendant walls write into
        # the vacated faces and grant their own small glazing replacements.
        opening = Box(*contract.decisions['well'])
        faces = [Frame((-1, -2, 0)), Frame((1, -2, self.length - 1), 2), Frame((-2, -2, self.length - 2), 3), Frame((2, -2, 1), 1)]
        widths = (3, 3, self.length - 2, self.length - 2)
        cut = set(opening.cells())
        for frame, width in zip(faces, widths):
            cut.update(frame.box(Box((0, 0, 0), (width - 1, 4, 0))).cells())
        for q in sorted(cut):
            if name_of(ctx.state(q)) != 'air':
                p.block(q, AIR, 'lightwell-cut')
        for q in sorted(set(contract.decisions['ring']) | set(contract.decisions['posts'])):
            p.block(q, log(self.timber), 'bearing-frame')
        for x, y, z in contract.decisions['cap']:
            across, along = min(x + 3, 3 - x), min(z + 1, self.length - z)
            if across == along == 3 or (x == 0 and along >= 3):
                block = state_of(self.material + 's')
            else:
                facing = ('east' if x < 0 else 'west') if across <= along else ('south' if z < self.length // 2 else 'north')
                block = stairs(self.material, facing)
            p.block((x, y, z), block, 'hipped-cap')
        p.block((0, 6, self.length // 2), state_of(self.timber + '_fence', north='false', south='false', east='false', west='false', waterlogged='false'), 'finial')
        for key, frame, width in zip(('front', 'rear', 'left', 'right'), faces, widths):
            p.children.append(Child(key, GlazedPanel(width, self.timber, FlushWindow), frame=frame))
        return p


@dataclass(frozen=True)
class GlazedPanel:
    """A thin supported timber panel with a scoped, replaceable glazing bay."""
    width: int = 3
    timber: str = 'dark_oak'
    window: object = None
    height: int = 3

    def capability(self):
        return Capability('wall.glazed-panel', ('wall', 'enclosure', 'composite'), offers=('wall-installation',), inputs={'width': Domain(1, 9), 'height': Domain(2, 4)}, guarantees=('sealed framed glazing', 'actual sill bearing'))

    def negotiate(self, ctx, parameters):
        box = Box((0, 0, 0), (self.width - 1, self.height + 1, 0))
        bays = ((0, self.width - 1),) if self.width < 5 else ((0, self.width // 2 - 1), (self.width // 2 + 1, self.width - 1))
        ports, grants = [], []
        for index, (first, last) in enumerate(bays):
            bay = Box((first, 1, 0), (last, self.height, 0))
            key = 'glazing-' + str(index)
            ports.append(Port(key, 'wall-installation', Frame((first, 1, 0)), bay, 1, key, facts={'thickness': 1}))
            grants.append(Grant(key, bay, ('install',), ('window',), bay.volume))
        return Contract(box, ports=tuple(ports), grants=tuple(grants), rules=(Rule('support', tuple((x, -1, 0) for x in range(self.width))), Rule('sealed', tuple(box.cells()), phase='complete')), decisions={'bays': bays})

    def realize(self, ctx, contract):
        from .roofcraft import FlushWindow
        p = Plan()
        p.fill(contract.envelope, log(self.timber, 'x'), 'panel-frame')
        for index, (first, last) in enumerate(contract.decisions['bays']):
            p.children.append(Child('window-' + str(index), (self.window or FlushWindow)(last - first + 1, self.height), frame=Frame((first, 1, 0)), bindings=(Binding(ctx.path, 'glazing-' + str(index)),)))
        return p
