# Backed roof unions, dormer height fields and structural root sweeps.
import math

from .materials import ROOFS, WALLS, DIRECTIONS, log
from .blocks import is_air


def surface(mass):
    """Resolve a gable or hip to a sealed height field with integrated dormer bumps."""
    s = mass.levels[-1]
    r = mass.roof
    rect = s.rect.inset(-r['eaves'])
    base = s.floor + s.height
    result = {}
    dormer_centers = []
    if r['dormers']:
        lo, hi = (s.rect.x0, s.rect.x1) if r['ridge'] == 'x' else (s.rect.z0, s.rect.z1)
        dormer_centers = [lo + round((hi - lo) * f) for f in ([.5] if r['dormers'] == 1 else [.27, .73])]
    for x, z in sorted(rect.cells()):
        dx = min(x - rect.x0, rect.x1 - x)
        dz = min(z - rect.z0, rect.z1 - z)
        cross = dz if r['ridge'] == 'x' else dx
        run = min(dx, dz) if r['form'] == 'hip' else cross
        h = base + math.floor(run * r['pitch'])
        face = ('south' if z < rect.center[1] else 'north') if r['ridge'] == 'x' else ('east' if x < rect.center[0] else 'west')
        verge = (x in (rect.x0, rect.x1)) if r['ridge'] == 'x' else (z in (rect.z0, rect.z1))
        if r['form'] == 'hip' and dx < dz:
            face = 'east' if x < rect.center[0] else 'west'
        if r['form'] == 'hip' and dz <= dx:
            face = 'south' if z < rect.center[1] else 'north'
        for center in dormer_centers:
            lateral = x if r['ridge'] == 'x' else z
            along = z if r['ridge'] == 'x' else x
            front = s.rect.z1 if r['ridge'] == 'x' else s.rect.x0
            into = front - along if r['ridge'] == 'x' else along - front
            if abs(lateral - center) <= 2 and -1 <= into <= 4:
                dh = base + 5 - abs(lateral - center)
                if dh > h:
                    h = dh
                    face = ('east' if x < center else 'west') if r['ridge'] == 'x' else ('south' if z < center else 'north')
                    verge = into == -1
        result[x, z] = dict(height=h, face=face, trim=verge, owner=mass.name)
    return result, dormer_centers


def build_roofs(ctx):
    """Merge roof surfaces before placement, then close gables and install crowns."""
    g, plan, mat = ctx.g, ctx.plan, ctx.plan.design.materials
    maps, centers = {}, {}
    merged = {}
    for mass in plan.masses:
        if mass.roof is None:
            continue
        maps[mass.name], centers[mass.name] = surface(mass)
        for p, value in maps[mass.name].items():
            if p not in merged or value['height'] > merged[p]['height']:
                merged[p] = value
    for mass in plan.masses:
        if mass.roof is None:
            continue
        s = mass.levels[-1]
        heights = maps[mass.name]
        inner = ctx.inner[s.id]
        for x, z in sorted(s.cells):
            h = heights[x, z]['height']
            if (x, z) not in inner:
                for y in range(s.floor + s.height, h):
                    g.put(x, y, z, log(mat.frame) if x == s.rect.center[0] or z == s.rect.center[1] else g.rng.choice(WALLS[s.wall]), **({'axis': 'y'} if x == s.rect.center[0] or z == s.rect.center[1] else {}))
            elif s.attic:
                g.box((x, s.floor + 1, z, x, h - 1, z), 'air')
                ctx.interior.update((x, y, z) for y in range(s.floor + 1, h))
        # Dormer glazing sits in the actual continuous gable wall.
        for center in centers[mass.name]:
            if mass.roof['ridge'] == 'x':
                points = [(center + d, s.floor + s.height + y, s.rect.z1) for d in (-1, 1) for y in (1, 2)]
                side = 'south'
            else:
                points = [(s.rect.x0, s.floor + s.height + y, center + d) for d in (-1, 1) for y in (1, 2)]
                side = 'west'
            for p in points:
                g.put(*p, 'light_gray_stained_glass')
            g.metadata['openings'].append({'kind': 'window', 'owner': s.id, 'cells': points, 'side': side})
    backing, stair, slab = ROOFS[mat.roof]
    trim = ('smooth_sandstone', 'smooth_sandstone_stairs', 'smooth_sandstone_slab') if mat.trim == 'sandstone' else (mat.trim + '_planks', mat.trim + '_stairs', mat.trim + '_slab')
    for (x, z), v in sorted(merged.items()):
        h = v['height']
        # Eaves stop against taller inhabited volumes, sealing at their solid walls.
        if any(m.name != v['owner'] and any((x, z) in ctx.inner[s.id] and s.floor <= h + 1 <= s.floor + s.height for s in m.levels) for m in plan.masses):
            continue
        b, st, sl = trim if v['trim'] else (backing, stair, slab)
        neighbor = [merged[x + dx, z + dz]['height'] for dx, dz in DIRECTIONS.values() if (x + dx, z + dz) in merged]
        covered = [surface_map[x, z]['height'] for surface_map in maps.values() if (x, z) in surface_map]
        low = min(neighbor + covered, default=h)
        owner = next(m for m in plan.masses if m.name == v['owner'])
        if (x, z) in owner.levels[-1].cells:
            low = max(low, owner.levels[-1].floor + owner.levels[-1].height)
        # Solid vertical risers eliminate cracks even for the 3:2 profile.
        for y in range(low, h + 1):
            g.put(x, y, z, b)
        g.put(x, h, z, b)
        # Ridge caps use slabs. Other courses preserve a clear tile rhythm.
        if all(h >= v2 for v2 in neighbor) and sum(h == v2 for v2 in neighbor) >= 1:
            g.put(x, h + 1, z, sl, type='bottom')
        else:
            g.put(x, h + 1, z, st, facing=v['face'], half='bottom', shape='straight')
    ctx.roof_maps = maps
    for mass in plan.masses:
        if not mass.roof:
            continue
        s, roof = mass.levels[-1], mass.roof
        if roof['form'] == 'gable':
            # Paired upper gable lights fit under the actual backed slope.
            axis_x = roof['ridge'] == 'x'
            for end in (s.rect.x0, s.rect.x1) if axis_x else (s.rect.z0, s.rect.z1):
                for offset in (-2, 2):
                    x, z = (end, s.rect.center[1] + offset) if axis_x else (s.rect.center[0] + offset, end)
                    top = maps[mass.name][x, z]['height'] - 2
                    bottom = max(s.floor + s.height + 1, top - 2)
                    if top >= bottom:
                        for y in range(bottom, top + 1):
                            g.put(x, y, z, 'light_gray_stained_glass')
        # Corbels attach the thick eaves to the top wall beam.
        for x, z in sorted(s.cells - ctx.inner[s.id]):
            if (x + z) % 4:
                continue
            for side, (dx, dz) in DIRECTIONS.items():
                if (x + dx, z + dz) in s.cells:
                    continue
                y = s.floor + s.height - 1
                if is_air(g.get(x + dx, y, z + dz)):
                    g.stair(x + dx, y, z + dz, mat.floor, {'north': 'south', 'south': 'north', 'east': 'west', 'west': 'east'}[side], 'top')
    for mass in plan.masses:
        if mass.roof and mass.roof['cupola']:
            cupola(ctx, mass, maps[mass.name])


def cupola(ctx, mass, heights):
    """Build a sealed ridge lantern with an explicit non-occupied solid core."""
    g, mat = ctx.g, ctx.plan.design.materials
    cx, cz = mass.levels[-1].rect.center
    base = heights[cx, cz]['height'] + 1
    roofmat, stairs, slab = ROOFS[mat.roof]
    for x in range(cx - 2, cx + 3):
        for z in range(cz - 2, cz + 3):
            bottom = heights[x, z]['height']
            g.box((x, bottom, z, x, base, z), log(mat.frame) if x in (cx - 2, cx + 2) and z in (cz - 2, cz + 2) else 'calcite', **({'axis': 'y'} if x in (cx - 2, cx + 2) and z in (cz - 2, cz + 2) else {}))
            for y in range(base + 1, base + 4):
                edge = x in (cx - 2, cx + 2) or z in (cz - 2, cz + 2)
                corner = x in (cx - 2, cx + 2) and z in (cz - 2, cz + 2)
                g.put(x, y, z, log(mat.frame) if corner else 'glass' if edge else 'glowstone', **({'axis': 'y'} if corner else {}))
    for radius in (3, 2, 1, 0):
        y = base + 7 - radius
        for x in range(cx - radius, cx + radius + 1):
            for z in range(cz - radius, cz + radius + 1):
                g.put(x, y, z, roofmat)
                if radius and (x in (cx - radius, cx + radius) or z in (cz - radius, cz + radius)):
                    face = 'east' if x == cx - radius else 'west' if x == cx + radius else 'south' if z == cz - radius else 'north'
                    g.put(x, y + 1, z, stairs, facing=face)
    g.put(cx, base + 8, cz, 'lightning_rod', facing='up')


def sweep_root(g, points, radius=1):
    """Sweep connected oriented timber through an arbitrary polyline."""
    for a, b in zip(points, points[1:]):
        delta = [b[i] - a[i] for i in range(3)]
        length = max(abs(d) for d in delta)
        axis = 'xyz'[max(range(3), key=lambda i: abs(delta[i]))]
        for step in range(length + 1):
            center = [round(a[i] + delta[i] * step / max(1, length)) for i in range(3)]
            for dx in range(-radius, radius + 1):
                for dy in range(-radius, radius + 1):
                    for dz in range(-radius, radius + 1):
                        if abs(dx) + abs(dy) + abs(dz) <= radius * 2:
                            p = center[0] + dx, max(1, center[1] + dy), center[2] + dz
                            g.put(*p, 'stripped_dark_oak_log' if dx == 0 else 'dark_oak_log', axis=axis)


def root_envelope(ctx, mass):
    """Grow a mound, broad hollowable trunk and radial buttresses before carving rooms."""
    g = ctx.g
    r = mass.rect
    cx, cz = r.center
    spread = mass.roots['spread']
    crown = mass.roots['crown']
    top = mass.levels[-1].floor + mass.levels[-1].height + 2
    rx, rz = r.width / 2 + spread, r.depth / 2 + spread
    for x in range(math.floor(cx - rx), math.ceil(cx + rx) + 1):
        for z in range(math.floor(cz - rz), math.ceil(cz + rz) + 1):
            radial = ((x - cx) / rx)**2 + ((z - cz) / rz)**2
            if radial >= 1:
                continue
            h = 1 + round((top - 1) * (1 - radial)**.5)
            for y in range(2, h + 1):
                block = 'moss_block' if y == h else g.rng.choice(('tuff', 'andesite', 'mossy_cobblestone', 'dirt'))
                g.put(x, y, z, block)
    # The trunk is a tapered irregular solid, not a box with logs on its corners.
    for y in range(top - 4, top + crown):
        radius = max(2, 4 - max(0, y - top) // 2)
        for x in range(cx - radius, cx + radius + 1):
            for z in range(cz - radius, cz + radius + 1):
                if abs(x - cx) + abs(z - cz) <= radius + 1:
                    g.put(x, y, z, 'dark_oak_log', axis='y')
    count = 7 + (spread % 3)
    for i in range(count):
        angle = (i + .15) * math.tau / count
        dx, dz = math.cos(angle), math.sin(angle)
        # Skip the entrance sector; its two flanking roots remain wide apart.
        if dz > .8 and abs(dx) < .35:
            continue
        tipx, tipz = round(cx + dx * (rx - 1)), round(cz + dz * (rz - 1))
        points = [(cx + round(dx * 2), top + crown - 2, cz + round(dz * 2)), (cx + round(dx * (r.width / 2 - 1)), top - 2, cz + round(dz * (r.depth / 2 - 1))), (tipx, 3, tipz), (tipx + round(dx), 2, tipz + round(dz))]
        sweep_root(g, points)
    for sign, lift in ((-1, crown), (1, crown - 1)):
        sweep_root(g, [(cx + sign, top, cz), (cx + sign * 3, top + lift, cz), (cx + sign * 4, top + lift + 2, cz), (cx + sign * 2, top + lift + 2, cz)], 1)
    # Carving and the closed irregular room boundary are shared with ordinary envelopes.
