# Terrain, outdoor rooms, approaches and adaptable property features.
from .components import SITE_COMPONENTS
from .blocks import is_air, name, full_support
from .materials import DIRECTIONS, OPPOSITE, log
from .plan import edge_point


def terrain(ctx):
    """Create bounded land or shallow water before foundations and excavation."""
    g, p = ctx.g, ctx.plan
    site = p.design.site
    main = p.masses[0].rect
    for x, z in sorted(p.site.cells()):
        # Rounded cut corners keep the diorama edge quiet.
        if min(x - p.site.x0, p.site.x1 - x) + min(z - p.site.z0, p.site.z1 - z) < 2:
            continue
        h = 1
        if site.terrain == 'slope':
            h += min(site.rise, max(0, main.z1 - z + 2) // 2)
        g.put(x, 0, z, g.rng.choice(('stone', 'stone', 'andesite')))
        if site.terrain == 'water':
            edge = min(x - p.site.x0, p.site.x1 - x, z - p.site.z0, p.site.z1 - z)
            if edge < 2:
                h = site.water_depth
                g.box((x, 1, z, x, h - 1, z), 'dirt')
                g.put(x, h, z, 'grass_block')
            else:
                g.put(x, 0, z, g.rng.choice(('mud', 'clay', 'gravel')))
                g.box((x, 1, z, x, site.water_depth, z), 'water', level='0')
                h = 0
        else:
            g.box((x, 1, z, x, h - 1, z), 'dirt')
            finish = 'grass_block'
            if site.paving != 'garden' and p.site.inset(4).contains(x, z):
                finish = ('terracotta' if x % 5 < 2 and z % 5 < 2 else 'smooth_sandstone') if site.paving == 'courtyard' else g.rng.choice(('coarse_dirt', 'coarse_dirt', 'gravel'))
            g.put(x, h, z, finish)
        ctx.ground[x, z] = h


def column(ctx, x, z, top, owner, timber=True):
    """Take a post or pier to actual ground or waterbed, recording its load path."""
    g, mat = ctx.g, ctx.plan.design.materials
    bottom = ctx.ground.get((x, z), 0)
    block = log(mat.frame) if timber else 'stone_bricks'
    g.box((x, bottom, z, x, top, z), block, **({'axis': 'y'} if timber else {}))
    g.metadata['supports'].append({'owner': owner, 'bottom': (x, bottom, z), 'top': (x, top, z), 'kind': 'post' if timber else 'pier'})


def attachment(ctx, a):
    """Construct supported outdoor floors, open pergolas or striped market canopies."""
    g, mat = ctx.g, ctx.plan.design.materials
    r, y = a['rect'], a['floor']
    block = ('smooth_sandstone' if mat.wall == 'white' else 'stone_bricks') if a['kind'] == 'terrace' else mat.floor + '_planks'
    for x, z in sorted(r.cells()):
        g.put(x, y, z, block if (x + z) % 5 else (('terracotta' if mat.wall == 'white' else 'andesite') if a['kind'] == 'terrace' else log(mat.floor)))
    for x in (r.x0, r.x1):
        for z in (r.z0, r.z1):
            column(ctx, x, z, y, a['id'], a['kind'] != 'terrace')
    side = a['side']
    dx, dz = DIRECTIONS[side]
    cx, cz = a['door']
    # Keep a generous center gateway in the rail and an approach to the door.
    outer = edge_point(r, side)
    for x, z in sorted(r.cells()):
        if x not in (r.x0, r.x1) and z not in (r.z0, r.z1):
            continue
        on_wall = (side == 'south' and z == r.z0 or side == 'north' and z == r.z1 or side == 'east' and x == r.x0 or side == 'west' and x == r.x1)
        gate = abs((x - outer[0]) if dz else (z - outer[1])) <= 1
        if not on_wall and not gate:
            g.put(x, y + 1, z, mat.frame + '_fence')
    g.door(cx, y, cz, side, a['id'], 'warped' if mat.wall == 'white' else 'spruce')
    ctx.opening_route((cx, y + 1, cz), side, a['id'])
    centerline = []
    for step in range(1, max(r.width, r.depth) + 1):
        x, z = cx + dx * step, cz + dz * step
        if not r.contains(x, z):
            break
        g.box((x, y + 1, z, x, y + 2, z), 'air')
        centerline.append((x, y + 1, z))
    g.route(centerline, a['id'])
    a['target'] = centerline[len(centerline) // 2]
    if a['cover'] != 'open':
        beam = y + 5
        for x in (r.x0, r.x1):
            for z in (r.z0, r.z1):
                g.box((x, y + 1, z, x, beam, z), log(mat.frame), axis='y')
        for z in (r.z0, r.z1):
            g.box((r.x0, beam, z, r.x1, beam, z), log(mat.frame), axis='x')
        for x in (r.x0, r.x1):
            g.box((x, beam, r.z0, x, beam, r.z1), log(mat.frame), axis='z')
        if a['cover'] == 'pergola':
            for z in range(r.z0, r.z1 + 1, 2):
                g.box((r.x0, beam + 1, z, r.x1, beam + 1, z), mat.floor + '_slab', type='bottom')
        else:
            for x, z in sorted(r.cells()):
                g.put(x, beam, z, 'red_wool' if (x - r.x0) % 4 < 2 else 'white_wool')
        g.lantern(r.x0 + 1, beam - 1, r.z0, a['id'], True)
    else:
        g.put(r.x0, y + 1, r.z0, mat.frame + '_fence')
        g.lantern(r.x0, y + 2, r.z0, a['id'])
    # Domestic furniture at the edge, separated from the reserved entry axis.
    fx, fz = r.x0 + 1, r.z0 + 1
    if (fx, y + 1, fz) not in g.reservations:
        g.put(fx, y + 1, fz, 'barrel' if a['kind'] == 'market' else mat.floor + '_stairs', **({'facing': 'south'}))
        if a['kind'] == 'market':
            g.put(fx, y + 2, fz, 'melon')
            g.inventory((fx, y + 1, fz), [('emerald', 8), ('bread', 12)])
    g.metadata['features'].append({'owner': a['id'], 'kind': a['kind'], 'target': a['target']})


def approach(ctx):
    """Connect a dry start to the threshold with supported directional stairs."""
    g, p, mat = ctx.g, ctx.plan, ctx.plan.design.materials
    e = p.entrance
    x, z = e['point']
    floor, side = e['floor'], e['side']
    dx, dz = DIRECTIONS[side]
    first_tread = e['first_tread']
    ground_floor = e['landing_floor']
    length = e['approach_length']
    points = []
    for d in range(1, length + 1):
        px, pz = x + dx * d, z + dz * d
        uphill = ground_floor > floor
        if uphill:
            y = min(ground_floor, floor + max(0, d - first_tread))
            stepped = first_tread < d <= first_tread + ground_floor - floor
        else:
            y = max(ground_floor, floor - max(0, d - first_tread))
            stepped = d >= first_tread and y > ground_floor
        for w in (-1, 0, 1):
            xx, zz = px + (w if dz else 0), pz + (w if dx else 0)
            material = mat.floor + '_planks' if p.design.site.terrain == 'water' else 'stone_bricks'
            g.box((xx, 0, zz, xx, y, zz), material)
            if stepped:
                facing = side if uphill else OPPOSITE[side]
                g.stair(xx, y, zz, mat.floor if p.design.site.terrain == 'water' else 'stone_brick', facing)
                g.metadata['stairs'].append({'owner': 'approach', 'point': (xx, y, zz), 'facing': facing})
            # Clear the corridor through terrain and root buttresses before decoration.
            g.box((xx, y + 1, zz, xx, max(y + (3 if d > first_tread else 2), ctx.ground.get((xx, zz), 1) + 2), zz), 'air')
        points.append((px, y + 1, pz))
    g.route(points, 'approach')
    ctx.start = points[-1]
    g.metadata['start'] = ctx.start
    g.metadata['entrance'] = (x, floor + 1, z)


def approach_lighting(ctx):
    """Place public path lamps after path construction, outside all reserved passages."""
    g, p = ctx.g, ctx.plan
    sx, sy, sz = ctx.start
    candidates = [(sx + dx, sz + dz) for dx in range(-6, 7) for dz in range(-6, 7) if 2 <= abs(dx) + abs(dz) <= 6]
    candidates.sort(key=lambda q: (abs(q[0] - sx) + abs(q[1] - sz), q))
    installed = []
    for x, z in candidates:
        if not p.site.inset(1).contains(x, z) or any(abs(x - px) + abs(z - pz) < 4 for px, pz in installed):
            continue
        y = p.design.site.water_depth + 1 if p.design.site.terrain == 'water' else ctx.ground.get((x, z), 1)
        if abs(y + 2 - sy) > 4 or not g.free([(x, y + 1, z), (x, y + 2, z)]):
            continue
        if p.design.site.terrain == 'water':
            column(ctx, x, z, y, 'approach-light')
        elif not full_support(g.get(x, y, z)):
            continue
        g.put(x, y + 1, z, 'stone_brick_wall')
        g.lantern(x, y + 2, z, 'approach')
        installed.append((x, z))
        if len(installed) == 2:
            return
    if not installed:
        raise ValueError('no clear site edge for an approach lamp')


def feature(ctx, f):
    """Realize a reserved feature plot with contained water, supports and access."""
    g, p, mat = ctx.g, ctx.plan, ctx.plan.design.materials
    r, kind = f['rect'], f['kind']
    if kind in SITE_COMPONENTS:
        SITE_COMPONENTS[kind].realize(ctx, f)
        return
    cx, cz = r.center
    # All working plots get an engineered level platform on their terrain.
    y = max(ctx.ground.get(q, 1) for q in r.cells())
    if p.design.site.terrain == 'water':
        y = p.design.site.water_depth + 1
    elif kind == 'landing':
        # A river reach is excavated around the jetty, with a continuous gravel bed.
        y = 2
        for x, z in sorted(r.inset(-2).cells()):
            g.box((x, 1, z, x, max(3, ctx.ground.get((x, z), 1) + 2), z), 'air')
            g.put(x, 0, z, 'gravel')
            g.put(x, 1, z, 'water', level='0')
            ctx.ground[x, z] = 0
    if kind in ('landing',):
        block = mat.floor + '_planks'
    else:
        block = 'grass_block'
    for x, z in sorted(r.cells()):
        if kind != 'landing':
            g.box((x, 1, z, x, y - 1, z), 'dirt')
        g.put(x, y, z, block)
    if kind == 'pool':
        for x, z in sorted(r.cells()):
            edge = x in (r.x0, r.x1) or z in (r.z0, r.z1)
            g.put(x, y, z, 'smooth_quartz' if edge else 'water', **({} if edge else {'level': '0'}))
            if not edge:
                g.put(x, y - 1, z, 'prismarine_bricks')
        # Two loungers occupy the dry coping edge.
        for x in (r.x0 + 1, r.x1 - 1):
            g.stair(x, y + 1, r.z1, 'birch', 'north')
    elif kind == 'field':
        for x, z in sorted(r.inset(1).cells()):
            if x == cx:
                g.put(x, y, z, 'water', level='0')
            else:
                g.put(x, y, z, 'farmland', moisture='7')
                g.put(x, y + 1, z, 'wheat', age=str(g.rng.choice((5, 7, 7, 7))))
        for x, z in sorted(r.cells() - r.inset(1).cells()):
            if (x, z) != (cx, r.z1):
                g.put(x, y + 1, z, 'stone_brick_slab', type='bottom')
    elif kind == 'well':
        g.box((cx - 1, y, cz - 1, cx + 1, y + 1, cz + 1), 'stone_bricks')
        g.put(cx, y + 1, cz, 'water', level='0')
        for x in (cx - 2, cx + 2):
            for z in (cz - 2, cz + 2):
                g.box((x, y + 1, z, x, y + 5, z), log(mat.frame), axis='y')
        for x in range(cx - 3, cx + 4):
            for z in range(cz - 3, cz + 4):
                g.put(x, y + 6, z, mat.floor + '_slab', type='bottom')
        g.box((cx - 2, y + 4, cz, cx + 2, y + 4, cz), log(mat.frame), axis='x')
        g.put(cx, y + 3, cz, 'chain', axis='y')
    elif kind == 'windmill':
        g.box((cx - 1, y + 1, cz - 1, cx + 1, y + 5, cz + 1), 'stone_bricks')
        g.box((cx, y + 6, cz, cx, y + 11, cz), log(mat.frame), axis='y')
        g.box((cx - 1, y + 11, cz - 1, cx + 1, y + 11, cz + 1), mat.floor + '_slab', type='bottom')
        sy, sz = y + 8, cz + 2
        g.put(cx, sy, cz + 1, log(mat.frame), axis='z')
        for step in range(-4, 5):
            g.put(cx + step, sy, sz, log(mat.frame), axis='x')
            g.put(cx, sy + step, sz, log(mat.frame), axis='y')
        for d in (2, 3, 4):
            for width in (1,):
                for xx, yy in ((cx + width, sy + d), (cx + d, sy - width), (cx - width, sy - d), (cx - d, sy + width)):
                    g.put(xx, yy, sz, 'oak_trapdoor', facing='south', open='true')
        g.put(cx + 1, y + 1, cz + 2, 'grindstone', face='floor', facing='south')
    elif kind == 'landing':
        for x in (r.x0, r.x1):
            for z in (r.z0, r.z1):
                column(ctx, x, z, y + 1, f['id'])
        g.put(r.x1, y + 2, r.z1, 'lantern', hanging='false')
        g.put(r.x0 + 1, y + 1, r.z0 + 1, 'barrel')
        g.inventory((r.x0 + 1, y + 1, r.z0 + 1), [('oak_boat', 1), ('fishing_rod', 1), ('lead', 2)])
        # A low mooring arm has visible water beside the bollard.
        for x in range(r.x1 + 1, r.x1 + 3):
            g.put(x, y, r.z1, mat.floor + '_planks')
        column(ctx, r.x1 + 2, r.z1, y + 1, f['id'])
    else:
        for x, z in sorted(r.inset(1).cells()):
            if (x + z) % 2 == 0:
                g.put(x, y + 1, z, g.rng.choice(('allium', 'cornflower', 'oxeye_daisy')))
    f['target'] = (cx, y + 1, r.z1 + 1)
    f['floor'] = y
    g.metadata['features'].append({'owner': f['id'], 'kind': kind, 'target': f['target']})
    # Lit working edge outside the entrance into the feature.
    g.put(r.x0, y + 1, r.z1, 'stone_bricks')
    g.lantern(r.x0, y + 2, r.z1, f['id'])


def connect_features(ctx):
    """Route exterior walks around occupied footprints using a bounded grid search."""
    from collections import deque
    g, p = ctx.g, ctx.plan
    start = ctx.start[0], ctx.start[2]
    forbidden = set()
    for mass in p.masses:
        forbidden |= mass.rect.inset(-2 if mass.envelope == 'framed' else -mass.roots['spread']).cells()
    for a in p.attachments:
        forbidden |= a['rect'].cells()
    for f in p.features:
        forbidden |= f['rect'].cells()
    for route in g.metadata['routes']:
        forbidden |= {(x, z) for x, _, z in route['points']}
    forbidden |= {(s['point'][0], s['point'][2]) for s in g.metadata['stairs']}
    forbidden.discard(start)
    targets = [(f['target'], f['id']) for f in p.features]
    # Secondary upper terraces connect through their own doors; they need no ground stair.
    for target, owner in targets:
        target2 = target[0], target[2]
        forbidden.discard(target2)
        parents = {start: None}
        queue = deque([start])
        while queue and target2 not in parents:
            x, z = queue.popleft()
            for dx, dz in DIRECTIONS.values():
                q = x + dx, z + dz
                if q not in parents and q not in forbidden and p.site.contains(*q):
                    parents[q] = (x, z)
                    queue.append(q)
        if target2 not in parents:
            raise ValueError(f'{owner}: no clear exterior approach; change its side')
        path = []
        q = target2
        while q is not None:
            path.append(q)
            q = parents[q]
        path.reverse()
        # Paths use the entrance landing elevation. Terraced rises use a dedicated last run.
        start_y = ctx.start[1] - 1
        target_y = target[1] - 1
        difference = target_y - start_y
        if abs(difference) > len(path) - 2:
            raise ValueError(f'{owner}: path too short for terrain transition')
        points = []
        for i, (x, z) in enumerate(path):
            h = start_y
            remaining = len(path) - 1 - i
            if remaining < abs(difference):
                h += (1 if difference > 0 else -1) * (abs(difference) - remaining)
            g.box((x, 0, z, x, h - 1, z), 'stone_bricks' if p.design.site.terrain != 'water' else 'spruce_planks')
            g.put(x, h, z, 'coarse_dirt' if p.design.site.terrain != 'water' else 'spruce_planks')
            g.box((x, h + 1, z, x, max(h + 3, ctx.ground.get((x, z), 1) + 2), z), 'air')
            if difference > 0 and remaining < difference:
                px, pz = path[i - 1]
                facing = next(k for k, delta in DIRECTIONS.items() if delta == (x - px, z - pz))
                g.stair(x, h, z, 'stone_brick', facing)
            elif difference < 0 and remaining <= -difference and i + 1 < len(path):
                nx, nz = path[i + 1]
                facing = next(k for k, delta in DIRECTIONS.items() if delta == (x - nx, z - nz))
                g.stair(x, h, z, 'stone_brick', facing)
            points.append((x, h + 1, z))
        g.route(points, owner)


def planting(ctx):
    """Add restrained gardens and trees only in genuinely free site margins."""
    g, p = ctx.g, ctx.plan
    policy = p.design.site.planting
    forbidden = set()
    for m in p.masses:
        forbidden |= m.rect.inset(-3).cells()
    for a in p.attachments + p.features:
        forbidden |= a['rect'].inset(-2).cells()
    for route in g.metadata['routes']:
        for x, _, z in route['points']:
            forbidden |= {(x + dx, z + dz) for dx in range(-2, 3) for dz in range(-2, 3)}
    for light in g.metadata['lights']:
        x, _, z = light['point']
        forbidden |= {(x + dx, z + dz) for dx in range(-2, 3) for dz in range(-2, 3)}
    candidates = [(x, z) for x, z in sorted(p.site.inset(3).cells()) if (x, z) not in forbidden and name(g.get(x, ctx.ground.get((x, z), 0), z)) == 'grass_block']
    tree_count = 0 if policy == 'sparse' else 2
    g.rng.shuffle(candidates)
    trunks = []
    for x, z in candidates:
        if len(trunks) >= tree_count:
            break
        if any(abs(x - tx) + abs(z - tz) < 10 for tx, tz in trunks):
            continue
        if any((x + dx, z + dz) in forbidden for dx in range(-2, 3) for dz in range(-2, 3)):
            continue
        tree(ctx, x, z, policy)
        trunks.append((x, z))
    for x, z in sorted(p.site.inset(1).cells()):
        if (x, z) in forbidden:
            continue
        y = ctx.ground.get((x, z), 0) + 1
        if not is_air(g.get(x, y, z)):
            continue
        if name(g.get(x, y - 1, z)) == 'grass_block' and g.rng.random() < .12:
            g.put(x, y, z, g.rng.choice(('short_grass', 'short_grass', 'fern', 'cornflower', 'oxeye_daisy')))
        elif p.design.site.terrain == 'water' and name(g.get(x, p.design.site.water_depth, z)) == 'water' and g.rng.random() < .035:
            g.put(x, p.design.site.water_depth + 1, z, 'lily_pad')


def tree(ctx, x, z, policy):
    """Grow a small connected tree using the site's planting vocabulary."""
    g = ctx.g
    base = ctx.ground[x, z]
    height = g.rng.randint(9, 12) if policy == 'conifer' else g.rng.randint(7, 10) if policy == 'mediterranean' else g.rng.randint(5, 8)
    wood = 'jungle' if policy == 'mediterranean' else 'spruce' if policy == 'conifer' else 'oak'
    g.box((x, base + 1, z, x, base + height, z), wood + '_log', axis='y')
    leaves = set()
    if policy == 'mediterranean':
        for dx, dz in DIRECTIONS.values():
            for d in range(4):
                leaves.add((x + dx * d, base + height + (1 if d < 3 else 0), z + dz * d))
                if d == 3:
                    leaves.add((x + dx * d, base + height - 1, z + dz * d))
    elif policy == 'conifer':
        for level in range(3, height + 2):
            radius = max(0, (height - level) // 4 + (1 if level % 3 == 0 else 0))
            for dx in range(-radius, radius + 1):
                for dz in range(-radius, radius + 1):
                    if abs(dx) + abs(dz) <= radius + 1:
                        leaves.add((x + dx, base + level, z + dz))
    else:
        for dy in range(-3, 2):
            radius = max(0, 2 - abs(dy) // 2) if policy != 'conifer' else (2 if dy % 2 == 0 else 1)
            for dx in range(-radius, radius + 1):
                for dz in range(-radius, radius + 1):
                    if abs(dx) + abs(dz) <= radius + 1:
                        leaves.add((x + dx, base + height + dy, z + dz))
    for point in sorted(leaves):
        if is_air(g.get(*point)):
            g.put(*point, wood + '_leaves', persistent='true', distance='1')
