# Lightweight high-level room-graph programs and settlement intent.
from hearth import Scene, Box, Frame
from hearth.randomness import Scope
from hearth.kernel import Capability, Contract, Plan, Child, Binding, Rule, Port
from hearth.environment import Terrain, gradient, wave, radial
from hearth.components.building import Blueprint, Building, RoomSpec, adapted
from hearth.components.landscape import PathNetwork, Landscape, LampPost, Terrace
from hearth.components.primitives import Window
from hearth.composition import encapsulate


def dwelling(seed):
    rng = Scope(seed).child('dwelling').stream('layout')
    rooms = [RoomSpec(0, 0, 0, 'living'), RoomSpec(0, 0, 1, 'bedroom')]
    if rng.choice((True, False)):
        rooms.append(RoomSpec(0, 1, 0, 'kitchen'))
    if rng.choice((True, False)):
        rooms.append(RoomSpec(1, 0, 0, 'library'))
    return Blueprint(tuple(rooms), bay=rng.choice((9, 11)), porch=True, roof_axis='auto', steep=True)


def atelier(seed):
    rng = Scope(seed).child('atelier').stream('layout')
    rooms = [RoomSpec(0, 0, 0, 'workshop'), RoomSpec(1, 0, 0, 'storage')]
    if rng.choice((True, False)):
        rooms.append(RoomSpec(1, 0, 1, 'bedroom'))
    if rng.choice((True, False)):
        rooms.append(RoomSpec(0, 1, 0, 'kitchen'))
    return Blueprint(tuple(rooms), bay=rng.choice((9, 11)), porch=rng.choice((True, False)), roof_axis='x', roof_material='brick', steep=rng.choice((True, False)), window_component=Window(shutters=False))


def commons(seed):
    rng = Scope(seed).child('commons').stream('layout')
    rooms = [RoomSpec(0, 0, 0, 'library'), RoomSpec(0, 1, 0, 'living'), RoomSpec(1, 1, 0, 'kitchen')]
    for level in range(1, rng.choice((2, 3))):
        rooms.append(RoomSpec(0, 1, level, 'bedroom' if level == 1 else 'library'))
    if rng.choice((True, False)):
        rooms.append(RoomSpec(1, 0, 0, 'storage'))
    return Blueprint(tuple(rooms), bay=rng.choice((9, 11)), porch=True, roof_axis='z')


PROGRAMS = {'dwelling': dwelling, 'atelier': atelier, 'commons': commons}


def building_scene(design, seed=0, field=None, water=None):
    design.validate()
    b = design.bay
    width = (max(r.x for r in design.rooms) + 1) * b
    depth = (max(r.z for r in design.rooms) + 1) * b
    domain = Box((-8, -4, -18), (width + 8, 64, depth + 8))
    scene = Scene(seed, domain=domain)
    expression = field if field is not None else gradient(base=4) + wave(.8, .1, .11, Scope(seed).stream('environment').random() * 6)
    scene.place('land', Terrain(domain, expression, water))
    # Encapsulation preserves the public building instance as the selected child.
    choice = adapted('building', design, '/land')
    scene.compose_choice(choice)
    return scene.finalize()


class Settlement:

    def __init__(self, designs, domain, terrain):
        self.designs = tuple(designs)
        self.domain = domain
        self.terrain = terrain

    def capability(self):
        return Capability('settlement.village', ('settlement', 'composite'), offers=('public-access',), guarantees=('building entrances connected',))

    def negotiate(self, ctx, parameters):
        gateway = (0, ctx.elevation(0, -12) + 1, -12)
        interface = Box((-1, gateway[1] - 1, -12), (1, gateway[1] + 1, -12))
        port = Port('public', 'public-access', Frame(gateway), interface, 10000, delegate=('connections', 'public'))
        return Contract(self.domain, ports=(port,), decisions={'buildings': len(self.designs), 'arrangement': 'opposed street with staggered frontage', 'circulation': ctx.scope.choose(('spine', 'loop'), 'circulation'), 'gateway': gateway})

    def realize(self, ctx, contract):
        p = Plan()
        for i, design in enumerate(self.designs):
            row = i // 2
            left = i % 2 == 0
            z = row * 38 + ctx.scope.child(f'plot-{i}').choose((-2, 0, 2), 'frontage')
            origin = (-16, 0, z - design.bay // 2) if left else (16, 0, z + design.bay // 2)
            p.children.append(adapted(f'plot-{i}', design, self.terrain, Frame(origin, 1 if left else 3)))
        p.children.append(Child('square', Terrace(), frame=Frame((5, 0, 10)), bindings=(Binding(self.terrain, 'construction', 'fill'),)))
        p.children.append(Child('connections', SettlementConnections(len(self.designs), self.domain, self.terrain, contract.decisions['circulation'], contract.decisions['gateway'])))
        p.children.append(Child('landscape', Landscape(self.domain, self.terrain, 8, 12, 0.045)))
        return p


class SettlementConnections:

    def __init__(self, count, domain, terrain, mode='spine', gateway=None):
        self.count = count
        self.domain = domain
        self.terrain = terrain
        self.mode = mode
        self.gateway = gateway

    def capability(self):
        return Capability('settlement.connections', ('connection', 'composite'))

    def negotiate(self, ctx, parameters):
        parent = ctx.path.rsplit('/', 1)[0]
        targets = tuple(ctx.local(ctx.view.port(f'{parent}/plot-{i}', 'street').frame.origin) for i in range(self.count))
        targets = targets + (ctx.local(ctx.view.port(parent + '/square', 'entrance').frame.origin), self.gateway)
        interface = Box((self.gateway[0] - 1, self.gateway[1] - 1, self.gateway[2]), (self.gateway[0] + 1, self.gateway[1] + 1, self.gateway[2]))
        port = Port('public', 'public-access', Frame(self.gateway), interface, 10000, delegate=('paths', 'public'))
        return Contract(self.domain, ports=(port,), rules=(Rule('route', targets, self.domain, phase='complete'),))

    def realize(self, ctx, contract):
        parent = ctx.path.rsplit('/', 1)[0]
        points = []
        links = []
        bindings = [Binding(self.terrain, 'construction', 'surface')]
        for i in range(self.count):
            host = f'{parent}/plot-{i}'
            port = ctx.view.port(host, 'street')
            x, y, z = ctx.local(port.frame.origin)
            lane = (-3 if x < 0 else 3) if self.mode == 'loop' else 0
            points.extend(((x, y, z), (lane, ctx.elevation(lane, z) + 1, z)))
            links.append((2 * i, 2 * i + 1))
            bindings.append(Binding(host, 'street', 'surface'))
            if i and self.mode == 'spine':
                links.append((2 * i - 1, 2 * i + 1))
        if self.mode == 'loop':
            low = min(q[2] for q in points)
            high = max(q[2] for q in points)
            base = len(points)
            for x, z in ((-3, low), (-3, high), (3, high), (3, low)):
                points.append((x, ctx.elevation(x, z) + 1, z))
            links.extend((base + i, base + (i + 1) % 4) for i in range(4))
        square = ctx.local(ctx.view.port(parent + '/square', 'entrance').frame.origin)
        lane = 3 if self.mode == 'loop' else 0
        points.extend((square, (lane, ctx.elevation(lane, square[2]) + 1, square[2])))
        links.append((len(points) - 2, len(points) - 1))
        if self.mode == 'spine':
            links.append((1, len(points) - 1))
        points.append(self.gateway)
        links.append((1, len(points) - 1))
        bindings.append(Binding(parent + '/square', 'entrance', 'surface'))
        p = Plan(children=[Child('paths', PathNetwork(tuple(points), tuple(links), gateway=self.gateway), bindings=tuple(bindings))])
        for i in range(self.count):
            p.relations.append((ctx.path + '/paths', 'connects', f'{parent}/plot-{i}'))
        for i, z in enumerate(sorted({q[-1] for q in points[:self.count * 2]})):
            x = 5 if i % 2 else -5
            y = ctx.elevation(x, z + 4) + 1
            p.children.append(Child(f'lamp-{i}', LampPost(), frame=Frame((x, y, z + 4))))
        return p


def settlement_scene(seed=0, count=None):
    scope = Scope(seed)
    rng = scope.stream('settlement-layout')
    if count is None:
        count = rng.choice((3, 4, 5))
    if count < 1:
        raise ValueError('A settlement needs at least one building')
    designs = []
    for i in range(count):
        local = scope.child(f'building-{i}').stream('room-graph')
        rooms = [RoomSpec(0, 0, 0, local.choice(('living', 'workshop', 'library')))]
        for x, z in local.sample(((1, 0), (0, 1)), local.choice((0, 1, 2))):
            rooms.append(RoomSpec(x, z, 0, local.choice(('storage', 'kitchen', 'living'))))
        if local.choice((True, True, False)):
            column = local.choice(rooms)
            for level in range(1, local.choice((2, 2, 3))):
                rooms.append(RoomSpec(column.x, column.z, level, 'bedroom' if level == 1 else 'library'))
        designs.append(Blueprint(tuple(rooms), bay=local.choice((9, 11)), porch=local.choice((True, True, False)), roof_axis=local.choice(('auto', 'x', 'z')), roof_material=local.choice(('deepslate_tile', 'brick')), steep=local.choice((True, True, False))))
    rows = (count + 1) // 2
    domain = Box((-49, -4, -23), (49, 64, (rows - 1) * 38 + 28))
    field = gradient(.012, -.009, 4.8) + wave(.7, .075, .09, rng.random() * 6) + radial(-36, rows * 12, 26, 1.1)
    scene = Scene(seed, domain=domain)
    scene.place('land', Terrain(domain, field, edit_limit=20000))
    scene.place('village', Settlement(designs, domain, '/land'))
    return scene.finalize()
