# Public paths, planted borders, lamps and constrained landscape compositions.
from dataclasses import dataclass
from hearth.space import Box, Frame
from hearth.blocks import state_of, stairs, slab, log, leaves, name_of
from hearth.kernel import Capability, Contract, Port, Grant, Rule, Plan, Child, Binding, ContractError
from hearth.composition import scatter
from .primitives import Tree, Grass
from .interior import Light


@dataclass(frozen=True)
class PathNetwork:
    waypoints: tuple[tuple[int, int], ...]
    links: tuple[tuple[int, int], ...]
    width: int = 3
    gateway: tuple | None = None

    def capability(self):
        return Capability('landscape.paths', ('path', 'connection'), offers=('public-access',), adaptations=('terrain treads', 'bounded retaining edges'), guarantees=('connected public routes',))

    def _points(self):
        points = set()
        for a, b in self.links:
            x, z = self.waypoints[a][0], self.waypoints[a][-1]
            xx, zz = self.waypoints[b][0], self.waypoints[b][-1]
            dx = 1 if xx >= x else -1
            dz = 1 if zz >= z else -1
            offsets = range(-(self.width // 2), self.width // 2 + 1)
            if x != xx:
                for px in range(x, xx + dx, dx):
                    for offset in offsets:
                        points.add((px, z + offset))
            if z != zz:
                for pz in range(z, zz + dz, dz):
                    for offset in offsets:
                        points.add((xx + offset, pz))
            if x == xx and z == zz:
                for ox in offsets:
                    for oz in offsets:
                        points.add((x + ox, z + oz))
        return sorted(points)

    def negotiate(self, ctx, parameters):
        points = []
        for x, z in self._points():
            hint = min(self.waypoints, key=lambda q: abs(q[0] - x) + abs(q[-1] - z))
            maximum = hint[1] + ctx.frame.origin[1] if len(hint) == 3 else None
            wx, _, wz = ctx.world((x, 0, z))
            h = ctx.view.elevation(wx, wz, maximum=maximum) - ctx.frame.origin[1]
            points.append((x, h, z))
        if not points:
            raise ContractError('empty-path', ctx.path)
        box = Box.enclosing(points).expand(2)
        targets = []
        surface = {(x, z): y for x, y, z in points}
        for q in self.waypoints:
            x, z = q[0], q[-1]
            targets.append((x, surface[(x, z)] + 1, z))
        clears = tuple((x, y + k, z) for x, y, z in points for k in (1, 2))
        public = self.gateway or targets[0]
        interface = Box((public[0] - 1, public[1] - 1, public[2]), (public[0] + 1, public[1] + 1, public[2]))
        return Contract(box, ports=(Port('public', 'public-access', Frame(public), interface, 10000, 'join'),), grants=(Grant('join', interface, ('surface',), ('path',), 9),), rules=(Rule('support', tuple(points)), Rule('clear', clears), Rule('route', tuple(targets), box, phase='complete')), decisions={'surface': points, 'waypoints': self.waypoints, 'links': self.links}, reads=(box,))

    def realize(self, ctx, contract):
        p = Plan()
        rng = ctx.rng('materials')
        heights = {(x, z): y for x, y, z in contract.decisions['surface']}
        for x, y, z in contract.decisions['surface']:
            uphill = []
            for dx, dz, face in ((1, 0, 'east'), (-1, 0, 'west'), (0, 1, 'south'), (0, -1, 'north')):
                if heights.get((x + dx, z + dz), y) > y:
                    uphill.append(face)
            material = stairs('stone_brick', uphill[0]) if uphill else state_of(rng.choice(('stone_bricks', 'stone_bricks', 'andesite', 'cobblestone', 'mossy_stone_bricks')))
            p.block((x, y, z), material, 'paving')
        return p


@dataclass(frozen=True)
class Planter:
    width: int = 3
    flowers: tuple[str, ...] = ('cornflower', 'azure_bluet', 'allium')

    def capability(self):
        return Capability('garden.planter', ('decoration', 'plant'), guarantees=('grounded bed',))

    def negotiate(self, ctx, parameters):
        return Contract(Box((0, 0, 0), (self.width - 1, 1, 1)), rules=(Rule('support', tuple((x, -1, z) for x in range(self.width) for z in range(2))),))

    def realize(self, ctx, contract):
        p = Plan()
        rng = ctx.rng('flowers')
        for x in range(self.width):
            p.block((x, 0, 0), state_of('dirt'), 'soil')
            p.block((x, 1, 0), state_of(rng.choice(self.flowers)), 'flower')
            p.block((x, 0, 1), state_of('spruce_trapdoor', facing='south', half='bottom', open='true', powered='false', waterlogged='false'), 'edging')
        return p


@dataclass(frozen=True)
class LampPost:

    def capability(self):
        return Capability('landscape.lamppost', ('light', 'outdoor-light', 'composite'))

    def negotiate(self, ctx, parameters):
        return Contract(Box((0, 0, 0), (1, 4, 0)), rules=(Rule('support', ((0, -1, 0),)),))

    def realize(self, ctx, contract):
        p = Plan()
        p.block((0, 0, 0), state_of('stone_bricks'))
        for y in range(1, 4):
            p.block((0, y, 0), state_of('spruce_fence', north='false', south='false', east='false', west='false', waterlogged='false'))
        p.block((0, 4, 0), slab('spruce'))
        p.block((1, 4, 0), slab('spruce'))
        p.children.append(Child('lantern', Light(), frame=Frame((1, 2, 0))))
        return p


@dataclass(frozen=True)
class Landscape:
    area: Box
    terrain: str
    tree_minimum: int = 6
    tree_maximum: int = 14
    density: float = 0.08

    def capability(self):
        return Capability('landscape.scatter', ('landscape', 'composite'), adaptations=('clearance-aware scattering',), guarantees=('minimum tree count', 'preserved routes'))

    def negotiate(self, ctx, parameters):
        return Contract(self.area, decisions={'minimum_trees': self.tree_minimum, 'maximum_trees': self.tree_maximum, 'density': self.density})

    def realize(self, ctx, contract):
        p = Plan()
        x0, _, z0 = self.area.lo
        x1, _, z1 = self.area.hi
        candidates = [(x, z) for x in range(x0 + 4, x1 - 3, 4) for z in range(z0 + 4, z1 - 3, 4)]

        def legal(q, accepted):
            x, z = q
            if any(abs(x - a) + abs(z - b) < 10 for a, b in accepted):
                return False
            h = ctx.elevation(x, z)
            if name_of(ctx.state((x, h, z))) != 'grass_block':
                return False
            box = Box((x - 3, h + 1, z - 3), (x + 3, h + 16, z + 3))
            return all(ctx.view.domain.contains(ctx.world(c)) for c in box.corners()) and ctx.view.clear(ctx.frame.box(box)) and not ctx.view.protected(ctx.frame.box(Box((x - 3, h, z - 3), (x + 3, h + 2, z + 3))))

        chosen = scatter(ctx, candidates, legal, self.tree_minimum, self.tree_maximum, 'trees')
        for i, (x, z) in enumerate(chosen):
            y = ctx.elevation(x, z) + 1
            h = ctx.scope.child(f'tree-{i}').choose((9, 11, 13, 15), 'height')
            p.children.append(Child(f'tree-{i}', Tree(h), frame=Frame((x, y, z)), bindings=(Binding(self.terrain, 'planting', 'plant'),)))
        rng = ctx.rng('groundcover')
        for x in range(x0 + 1, x1):
            for z in range(z0 + 1, z1):
                if rng.random() > self.density or any(abs(x - a) <= 3 and abs(z - b) <= 3 for a, b in chosen):
                    continue
                h = ctx.elevation(x, z)
                if name_of(ctx.state((x, h, z))) != 'grass_block':
                    continue
                if name_of(ctx.state((x, h + 1, z))) != 'air':
                    continue
                if ctx.view.protected(ctx.frame.box(Box((x, h + 1, z), (x, h + 2, z)))):
                    continue
                p.children.append(Child(f'grass-{x}-{z}', Grass(), frame=Frame((x, h + 1, z)), bindings=(Binding(self.terrain, 'planting', 'plant'),)))
        return p


@dataclass(frozen=True)
class FacadeGarden:
    width: int
    depth: int
    sides: tuple[str, ...]

    def capability(self):
        return Capability('facade.garden', ('decoration', 'plant'), adaptations=('avoid facade openings',), guarantees=('unobstructed doorways',))

    def negotiate(self, ctx, parameters):
        return Contract(Box((-1, 0, -1), (self.width, 3, self.depth)))

    def realize(self, ctx, contract):
        p = Plan()
        w, d = self.width, self.depth
        frames = {'north': Frame((0, 0, -1)), 'south': Frame((w - 1, 0, d), 2), 'east': Frame((w, 0, 0), 1), 'west': Frame((-1, 0, d - 1), 3)}
        for side in self.sides:
            f = frames[side]
            length = w if side in ('north', 'south') else d
            for y in range(1, 4):
                q = f.point((1, y, 0))
                if name_of(ctx.view.state(ctx.world(q))) == 'air':
                    p.block(q, leaves('oak'), 'climber')
            for x in (2, length - 3):
                q = f.point((x, 0, 0))
                above = f.point((x, 1, 0))
                if name_of(ctx.view.state(ctx.world(q))) == 'air' and name_of(ctx.view.state(ctx.world(above))) == 'air':
                    p.block(q, slab('spruce', 'top'), 'flower-shelf')
                    p.block(above, state_of('potted_poppy' if x == 2 else 'potted_fern'), 'pot')
        return p


@dataclass(frozen=True)
class Well:

    def capability(self):
        return Capability('square.well', ('amenity', 'composite'), guarantees=('contained water', 'grounded cover'))

    def negotiate(self, ctx, parameters):
        basin = tuple((x, 1, z) for x in range(1, 3) for z in range(1, 3))
        return Contract(Box((-1, 0, -1), (4, 6, 4)), rules=(Rule('support', tuple((x, -1, z) for x in range(4) for z in range(4))), Rule('pool', basin), Rule('support', tuple((x, y, z) for x, z in ((0, 0), (3, 3)) for y in range(2, 6)))))

    def realize(self, ctx, contract):
        p = Plan()
        p.fill(Box((0, 0, 0), (3, 1, 3)), state_of('stone_bricks'), 'basin')
        p.fill(Box((1, 1, 1), (2, 1, 2)), state_of('water', level='0'), 'water')
        for x, z in ((0, 0), (3, 3)):
            p.fill(Box((x, 2, z), (x, 5, z)), log(), 'post')
        for x in range(-1, 5):
            for z in range(-1, 5):
                p.block((x, 5 if x in (-1, 4) else 6, z), slab('dark_oak'), 'shelter')
        return p


@dataclass(frozen=True)
class Terrace:
    width: int = 10
    depth: int = 10

    def capability(self):
        return Capability('site.terrace', ('siteworks', 'composite'), adaptations=('small local fill', 'retaining border'), guarantees=('level civic amenity pad',))

    def negotiate(self, ctx, parameters):
        heights = [ctx.view.elevation(ctx.world((x, 0, z))[0], ctx.world((x, 0, z))[2]) - ctx.frame.origin[1] for x in range(self.width) for z in range(self.depth)]
        top = max(heights)
        if top - min(heights) > 3:
            raise ContractError('terrace-relief', ctx.path, conditions='Small terrace limit is 3')
        region = Box((0, top, 0), (self.width - 1, top + 2, self.depth - 1))
        return Contract(Box((0, min(heights), 0), (self.width - 1, top + 8, self.depth - 1)), ports=(Port('entrance', 'public-access', Frame((0, top + 1, self.depth // 2)), region, 10, 'paving'),), grants=(Grant('paving', region, ('surface',), ('path',), 32),), decisions={'height': top, 'fill': sum(top - h for h in heights), 'excavation': 0})

    def realize(self, ctx, contract):
        p = Plan()
        top = contract.decisions['height']
        for x in range(self.width):
            for z in range(self.depth):
                wx, _, wz = ctx.world((x, 0, z))
                bed = ctx.view.elevation(wx, wz) - ctx.frame.origin[1]
                for y in range(bed, top + 1):
                    material = 'cobblestone' if y < top else ('stone_bricks' if x in (0, self.width - 1) or z in (0, self.depth - 1) else 'andesite')
                    p.block((x, y, z), state_of(material), 'terrace-paving')
        p.children.append(Child('well', Well(), frame=Frame((3, top + 1, 3))))
        for i, x in enumerate((1, self.width - 2)):
            p.children.append(Child(f'planter-{i}', Planter(2), frame=Frame((x if i == 0 else x - 1, top + 1, 1))))
        return p
