# Curve-following masonry with sampled installation sockets and grounded adaptation.
from dataclasses import dataclass
from hearth import Box, Frame, Binding, ContractError
from hearth.kernel import Capability, Contract, Domain, Port, Grant, Plan, Rule, Child
from hearth.blocks import state_of, slab
from .geometry import Polyline, bounds2


@dataclass(frozen=True)
class Boundary:
    curve: Polyline
    height: int = 2
    spacing: int = 7
    max_step: int = 1

    def capability(self):
        return Capability('extension.curved-boundary', ('boundary', 'siteworks', 'composite'), offers=('mount',), inputs={'height': Domain(1, 5), 'spacing': Domain(4, 12), 'max_step': Domain(0, 1)}, adaptations=('terrain-following base', 'stepped coping'), guarantees=('continuous cardinal contour', 'grounded boundary', 'mount sockets'))

    def negotiate(self, ctx, parameters):
        center = self.curve.cells()
        if len(center) > 220 or len(set(center)) != len(center):
            raise ContractError('boundary-domain', ctx.path, conditions='Simple open contour, at most 220 raster cells')
        levels = [ctx.elevation(x, z) for x, z in center]
        if any(abs(a - b) > self.max_step for a, b in zip(levels, levels[1:])):
            raise ContractError('boundary-grade', ctx.path, conditions='Adjacent wall foundations exceed step bound')
        ports = []
        grants = []
        for i in range(2, len(center) - 2, self.spacing):
            x, z = center[i]
            y = levels[i] + self.height + 2
            x0, z0 = center[max(0, i - 1)]
            x1, z1 = center[min(len(center) - 1, i + 1)]
            turn = 0 if abs(x1 - x0) >= abs(z1 - z0) else 1
            at = (x, y, z)
            key = f'mount-{i}'
            box = Box(at, at)
            ports.append(Port(key, 'mount', Frame(at, turn), box, 1, key, facts={'support': (x, y - 1, z), 'load': 'small-fixture'}))
            grants.append(Grant(key, box, ('install',), ('fixture',), 0))
        supports = tuple((x, levels[i], z) for i, (x, z) in enumerate(center))
        shell = tuple((x, y, z) for i, (x, z) in enumerate(center) for y in range(levels[i] + 1, levels[i] + self.height + 2))
        box = Box.enclosing((*supports, *shell)).expand(2)
        return Contract(box, tuple(ports), tuple(grants), (Rule('support', supports), Rule('sealed', shell)), decisions={'center': center, 'levels': levels, 'height': self.height}, reads=(box,))

    def realize(self, ctx, contract):
        p = Plan()
        d = contract.decisions
        rng = ctx.rng('masonry')
        for i, (x, z) in enumerate(d['center']):
            for y in range(d['levels'][i] + 1, d['levels'][i] + self.height + 1):
                p.block((x, y, z), state_of(rng.choice(('stone_bricks', 'stone_bricks', 'mossy_stone_bricks'))), 'contour-masonry')
            p.block((x, d['levels'][i] + self.height + 1, z), slab('stone_brick', 'top'), 'coping')
        return p


@dataclass(frozen=True)
class BoundaryLantern:

    def capability(self):
        return Capability('extension.boundary-lantern', ('fixture', 'light'), guarantees=('mounted actual light',))

    def negotiate(self, ctx, parameters):
        return Contract(Box((0, 0, 0), (0, 1, 0)), rules=(Rule('support', ((0, -1, 0),)), Rule('expected', ((0, 0, 0),), data={'names': ['lantern']})), atomic_object=True)

    def realize(self, ctx, contract):
        p = Plan()
        p.block((0, 0, 0), state_of('lantern', hanging='false', waterlogged='false'), 'lamp')
        return p


@dataclass(frozen=True)
class IlluminateBoundary:
    host: str
    every: int = 1

    def capability(self):
        return Capability('extension.boundary-lighting', ('composite',))

    def negotiate(self, ctx, parameters):
        ports = ctx.view.offers(self.host, 'mount')[::self.every]
        if not ports:
            raise ContractError('missing-mounts', ctx.path)
        return Contract(Box.enclosing(ctx.local(p.frame.origin) for p in ports).expand(1))

    def realize(self, ctx, contract):
        p = Plan()
        for port in ctx.view.offers(self.host, 'mount')[::self.every]:
            frame = Frame(ctx.local(port.frame.origin), (port.frame.turn - ctx.frame.turn) % 4)
            p.children.append(Child(port.key, BoundaryLantern(), frame=frame, bindings=(Binding(self.host, port.key),)))
        return p


@dataclass(frozen=True)
class BoundaryChain:
    curve: Polyline
    height: int = 2
    spacing: int = 7
    chunk: int = 96

    def capability(self):
        return Capability('extension.boundary-chain', ('boundary', 'composite'), offers=('mount',), inputs={'chunk': Domain(4, 220), 'height': Domain(1, 5), 'spacing': Domain(4, 12)}, guarantees=('continuous contour across component joins',))

    def parts(self):
        cells = self.curve.cells()
        if len(set(cells)) != len(cells):
            raise ContractError('boundary-self-intersection', conditions='A simple open contour is required')
        count = max(1, (len(cells) + self.chunk - 1) // self.chunk)
        size = (len(cells) + count - 1) // count
        return tuple(Boundary(Polyline(cells[i:i + size]), self.height, self.spacing) for i in range(0, len(cells), size))

    def negotiate(self, ctx, parameters):
        boxes = []
        ports = []
        last_height = None
        for i, part in enumerate(self.parts()):
            local = part.negotiate(ctx, {})
            if last_height is not None and abs(local.decisions['levels'][0] - last_height) > part.max_step:
                raise ContractError('boundary-grade', ctx.path, conditions='Grade bound also applies at component joins')
            last_height = local.decisions['levels'][-1]
            boxes.append(local.envelope)
            for p in local.ports:
                ports.append(Port(f'segment-{i}-{p.key}', p.kind, p.frame, p.region, p.capacity, facts=p.facts, delegate=(f'segment-{i}', p.key)))
        return Contract(Box.enclosing(p for box in boxes for p in box.corners()), ports=tuple(ports), decisions={'segments': len(boxes), 'curve': self.curve.points})

    def realize(self, ctx, contract):
        return Plan(children=[Child(f'segment-{i}', part) for i, part in enumerate(self.parts())])
