# Pitched roof surfaces and open porch attachments with explicit overlap policy.
from dataclasses import dataclass
from hearth.space import Box, Frame
from hearth.blocks import state_of, stairs, slab, log, name_of
from hearth.kernel import Capability, Contract, Grant, Port, Rule, Plan, Child
from .interior import Light
from .primitives import Window


@dataclass(frozen=True)
class Roof:
    width: int
    depth: int
    axis: str = 'z'
    material: str = 'deepslate_tile'
    steep: bool = True

    def capability(self):
        return Capability('roof.gable', ('roof', 'enclosure'), offers=('junction',), adaptations=('abut taller wall', 'merge shared eaves'), guarantees=('pitched weather skin',))

    def negotiate(self, ctx, parameters):
        span = self.width if self.axis == 'z' else self.depth
        height = (span + 2) // 2 + 2
        box = Box((-1, -1, -1), (self.width, height, self.depth))
        return Contract(box, (Port('junction', 'roof-seam', Frame(), box, 100, 'junction'),), (Grant('junction', box, ('join',), ('roof',), box.volume),), decisions={'axis': self.axis, 'span': span, 'steep': self.steep, 'material': self.material})

    def realize(self, ctx, contract):
        p = Plan()
        w, d = self.width, self.depth
        span = w if self.axis == 'z' else d
        length = d if self.axis == 'z' else w
        for cross in range(-1, span + 1):
            step = min(cross + 1, span - cross)
            y = step if self.steep else (step + 1) // 2
            face = ('east' if cross < span / 2 else 'west') if self.axis == 'z' else ('south' if cross < span / 2 else 'north')
            for along in range(-1, length + 1):
                x, z = (cross, along) if self.axis == 'z' else (along, cross)
                q = ctx.world((x, y, z))
                existing = name_of(ctx.view.state(q))
                if existing != 'air':
                    # Negotiated abutment is only to actual masonry/roof boundaries.
                    continue
                trim = along in (-1, length)
                material = 'dark_oak' if trim else self.material
                p.block((x, y, z), stairs(material, face), 'roof-course')
        for end in (0, length - 1):
            frame = Frame((0, 0, end)) if self.axis == 'z' else Frame((end, 0, span - 1), 3)
            p.children.append(Child(f'gable-{end}', Gable(span, self.steep), frame=frame))
        for cross in (-1, span):
            for along in range(length):
                x, z = (cross, along) if self.axis == 'z' else (along, cross)
                if name_of(ctx.view.state(ctx.world((x, -1, z)))) == 'air':
                    p.block((x, -1, z), slab('spruce', 'top'), 'eave')
        mid = span // 2
        peak = min(mid + 1, span - mid) if self.steep else (min(mid + 1, span - mid) + 1) // 2
        for along in range(-1, length + 1):
            x, z = (mid, along) if self.axis == 'z' else (along, mid)
            if name_of(ctx.view.state(ctx.world((x, peak + 1, z)))) == 'air':
                p.block((x, peak + 1, z), slab('dark_oak'), 'ridge')
        for along in (-1, length):
            x, z = (mid, along) if self.axis == 'z' else (along, mid)
            if name_of(ctx.view.state(ctx.world((x, peak + 2, z)))) == 'air':
                p.block((x, peak + 2, z), state_of('dark_oak_fence', north='false', south='false', east='false', west='false', waterlogged='false'), 'finial')
        return p


@dataclass(frozen=True)
class Porch:
    width: int
    depth: int = 3
    balcony: bool = False

    def capability(self):
        return Capability('attachment.porch', ('attachment', 'access', 'composite'), offers=('access',), guarantees=('sheltered access', 'supported canopy', 'usable upper terrace when requested'))

    def negotiate(self, ctx, parameters):
        if not 3 <= self.depth <= 5:
            from hearth import ContractError
            raise ContractError('porch-depth', ctx.path)
        center = self.width // 2
        clear = tuple((x, y, z) for x in range(center - 1, center + 2) for y in (1, 2) for z in range(-self.depth, 0))
        support = tuple((x, y, -self.depth) for x in (0, self.width - 1) for y in range(6)) + tuple((x, 5, 0) for x in (0, self.width - 1))
        rules = [Rule('clear', clear), Rule('support', support), Rule('route', ((center, 1, -1), (center, 1, -self.depth)), Box((0, 0, -self.depth), (self.width - 1, 3, 0)), phase='complete')]
        if self.balcony:
            guards = tuple((x, 7, -self.depth) for x in range(self.width)) + tuple((x, 7, z) for x in (0, self.width - 1) for z in range(-self.depth + 1, 0))
            rules += [Rule('expected', guards, data={'names': ['spruce_fence']}), Rule('support', tuple((x, 6, z) for x in range(self.width) for z in range(-self.depth, 0)))]
            rules += [Rule('clear', tuple((center, y, z) for y in (7, 8) for z in range(-self.depth + 1, 1))), Rule('route', ((center, 7, 0), (center, 7, -self.depth + 1)), Box((0, 6, -self.depth), (self.width - 1, 9, 0)), phase='complete')]
        return Contract(Box((0, 0, -self.depth), (self.width - 1, 9, -1)), rules=tuple(rules), decisions={'balcony': self.balcony, 'depth': self.depth, 'width': self.width})

    def realize(self, ctx, contract):
        p = Plan()
        w, d = self.width, self.depth
        p.fill(Box((0, 0, -d), (w - 1, 0, -1)), state_of('spruce_planks'), 'deck')
        for x in (0, w - 1):
            for z in (-d,):
                p.block((x, 1, z), state_of('stone_bricks'), 'post-shoe')
                p.fill(Box((x, 2, z), (x, 5, z)), log(), 'post')
        for x in range(w):
            p.block((x, 5, -d), log('spruce', 'x'), 'front-beam')
            if self.balcony:
                for z in range(-d, 0):
                    p.block((x, 6, z), state_of('spruce_planks'), 'balcony-deck')
            else:
                for z in range(-d, 0):
                    y = 6 + (z + d) // 3
                    p.block((x, y, z), stairs('deepslate_tile', 'south'), 'canopy')
        for x, facing in ((1, 'east'), (w - 2, 'west')):
            p.block((x, 4, -d), stairs('spruce', facing, half='top'), 'brace')
        rails = []
        for x in range(w):
            if abs(x - w // 2) > 1:
                rails.append((x, 1, -d))
        for x in (0, w - 1):
            rails.extend((x, 1, z) for z in range(-d + 1, -1))
        if self.balcony:
            rails.extend((x, 7, -d) for x in range(w))
            rails.extend((x, 7, z) for x in (0, w - 1) for z in range(-d + 1, 0))
        rail_points = set(rails)
        neighbors = {'north': (0, 0, -1), 'south': (0, 0, 1), 'east': (1, 0, 0), 'west': (-1, 0, 0)}
        for x, y, z in rails:
            if (x in (0, w - 1) and y == 1 and z in (-d, -1)):
                continue
            connections = {side: str((x + dx, y + dy, z + dz) in rail_points).lower() for side, (dx, dy, dz) in neighbors.items()}
            p.block((x, y, z), state_of('spruce_fence', waterlogged='false', **connections), 'balustrade')
        for x in (1, w - 2):
            p.children.append(Child('lantern-' + str(x), Light(), frame=Frame((x, 3, -d + 1))))
        if d >= 4:
            p.block((1, 1, -2), stairs('spruce', 'east'), 'porch-seat')
            p.block((w - 2, 1, -2), stairs('spruce', 'west'), 'porch-seat')
        return p


@dataclass(frozen=True)
class Gable:
    span: int
    steep: bool = True

    def capability(self):
        return Capability('wall.gable', ('wall', 'enclosure'), offers=('window',), adaptations=('abut existing upper wall',))

    def negotiate(self, ctx, parameters):
        height = (self.span + 1) // 2
        region = Box((1, 0, 0), (self.span - 2, height - 2, 0))
        box = Box((0, -1, -1), (self.span - 1, height - 1, 0))
        return Contract(box, (Port('infill', 'wall-installation', Frame((1, 0, 0)), region, 4, 'install', facts={'thickness': 1}),), (Grant('install', region, ('install',), ('window',), 30),))

    def realize(self, ctx, contract):
        p = Plan()
        for x in range(self.span):
            step = min(x + 1, self.span - x)
            y = step if self.steep else (step + 1) // 2
            for yy in range(y):
                if name_of(ctx.view.state(ctx.world((x, yy, 0)))) == 'air':
                    p.block((x, yy, 0), log('spruce') if x in (0, self.span // 2, self.span - 1) else state_of('smooth_sandstone'), 'gable-infill')
        available = {w.point for w in p.writes}
        at = (self.span // 2 - 1, 0, 0)
        if self.steep and all((at[0] + x, y, 0) in available for x in range(2) for y in (0, 1)) and all(name_of(ctx.view.state(ctx.world((at[0] + x, 1, -1)))) == 'air' for x in range(2)):
            from hearth.kernel import Binding
            p.children.append(Child('window', Window(2, 2, False), frame=Frame(at), bindings=(Binding(ctx.path, 'infill'),)))
        return p
