# Reusable campus graph composes arbitrary public building and circulation components.
from dataclasses import dataclass
from hearth import Box, Frame, ContractError
from hearth.kernel import Capability, Contract, Port, Rule, Plan, Child, Context
from .interfaces import Endpoint
from .circulation import connection
from .boundaries import BoundaryChain, IlluminateBoundary
from hearth.components.landscape import Landscape


@dataclass(frozen=True)
class Vertex:
    key: str
    component: object
    frame: Frame


@dataclass(frozen=True)
class Edge:
    a: str
    a_port: str
    b: str
    b_port: str
    covered: bool = False
    via: tuple = ()
    rise: int = 0
    width: int = 3


@dataclass(frozen=True)
class Campus:
    vertices: tuple[Vertex, ...]
    edges: tuple[Edge, ...]
    terrain: str
    envelope: Box
    boundary: object = None
    landscape: bool = True

    def capability(self):
        return Capability('extension.campus', ('settlement', 'composite'), offers=('public-access',), guarantees=('declared graph connected', 'usable realized building connections'), locked=('vertices', 'edges'))

    def negotiate(self, ctx, parameters):
        keys = {v.key for v in self.vertices}
        if len(keys) != len(self.vertices) or not keys:
            raise ContractError('graph-vertices', ctx.path)
        connected = {self.vertices[0].key}
        for edge in self.edges:
            if edge.a not in keys or edge.b not in keys or edge.a == edge.b:
                raise ContractError('graph-edge', ctx.path, conditions=str(edge))
        while True:
            more = {v for e in self.edges if e.a in connected or e.b in connected for v in (e.a, e.b)}
            if more <= connected:
                break
            connected |= more
        if connected != keys:
            raise ContractError('graph-disconnected', ctx.path, conditions=str(sorted(keys - connected)))
        first = self.vertices[0]
        context = Context(ctx.path + '/' + first.key, ctx.frame.compose(first.frame), ctx.scope.child(first.key), ctx.view, (), ctx.limits)
        offered = first.component.negotiate(context, {}).transformed(first.frame).ports
        port = next((p for p in offered if p.kind in ('access', 'public-access')), None)
        if port is None:
            raise ContractError('graph-access', ctx.path, conditions='First vertex must expose usable access')
        gateway = Port('public', 'access', port.frame, port.region, port.capacity, delegate=(first.key, port.key))
        return Contract(self.envelope, ports=(gateway,), decisions={'vertices': [v.key for v in self.vertices], 'edges': [(e.a, e.a_port, e.b, e.b_port, e.covered, e.width) for e in self.edges]})

    def realize(self, ctx, contract):
        p = Plan(children=[Child(v.key, v.component, frame=v.frame) for v in self.vertices])
        p.children.append(Child('circulation', Connections(ctx.path, self.edges, self.terrain, self.envelope)))
        if self.boundary is not None:
            p.children.extend((Child('boundary', BoundaryChain(self.boundary)), Child('boundary-lights', IlluminateBoundary(ctx.path + '/boundary', 2))))
        if self.landscape:
            p.children.append(Child('landscape', Landscape(self.envelope, self.terrain, 8, 12, 0.025)))
        return p


@dataclass(frozen=True)
class Connections:
    parent: str
    edges: tuple[Edge, ...]
    terrain: str
    envelope: Box

    def capability(self):
        return Capability('extension.connection-graph', ('connection', 'composite'))

    def negotiate(self, ctx, parameters):
        targets = []
        for edge in self.edges:
            for key, port in ((edge.a, edge.a_port), (edge.b, edge.b_port)):
                targets.append(ctx.local(ctx.view.port(self.parent + '/' + key, port).frame.origin))
        return Contract(self.envelope, rules=(Rule('route', tuple(targets), self.envelope, phase='complete'),))

    def realize(self, ctx, contract):
        p = Plan()
        for i, edge in enumerate(self.edges):
            a = Endpoint(self.parent + '/' + edge.a, edge.a_port)
            b = Endpoint(self.parent + '/' + edge.b, edge.b_port)
            p.children.append(connection(f'link-{i}', a, b, self.terrain, via=edge.via, covered=edge.covered, rise=edge.rise, width=edge.width))
        return p
