# Small architectural components with executable contracts.
from dataclasses import dataclass
from hearth.space import Box, Frame
from hearth.blocks import AIR, state_of, log, leaves, name_of
from hearth.kernel import Capability, Contract, Domain, Grant, Port, Rule, Plan, Child, Binding, ContractError


@dataclass(frozen=True)
class Volume:
    box: Box
    material: str
    role: str = 'masonry'

    def capability(self):
        return Capability('volume.' + self.role, (self.role,))

    def negotiate(self, ctx, parameters):
        return Contract(self.box)

    def realize(self, ctx, contract):
        plan = Plan()
        plan.fill(self.box, self.material)
        return plan


@dataclass(frozen=True)
class Grass:
    tall: bool = False

    def capability(self):
        return Capability('plant.grass', ('plant',), offers=('replacement',), assumptions=('substrate',))

    def negotiate(self, ctx, parameters):
        box = Box((0, 0, 0), (0, int(self.tall), 0))
        return Contract(box, (Port('replacement', 'planting', Frame(), box, grant='replace'),), (Grant('replace', box, ('plant',), ('tree',), 2, True),), (Rule('support', ((0, -1, 0),)),), atomic_object=True)

    def realize(self, ctx, contract):
        plan = Plan()
        if self.tall:
            for y, half in enumerate(('lower', 'upper')):
                plan.block((0, y, 0), state_of('tall_grass', half=half))
        else:
            plan.block((0, 0, 0), state_of('short_grass'))
        return plan


@dataclass(frozen=True)
class Tree:
    height: int = 10
    radius: int = 3

    def capability(self):
        return Capability('plant.conifer', ('plant', 'tree'), {'height': Domain(5, 18), 'radius': Domain(1, 4)}, assumptions=('substrate',), adaptations=('prune canopy against obstacles within accepted radius',))

    def negotiate(self, ctx, parameters):
        h, r = parameters.get('height', self.height), parameters.get('radius', self.radius)
        if not 5 <= h <= 18 or not 1 <= r <= 4:
            raise ContractError('tree-domain', ctx.path)
        return Contract(Box((-r, 0, -r), (r, h, r)), rules=(Rule('support', ((0, -1, 0),)), Rule('expected', ((0, 0, 0),), data={'names': ['spruce_log']})), decisions={'height': h, 'radius': r}, reads=(Box((-r, -1, -r), (r, h, r)),))

    def realize(self, ctx, contract):
        p = Plan()
        h = contract.decisions['height']
        radius = contract.decisions['radius']
        for y in range(h):
            p.block((0, y, 0), log())
        for y in range(3, h + 1):
            if y % 3 == 0 and y != h:
                continue
            r = min(radius, max(0, (h - y + 1) // 2))
            if y % 3 == 0:
                r = max(0, r - 1)
            for x in range(-r, r + 1):
                for z in range(-r, r + 1):
                    if abs(x) + abs(z) > r + 1 or (x == 0 and z == 0 and y < h):
                        continue
                    q = ctx.world((x, y, z))
                    if ctx.view.domain and not ctx.view.domain.contains(q):
                        continue
                    if name_of(ctx.view.state(q)) == 'air':
                        p.block((x, y, z), leaves())
        return p


@dataclass(frozen=True)
class Wall:
    width: int = 9
    height: int = 5
    thickness: int = 1
    infill: str | None = None
    timber: str = 'spruce'

    def capability(self):
        return Capability('wall.framed', ('wall', 'enclosure'), offers=('window', 'door'), assumptions=('floor below',), guarantees=('frame preserved', 'sealed infill'), inputs={'width': Domain(5, 64), 'height': Domain(4, 12), 'thickness': Domain(1, 3), 'frame.turn': Domain(choices=(0, 1, 2, 3))}, locked=('width', 'height', 'thickness', 'infill'))

    def negotiate(self, ctx, parameters):
        w, h, t = self.width, self.height, self.thickness
        if not 5 <= w <= 64 or not 4 <= h <= 12 or not 1 <= t <= 3:
            raise ContractError('wall-domain', ctx.path)
        box = Box((0, 0, 0), (w - 1, h - 1, t - 1))
        install = Box((1, 0, 0), (w - 2, h - 2, t - 1))
        ports = (Port('infill', 'wall-installation', Frame((1, 0, 0)), install, 20, 'install', 1, {'thickness': t, 'sill': 1, 'margin': 1}),)
        grants = (Grant('install', install, ('install',), ('window', 'door'), w * h * t),)
        boundary = tuple(box.cells())
        frame = tuple((x, y, z) for x in (0, w - 1) for y in range(-1, h) for z in range(t))
        return Contract(box, ports, grants, (Rule('support', frame), Rule('sealed', boundary, phase='complete')), decisions={'width': w, 'height': h, 'thickness': t})

    def realize(self, ctx, contract):
        p = Plan()
        p.fill(contract.envelope, self.infill or ctx.view.style['infill'])
        for x in (0, self.width - 1):
            p.fill(Box((x, 0, 0), (x, self.height - 1, self.thickness - 1)), log(self.timber))
        p.fill(Box((0, self.height - 1, 0), (self.width - 1, self.height - 1, self.thickness - 1)), log(self.timber, 'x'))
        return p


@dataclass(frozen=True)
class Window:
    width: int = 2
    height: int = 2
    shutters: bool = True
    glass: str = 'minecraft:glass'

    def capability(self):
        return Capability('window.shuttered' if self.shutters else 'window.lattice', ('window', 'sealed-opening'), offers=('daylight',), adaptations=('depth reveal',), guarantees=('sealed opening', 'frame margins'))

    def negotiate(self, ctx, parameters):
        if not ctx.bindings:
            raise ContractError('window-host', ctx.path)
        port = ctx.view.port(ctx.bindings[0].host, ctx.bindings[0].port)
        depth = port.facts['thickness']
        box = Box((-1, -1, -1), (self.width, self.height - 1, depth - 1))
        inner = Box((0, 0, 0), (self.width - 1, self.height - 1, depth - 1))
        if not all(port.region.contains(ctx.world(q)) for q in inner.corners()):
            raise ContractError('window-margins', ctx.path, ctx.frame.origin, 'Opening crosses excluded framing')
        return Contract(box, rules=(Rule('sealed', tuple(inner.cells())),), decisions={'depth': depth})

    def realize(self, ctx, contract):
        p = Plan()
        d = contract.decisions['depth']
        for z in range(d):
            p.fill(Box((0, 0, z), (self.width - 1, self.height - 1, z)), self.glass, 'glazing')
        if self.shutters:
            from hearth.blocks import slab
            for x in (-1, self.width):
                for y in range(self.height):
                    p.block((x, y, -1), state_of('spruce_trapdoor', facing='north', half='bottom', open='true', powered='false', waterlogged='false'), 'shutters')
            for x in range(self.width):
                p.block((x, -1, -1), slab('spruce', 'top'), 'sill')
        else:
            for x in range(self.width):
                p.block((x, self.height - 1, -1), state_of('spruce_trapdoor', facing='north', half='top', open='false', powered='false', waterlogged='false'), 'hood')
        return p


@dataclass(frozen=True)
class Pool:
    width: int = 7
    depth: int = 6

    def capability(self):
        return Capability('water.basin', ('pool', 'siteworks'), adaptations=('bounded excavation',), guarantees=('contained water',))

    def negotiate(self, ctx, parameters):
        box = Box((0, -2, 0), (self.width - 1, 0, self.depth - 1))
        water = tuple(Box((1, -1, 1), (self.width - 2, 0, self.depth - 2)).cells())
        return Contract(box, rules=(Rule('pool', water),), decisions={'excavation_limit': box.volume})

    def realize(self, ctx, contract):
        p = Plan()
        p.fill(contract.envelope, state_of('smooth_sandstone'), 'basin')
        p.fill(Box((1, -1, 1), (self.width - 2, 0, self.depth - 2)), state_of('water', level='0'), 'water')
        return p


@dataclass(frozen=True)
class Assembly:
    name: str
    envelope: Box
    children: tuple[Child, ...]
    rules: tuple[Rule, ...] = ()
    ports: tuple[Port, ...] = ()

    def capability(self):
        return Capability(self.name, ('composite',))

    def negotiate(self, ctx, parameters):
        return Contract(self.envelope, ports=self.ports, rules=self.rules)

    def realize(self, ctx, contract):
        return Plan(children=list(self.children))
