# Measured, bounded support and access strategies independent of terrain recipes.
from dataclasses import dataclass
from hearth.space import Box, Frame
from hearth.blocks import state_of, log, stairs, name_of, solid
from hearth.kernel import Capability, Contract, Port, Grant, Rule, Plan, ContractError


@dataclass(frozen=True)
class SiteLimits:
    relief: int = 5
    support_height: int = 8
    approach_length: int = 12
    fill_volume: int = 1600
    excavation_volume: int = 300
    bed_thickness: int = 2


def survey(ctx, footprint, limits=SiteLimits()):
    samples = {}
    for x, z in sorted(footprint):
        wx, _, wz = ctx.world((x, 0, z))
        obs = ctx.view.sample(wx, wz)
        if obs.support_depth < limits.bed_thickness:
            raise ContractError('substrate-depth', ctx.path, (wx, obs.elevation, wz), f'Require {limits.bed_thickness} contiguous solid bed cells, found {obs.support_depth}')
        samples[(x, z)] = (obs.elevation - ctx.frame.origin[1], None if obs.water is None else obs.water - ctx.frame.origin[1], obs.support_depth)
    heights = [v[0] for v in samples.values()]
    relief = max(heights) - min(heights)
    if relief > limits.relief:
        raise ContractError('site-relief', ctx.path, ctx.frame.origin, f'Measured {relief}, permitted {limits.relief}')
    surface = max(max(v[0], v[1] if v[1] is not None else v[0]) for v in samples.values())
    floor = surface + 1
    if floor - min(heights) > limits.support_height:
        raise ContractError('support-height', ctx.path, ctx.frame.origin, 'Bounded support depth exceeded')
    wet = any(v[1] is not None and v[1] >= v[0] for v in samples.values())
    strategies = ['piers'] if wet or relief >= 3 else ['stepped', 'piers']
    strategy = ctx.scope.choose(strategies, 'adaptation')
    return {'floor': floor, 'relief': relief, 'wet': wet, 'strategy': strategy, 'samples': samples, 'feasible_strategies': strategies}


@dataclass(frozen=True)
class Foundation:
    footprint: tuple[tuple[int, int], ...]
    floor: int
    strategy: str
    limits: SiteLimits = SiteLimits()

    @classmethod
    def rectangular(cls, width, depth, floor, strategy='stepped', limits=SiteLimits()):
        """Create a rectangular contact component without client voxel operations."""
        if width < 1 or depth < 1:
            raise ValueError('Positive footprint dimensions required')
        return cls(tuple((x, z) for x in range(width) for z in range(depth)), floor, strategy, limits)

    def capability(self):
        return Capability('adapter.foundation', ('siteworks', 'support'), offers=('support',), assumptions=('actual bed within bounded depth',), adaptations=('stepped perimeter', 'piers to bed'), guarantees=('grounded support grid',))

    def negotiate(self, ctx, parameters):
        positions = set(self.footprint)
        supports = []
        columns = []
        for x, z in sorted(positions):
            boundary = any((x + dx, z + dz) not in positions for dx, dz in ((1, 0), (-1, 0), (0, 1), (0, -1)))
            grid = (x % 4 == 0 and z % 4 == 0)
            # Piers include perimeter every third cell and all corners.
            edge_pier = boundary and (x % 3 == 0 or z % 3 == 0 or sum((x + dx, z + dz) not in positions for dx, dz in ((1, 0), (-1, 0), (0, 1), (0, -1))) >= 2)
            if not (grid or (boundary if self.strategy == 'stepped' else edge_pier)):
                continue
            wx, _, wz = ctx.world((x, 0, z))
            bed = ctx.view.elevation(wx, wz) - ctx.frame.origin[1]
            if self.floor - bed > self.limits.support_height:
                raise ContractError('support-height', ctx.path, ctx.world((x, bed, z)))
            supports.extend((x, bed - k, z) for k in range(self.limits.bed_thickness))
            columns.append((x, bed, z, self.floor - 1))
        count = sum(max(0, top - bed) for x, bed, z, top in columns)
        if count > self.limits.fill_volume:
            raise ContractError('fill-volume', ctx.path, conditions=f'{count} > {self.limits.fill_volume}')
        for x, bed, z, top in columns:
            supports.extend((x, y, z) for y in range(bed + 1, top + 1))
        box = Box.enclosing([(x, bed - self.limits.bed_thickness + 1, z) for x, bed, z, top in columns] + [(x, self.floor - 1, z) for x, bed, z, top in columns])
        return Contract(box, rules=(Rule('support', tuple(supports)),), decisions={'columns': columns, 'strategy': self.strategy, 'fill': count, 'excavation': 0}, reads=(box,))

    def realize(self, ctx, contract):
        p = Plan()
        for x, bed, z, top in contract.decisions['columns']:
            for y in range(bed + 1, top + 1):
                material = log('spruce') if self.strategy == 'piers' and y > bed + 1 else state_of('stone_bricks')
                p.block((x, y, z), material, 'contact')
        return p


@dataclass(frozen=True)
class Approach:
    center: int
    floor: int
    porch: int = 0
    limits: SiteLimits = SiteLimits()

    def capability(self):
        return Capability('adapter.approach', ('siteworks', 'access'), offers=('street', 'surface'), adaptations=('stairs', 'landing', 'bounded fill'), guarantees=('two-block headroom', 'entrance connection'))

    def negotiate(self, ctx, parameters):
        rows = []
        for step in range(1, self.limits.approach_length + 1):
            z = -self.porch - step
            wx, _, wz = ctx.world((self.center, 0, z))
            ground = max(max(ctx.view.elevation(ctx.world((x, 0, z))[0], ctx.world((x, 0, z))[2]), (ctx.view.water(ctx.world((x, 0, z))[0], ctx.world((x, 0, z))[2]) or -10000) + 1) - ctx.frame.origin[1] for x in range(self.center - 1, self.center + 2))
            y = max(ground, self.floor - step + 1)
            rows.append((z, y))
            if y == ground:
                break
        else:
            raise ContractError('approach-detour', ctx.path, ctx.frame.origin, 'Cannot meet terrain within length bound')
        cells = []
        support = []
        for z, y in rows:
            cells.extend((x, y + k, z) for x in range(self.center - 1, self.center + 2) for k in (1, 2))
            support.extend((x, y, z) for x in range(self.center - 1, self.center + 2))
        low = min(ctx.view.elevation(*((lambda p: (p[0], p[2]))(ctx.world((x, 0, z))))) - ctx.frame.origin[1] for z, y in rows for x in range(self.center - 1, self.center + 2))
        box = Box((self.center - 1, low, rows[-1][0]), (self.center + 1, max(y for z, y in rows) + 3, -self.porch - 1))
        end = (self.center, rows[-1][1] + 1, rows[-1][0])
        region = Box((self.center - 1, rows[-1][1], rows[-1][0]), (self.center + 1, self.floor + 2, min(-self.porch - 1, rows[-1][0] + 1)))
        port = Port('street', 'access', Frame(end), region, 10, 'surface')
        grant = Grant('surface', region, ('surface',), ('path',), 18)
        route = tuple((self.center, y + 1, z) for z, y in rows)
        return Contract(box, (port,), (grant,), (Rule('clear', tuple(cells)), Rule('support', tuple(support)), Rule('route', route, box.expand(1), phase='complete')), decisions={'rows': rows, 'end': end}, reads=(box,))

    def realize(self, ctx, contract):
        p = Plan()
        rows = contract.decisions['rows']
        for index, (z, y) in enumerate(rows):
            for x in range(self.center - 1, self.center + 2):
                wx, _, wz = ctx.world((x, 0, z))
                bed = ctx.view.elevation(wx, wz) - ctx.frame.origin[1]
                for yy in range(bed + 1, y):
                    p.block((x, yy, z), state_of('cobblestone'), 'step-footing')
                p.block((x, y, z), stairs('stone_brick', 'south') if index and rows[index - 1][1] > y else state_of('stone_bricks'), 'treads')
        return p
