# Ground-supported planting from public boundary geometry and measured free space.
from dataclasses import dataclass
from hearth import Box, Frame, ContractError
from hearth.kernel import Capability, Contract, Domain, Rule, Port, Plan, Child, Binding
from hearth.blocks import name_of, state_of


@dataclass(frozen=True)
class GardenPlant:
    species: str = 'fern'

    def capability(self):
        return Capability('plant.garden.' + self.species, ('plant', 'garden'),
                          inputs={'species': Domain(choices=('fern', 'azalea', 'flowering_azalea', 'poppy', 'allium', 'rose_bush'))},
                          assumptions=('actual soil support',), guarantees=('whole plant', 'supported'))

    def negotiate(self, ctx, parameters):
        if self.species not in ('fern', 'azalea', 'flowering_azalea', 'poppy', 'allium', 'rose_bush'):
            raise ContractError('garden-plant-domain', ctx.path)
        if name_of(ctx.state((0, -1, 0))) not in ('grass_block', 'dirt', 'coarse_dirt', 'podzol', 'rooted_dirt', 'moss_block'):
            raise ContractError('planting-substrate', ctx.path, ctx.world((0, -1, 0)))
        height = 2 if self.species == 'rose_bush' else 1
        return Contract(Box((0, 0, 0), (0, height - 1, 0)), rules=(Rule('support', ((0, -1, 0),)),
                        Rule('expected', tuple((0, y, 0) for y in range(height)), data={'names': [self.species]})), atomic_object=True)

    def realize(self, ctx, contract):
        p = Plan()
        if self.species == 'rose_bush':
            for y, half in enumerate(('lower', 'upper')):
                p.block((0, y, 0), state_of(self.species, half=half), 'plant')
        else:
            p.block((0, 0, 0), state_of(self.species), 'plant')
        return p


@dataclass(frozen=True)
class BoundaryPlanting:
    """Plant a soft border near a discovered footprint; never excavate or fill."""
    host: str
    terrain: str
    density: float = .55
    minimum_distance: int = 2
    maximum_distance: int = 4

    def capability(self):
        return Capability('garden.boundary-border', ('garden', 'composite'), assumptions=('footprint-boundary interface',),
                          adaptations=('actual soil and clearance', 'cluster density'),
                          inputs={'density': Domain(0, 1), 'minimum_distance': Domain(1, 6), 'maximum_distance': Domain(1, 6)},
                          guarantees=('no terrain edits', 'retained paths and existing consumers'))

    def negotiate(self, ctx, parameters):
        if not 0 <= self.density <= 1 or not 1 <= self.minimum_distance <= self.maximum_distance <= 6:
            raise ContractError('border-domain', ctx.path)
        ports = ctx.view.offers(self.host, 'footprint-boundary')
        if not ports:
            raise ContractError('planting-boundary-interface', ctx.path, conditions='Host must expose a footprint-boundary port')
        port = ports[0]
        points = tuple(ctx.local(port.frame.point(tuple(q))) for q in port.facts['outline'])
        radius = self.maximum_distance
        box = Box.enclosing(points).expand(radius)
        # Surface observations determine height, not the boundary's nominal Y.
        candidates, rejected = [], {'unknown': 0, 'substrate': 0, 'occupied_or_protected': 0}
        edge = {(x, z) for x, _, z in points}
        for x in range(box.lo[0], box.hi[0] + 1):
            for z in range(box.lo[2], box.hi[2] + 1):
                distance = min(abs(x - a) + abs(z - b) for a, b in edge)
                if not self.minimum_distance <= distance <= radius:
                    continue
                # A stable field creates patches instead of a uniform perimeter hedge.
                patch = ctx.scope.child(f'patch-{x // 3}-{z // 3}').stream('density').uniform(.2, 1)
                rng = ctx.scope.child(f'cell-{x}-{z}').stream('planting')
                if rng.random() > self.density * patch:
                    continue
                wp = ctx.world((x, 0, z))
                if ctx.view.domain and not (ctx.view.domain.lo[0] <= wp[0] <= ctx.view.domain.hi[0] and ctx.view.domain.lo[2] <= wp[2] <= ctx.view.domain.hi[2]):
                    rejected['unknown'] += 1
                    continue
                y = ctx.elevation(x, z) + 1
                if name_of(ctx.state((x, y - 1, z))) not in ('grass_block', 'podzol', 'dirt'):
                    rejected['substrate'] += 1
                    continue
                area = Box((x, y, z), (x, y + 1, z))
                if not all(ctx.view.domain.contains(ctx.world(q)) for q in area.corners()):
                    rejected['unknown'] += 1
                    continue
                if any(name_of(ctx.state(q)) != 'air' for q in area.cells()) or ctx.view.protected(ctx.frame.box(area)):
                    rejected['occupied_or_protected'] += 1
                    continue
                species = rng.choice(('fern', 'fern', 'azalea', 'flowering_azalea', 'poppy', 'allium', 'rose_bush'))
                candidates.append(((x, y, z), species))
        bounds = Box.enclosing([*points, *(p for p, _ in candidates), *((p[0], p[1] + 1, p[2]) for p, _ in candidates)])
        return Contract(bounds, decisions={'plants': candidates, 'rejected': rejected, 'optional_minimum': 0, 'outline': points})

    def realize(self, ctx, contract):
        p = Plan()
        for q, species in contract.decisions['plants']:
            p.children.append(Child(f'plant-{q[0]}-{q[2]}', GardenPlant(species), frame=Frame(tuple(q)), bindings=(Binding(self.terrain, 'planting', 'plant'),)))
        p.relations.append((ctx.path, 'landscapes', self.host))
        return p
