# Polygon halls, public facade installation and concentric hipped roof components.
from dataclasses import dataclass
from hearth import Box, Frame, Binding, ContractError
from hearth.kernel import Capability, Contract, Port, Grant, Rule, Plan, Child, Domain, Context
from hearth.blocks import state_of, log, slab, stairs
from hearth.adapters.site import survey, Foundation, Approach, SiteLimits
from hearth.components.interior import Door, Light
from hearth.components.primitives import Window
from .geometry import Polygon, CARDINAL, bounds2, perimeter
from .sites import edge_frames
from .circulation import Surface
from .interiors import HallInterior, FUNCTION_GROUPS


@dataclass(frozen=True)
class PolygonShell:
    footprint: Polygon
    height: int = 5
    entrances: tuple = ('north', 'south')

    def capability(self):
        return Capability('extension.polygon-shell', ('wall', 'enclosure'), offers=('wall-installation',), guarantees=('continuous enclosure', 'protected floor support'), inputs={'height': Domain(5, 7)})

    def negotiate(self, ctx, parameters):
        footprint = self.footprint.cells()
        shell = perimeter(footprint, diagonal=True)
        xs = [x for x, z in footprint]
        zs = [z for x, z in footprint]
        width, depth = max(xs) + 1, max(zs) + 1
        if min(xs) != 0 or min(zs) != 0:
            raise ContractError('polygon-origin', ctx.path, conditions='Normalize footprint minimum to (0,0)')
        frames = edge_frames(width, depth, 0)
        ports = []
        grants = []
        for side, f in frames.items():
            for role, offset, size, base in [('entry', 0, 1, 0)] if side in self.entrances else []:
                at = f.compose(Frame((offset, base, 0)))
                region = at.box(Box((0, 0, 0), (size - 1, 2, 0)))
                if not all((p[0], p[2]) in shell for p in region.cells()):
                    raise ContractError('polygon-entry', ctx.path, at.origin, 'Requested cardinal entry is not on the footprint boundary')
                key = 'entry-' + side
                ports.append(Port(key, 'wall-installation', at, region, 1, key, facts={'thickness': 1, 'role': role, 'side': side}))
                grants.append(Grant(key, region, ('install',), ('door',), 3))
            for i, offset in enumerate((-4, 3)):
                at = f.compose(Frame((offset, 1, 0)))
                region = at.box(Box((0, 0, 0), (1, 1, 0)))
                if all((p[0], p[2]) in shell for p in region.cells()):
                    key = f'window-{side}-{i}'
                    ports.append(Port(key, 'wall-installation', at, region, 1, key, facts={'thickness': 1, 'role': 'window', 'side': side}))
                    grants.append(Grant(key, region, ('install',), ('window',), 4))
        blocks = tuple((x, y, z) for x, z in shell for y in range(self.height))
        return Contract(bounds2(footprint, 0, self.height - 1), tuple(ports), tuple(grants), rules=(Rule('sealed', blocks), Rule('support', tuple((x, -1, z) for x, z in shell))), decisions={'shell': shell, 'height': self.height})

    def realize(self, ctx, contract):
        p = Plan()
        vertices = set(self.footprint.vertices)
        for x, z in contract.decisions['shell']:
            for y in range(self.height):
                frame = (x, z) in vertices or y == self.height - 1
                p.block((x, y, z), log() if frame else state_of('smooth_sandstone'), 'frame' if frame else 'infill')
        return p


@dataclass(frozen=True)
class Facade:
    host: str
    shutters: bool = True

    def capability(self):
        return Capability('extension.facade-installations', ('composite',))

    def negotiate(self, ctx, parameters):
        box = ctx.view.contract(self.host).envelope
        return Contract(Box.enclosing(ctx.local(p) for p in box.corners()).expand(2))

    def realize(self, ctx, contract):
        p = Plan()
        for port in ctx.view.offers(self.host, 'wall-installation'):
            component = Door() if port.facts['role'] == 'entry' else Window(shutters=self.shutters)
            frame = Frame(ctx.local(port.frame.origin), (port.frame.turn - ctx.frame.turn) % 4)
            p.children.append(Child(port.key, component, frame=frame, bindings=(Binding(self.host, port.key),)))
        return p


@dataclass(frozen=True)
class HipRoof:
    footprint: Polygon
    run: int = 2
    lantern: bool = False
    material: str = 'deepslate_tile'

    def capability(self):
        return Capability('extension.hip-roof', ('roof', 'composite'), adaptations=('polygon inset courses',), inputs={'run': Domain(choices=(1, 2, 3))}, guarantees=('continuous concentric weather surface',))

    def negotiate(self, ctx, parameters):
        points = set(self.footprint.cells())
        points |= {(x + dx, z + dz) for x, z in tuple(points) for dx, dz in CARDINAL}
        remaining = set(points)
        depths = {}
        depth = 0
        while remaining:
            ring = perimeter(remaining)
            for p in ring:
                depths[p] = depth
            remaining -= set(ring)
            depth += 1
        courses = tuple((x, depths[(x, z)] // self.run, z) for x, z in sorted(points))
        peak = max(y for x, y, z in courses)
        cx = (min(x for x, z in points) + max(x for x, z in points)) // 2
        cz = (min(z for x, z in points) + max(z for x, z in points)) // 2
        return Contract(bounds2(points, -1, peak + (15 if self.lantern else 7)), rules=(Rule('sealed', courses),), decisions={'courses': courses, 'peak': peak, 'center': (cx, cz), 'run': self.run, 'lantern': self.lantern})

    def realize(self, ctx, contract):
        p = Plan()
        d = contract.decisions
        heights = {(x, z): y for x, y, z in d['courses']}
        for x, y, z in d['courses']:
            face = next((f for dx, dz, f in ((1, 0, 'east'), (-1, 0, 'west'), (0, 1, 'south'), (0, -1, 'north')) if heights.get((x + dx, z + dz), y) > y), None)
            material = 'dark_oak' if y == 0 else self.material
            p.block((x, y, z), stairs(material, face) if face else slab(material, 'top'), 'hip-course')
        if self.lantern:
            cx, cz = d['center']
            p.children.append(Child('lantern', RoofLantern(), frame=Frame((cx - 2, d['peak'] + 1, cz - 2))))
        return p


@dataclass(frozen=True)
class RoofLantern:

    def capability(self):
        return Capability('extension.roof-lantern', ('roof', 'daylight', 'composite'))

    def negotiate(self, ctx, parameters):
        return Contract(Box((-1, -3, -1), (5, 13, 5)))

    def realize(self, ctx, contract):
        p = Plan()
        for x in range(5):
            for z in range(5):
                bed = ctx.elevation(x, z, maximum=0)
                if bed < -4:
                    raise ContractError('lantern-support', ctx.path, ctx.world((x, bed, z)))
                for y in range(bed + 1, 1):
                    p.block((x, y, z), state_of('spruce_planks'), 'ring-beam')
                if x in (0, 4) or z in (0, 4):
                    for y in (1, 2):
                        p.block((x, y, z), log() if x in (0, 4) and z in (0, 4) else state_of('glass'), 'clerestory')
        p.children.append(Child('cap', HipRoof(Polygon(((0, 0), (4, 0), (4, 4), (0, 4))), run=2), frame=Frame((0, 3, 0))))
        return p


@dataclass(frozen=True)
class Pavilion:
    footprint: Polygon
    terrain: str
    purpose: str = 'library'
    entrances: tuple = ('north', 'south')
    height: int = 5
    roof_run: int = 2
    lantern: bool = False
    limits: SiteLimits = SiteLimits()

    def capability(self):
        return Capability('extension.pavilion', ('building', 'habitable', 'composite'), offers=('access',), adaptations=('polygon support grid', 'independently measured approaches'), guarantees=('connected entrances', 'furnished lit enclosed hall'), inputs={'height': Domain(5, 7), 'purpose': Domain(choices=('living', 'library', 'workshop', 'kitchen', 'bedroom', 'storage'))}, locked=('footprint', 'purpose', 'entrances', 'height', 'roof_run', 'lantern'))

    def negotiate(self, ctx, parameters):
        points = self.footprint.cells()
        width = max(x for x, z in points) + 1
        depth = max(z for x, z in points) + 1
        if not 13 <= width <= 25 or not 13 <= depth <= 25 or not self.entrances:
            raise ContractError('hall-domain', ctx.path, conditions='Footprints 13..25, at least one declared entrance')
        measured = survey(ctx, points, self.limits)
        floor = measured['floor']
        ports = []
        approaches = []
        frames = edge_frames(width, depth, 0)
        low = min(v[0] for v in measured['samples'].values()) - 1
        roof = HipRoof(self.footprint, self.roof_run, self.lantern).negotiate(ctx, {})
        boxes = [bounds2(points, low, floor + self.height + 3).expand(3), Frame((0, floor + self.height + 2, 0)).box(roof.envelope)]
        targets = [(width // 2, floor + 1, depth // 2)]
        for side in self.entrances:
            if side not in frames:
                raise ContractError('hall-entrance', ctx.path, conditions=side)
            f = frames[side]
            comp = Approach(0, floor, limits=self.limits)
            ac = comp.negotiate(Context(ctx.path, ctx.frame.compose(f), ctx.scope.child('approach-' + side), ctx.view, (), ctx.limits), {})
            local = ac.transformed(f)
            port = local.ports[0]
            ports.append(Port(side, 'access', Frame(port.frame.origin, f.turn), port.region, 8, delegate=('approach-' + side, 'street')))
            approaches.append((side, f, comp))
            boxes.append(local.envelope)
            targets.extend(((f.origin[0], floor + 1, f.origin[2]), port.frame.origin))
        envelope = Box.enclosing(p for box in boxes for p in box.corners())
        interior = Box((1, floor + 1, 1), (width - 2, floor + self.height - 1, depth - 2))
        return Contract(envelope, tuple(ports), rules=(Rule('route', tuple(targets), envelope, phase='complete'), Rule('light', tuple(targets[:1]), interior, {'minimum': 1}, 'complete'), Rule('furnishing', box=interior, data={'groups': FUNCTION_GROUPS[self.purpose]}, phase='complete')), decisions={'floor': floor, 'width': width, 'depth': depth, 'footprint': points, 'vertices': self.footprint.vertices, 'strategy': measured['strategy'], 'entrances': self.entrances, 'height': self.height, 'purpose': self.purpose, 'roof_run': self.roof_run, 'lantern': self.lantern})

    def realize(self, ctx, contract):
        d = contract.decisions
        floor = d['floor']
        w = d['width']
        depth = d['depth']
        p = Plan(children=[Child('foundation', Foundation(tuple(d['footprint']), floor, d['strategy'], self.limits), bindings=(Binding(self.terrain, 'construction', 'fill'),)), Child('floor', Surface(tuple((x, floor, z) for x, z in d['footprint']), role='hall-floor')), Child('ceiling', Surface(tuple((x, floor + self.height + 1, z) for x, z in d['footprint']), role='hall-ceiling')), Child('shell', PolygonShell(self.footprint, self.height, self.entrances), frame=Frame((0, floor + 1, 0))), Child('facade', Facade(ctx.path + '/shell'))])
        for side in self.entrances:
            f = edge_frames(w, depth, 0)[side]
            p.children.append(Child('approach-' + side, Approach(0, floor, limits=self.limits), frame=f, bindings=(Binding(self.terrain, 'construction', 'surface'),)))
        p.children.append(Child('furnishings', HallInterior(self.footprint, self.purpose), frame=Frame((0, floor + 1, 0))))
        for x in range(4, w - 3, 5):
            for z in range(4, depth - 3, 5):
                p.children.append(Child(f'light-{x}-{z}', Light(), frame=Frame((x, floor + self.height - 1, z))))
        p.children.append(Child('roof', HipRoof(self.footprint, self.roof_run, self.lantern), frame=Frame((0, floor + self.height + 2, 0))))
        return p
