# Usable doors, lights and furnished rooms assembled as semantic children.
from dataclasses import dataclass
from hearth.space import Box, Frame
from hearth.blocks import state_of, stairs, slab, log
from hearth.kernel import Capability, Contract, Domain, Grant, Port, Rule, Plan, Child, Binding, ContractError
from .primitives import Volume, Wall, Window


@dataclass(frozen=True)
class Door:
    material: str = 'spruce'

    def capability(self):
        return Capability('opening.door', ('door', 'access'), guarantees=('paired halves', 'support', 'operable exit'))

    def negotiate(self, ctx, parameters):
        return Contract(Box((0, 0, 0), (0, 1, 0)), rules=(Rule('support', ((0, -1, 0),)),), atomic_object=True)

    def realize(self, ctx, contract):
        p = Plan()
        for y, half in enumerate(('lower', 'upper')):
            p.block((0, y, 0), state_of(self.material + '_door', facing='north', half=half, hinge='left', open='false', powered='false'))
        return p


@dataclass(frozen=True)
class Light:
    hanging: bool = True

    def capability(self):
        return Capability('light.lantern', ('light', 'room-light'), guarantees=('actual light source',))

    def negotiate(self, ctx, parameters):
        return Contract(Box((0, 0, 0), (0, 1, 0)), rules=(Rule('expected', ((0, 0, 0),), data={'names': ['lantern']}),))

    def realize(self, ctx, contract):
        p = Plan()
        p.block((0, 0, 0), state_of('lantern', hanging=str(self.hanging).lower(), waterlogged='false'))
        if self.hanging:
            p.block((0, 1, 0), state_of('chain', axis='y', waterlogged='false'))
        return p


@dataclass(frozen=True)
class Furniture:
    purpose: str = 'living'
    width: int = 11
    depth: int = 11

    def capability(self):
        return Capability('furniture.' + self.purpose, ('furniture', self.purpose), guarantees=('usable function',))

    def negotiate(self, ctx, parameters):
        if self.purpose not in ('living', 'bedroom', 'workshop', 'library', 'kitchen', 'storage'):
            raise ContractError('room-function', ctx.path, conditions=self.purpose)
        return Contract(Box((1, 0, 1), (self.width - 2, 2, self.depth - 2)))

    def realize(self, ctx, contract):
        p = Plan()
        w, d = self.width, self.depth
        inventory = {'id': 'minecraft:barrel', 'Items': [{'Slot': 0, 'id': 'minecraft:bread', 'count': 6}, {'Slot': 1, 'id': 'minecraft:torch', 'count': 12}]}
        p.block((1, 0, d - 2), state_of('barrel', facing='north', open='false'), 'supplies', nbt=inventory)
        if self.purpose == 'bedroom':
            for z, part in ((2, 'foot'), (3, 'head')):
                p.block((2, 0, z), state_of('red_bed', facing='south', part=part, occupied='false'), 'bed')
            p.block((2, 0, d - 3), state_of('bookshelf'))
        elif self.purpose == 'library':
            for x in (2, 3, w - 3):
                for y in range(2):
                    p.block((x, y, d - 2), state_of('bookshelf'))
            p.block((2, 0, 2), state_of('lectern', facing='south', has_book='false', powered='false'))
        elif self.purpose == 'workshop':
            p.block((2, 0, 2), state_of('crafting_table'))
            p.block((3, 0, 2), state_of('smithing_table'))
            p.block((2, 0, d - 3), state_of('anvil', facing='north'))
        elif self.purpose == 'kitchen':
            p.block((2, 0, 2), state_of('smoker', facing='south', lit='false'))
            p.block((3, 0, 2), state_of('furnace', facing='south', lit='false'))
            p.block((2, 0, d - 3), state_of('crafting_table'))
        else:
            for z in (2, 3):
                p.block((2, 0, z), stairs('spruce', 'east'), 'bench')
                p.block((w - 3, 0, d - 1 - z), stairs('spruce', 'west'), 'bench')
            p.block((3, 0, 2), state_of('spruce_fence', north='false', south='false', east='false', west='false', waterlogged='false'))
            p.block((3, 1, 2), state_of('spruce_pressure_plate', powered='false'), 'table')
        # Rugs avoid the central circulation cross and door landings.
        for x in (3, 4):
            for z in (d - 4, d - 3):
                p.block((x, 0, z), state_of('red_carpet'), 'rug')
        return p


@dataclass(frozen=True)
class Stair:
    rise: int = 6
    width: int = 2

    def capability(self):
        return Capability('circulation.stair', ('stairs', 'vertical-access'), guarantees=('walkable flight', 'two-block headroom'))

    def negotiate(self, ctx, parameters):
        points = tuple((x, i + 1, i) for i in range(self.rise) for x in range(self.width))
        clear = tuple((x, i + k, i) for i in range(self.rise) for x in range(self.width) for k in (1, 2, 3))
        support = tuple((x, i, i) for i in range(self.rise) for x in range(self.width))
        box = Box((0, 0, -1), (self.width - 1, self.rise + 2, self.rise))
        return Contract(box, rules=(Rule('support', support), Rule('clear', clear), Rule('route', points, box, phase='complete')))

    def realize(self, ctx, contract):
        p = Plan()
        for i in range(self.rise):
            for x in range(self.width):
                for y in range(max(0, i - 1), i):
                    p.block((x, y, i), state_of('spruce_planks'), 'stringer')
                p.block((x, i, i), stairs('spruce', 'south'), 'tread')
        return p


@dataclass(frozen=True)
class Room:
    purpose: str
    width: int = 11
    depth: int = 11
    doors: tuple[str, ...] = ('north',)
    windows: tuple[str, ...] = ('north', 'east', 'south', 'west')
    upper_open: bool = False
    stair_up: bool = False
    stair_down: bool = False
    window_component: object = None
    light_component: object = None

    def capability(self):
        return Capability('room.' + self.purpose, ('room', 'habitable', 'composite'), offers=('access',), guarantees=('lit', 'furnished', 'reachable', 'enclosed'), adaptations=('facade grid', 'substitutable windows and lights'), inputs={'width': Domain(choices=(9, 11, 13)), 'depth': Domain(choices=(9, 11, 13)), 'purpose': Domain(choices=('living', 'bedroom', 'library', 'workshop', 'kitchen', 'storage'))}, locked=('purpose', 'width', 'depth', 'doors', 'stair_up', 'stair_down'))

    def negotiate(self, ctx, parameters):
        w, d = self.width, self.depth
        if w not in (9, 11, 13) or d not in (9, 11, 13):
            raise ContractError('room-domain', ctx.path)
        doors = {'north': (w // 2, 1, 0), 'south': (w // 2, 1, d - 1), 'west': (0, 1, d // 2), 'east': (w - 1, 1, d // 2)}
        points = [doors[k] for k in self.doors] + [(w // 2, 1, d // 2)]
        if not self.doors:
            points = [(w // 2, 1, d // 2)]
        interior = Box((1, 1, 1), (w - 2, 5, d - 2))
        boundary = []
        for x in range(w):
            for z in range(d):
                # Upper landing leaves only the required stair slot open.
                if not (self.stair_down and x in (w - 4, w - 3) and 2 <= z <= 7):
                    boundary.append((x, 0, z))
                if not (self.stair_up and x in (w - 4, w - 3) and 2 <= z <= 7):
                    boundary.append((x, 6, z))
        function = {'bedroom': [['red_bed'], ['barrel']], 'workshop': [['crafting_table'], ['smithing_table']], 'library': [['bookshelf'], ['lectern']], 'kitchen': [['smoker'], ['furnace']], 'living': [['spruce_stairs'], ['barrel']], 'storage': [['barrel']]}
        rules = (Rule('route', tuple(points), Box((0, 1, 0), (w - 1, 8, d - 1)), phase='complete'), Rule('light', tuple(points), Box((0, 1, 0), (w - 1, 5, d - 1)), {'minimum': 1}, 'complete'), Rule('furnishing', box=interior, data={'groups': function[self.purpose]}, phase='complete'), Rule('sealed', tuple(boundary), phase='complete'))
        ports = tuple(Port(side, 'access', Frame(p), Box(p, (p[0], p[1] + 1, p[2])), 10) for side, p in doors.items() if side in self.doors)
        return Contract(Box((-1, 0, -1), (w, 9, d)), ports=ports, rules=rules, decisions={'purpose': self.purpose, 'doors': self.doors, 'windows': self.windows, 'stair_up': self.stair_up, 'stair_down': self.stair_down})

    def realize(self, ctx, contract):
        w, d = self.width, self.depth
        p = Plan()
        floor = Plan()
        for x in range(w):
            for z in range(d):
                if not (self.stair_down and x in (w - 4, w - 3) and 2 <= z <= 7):
                    floor.block((x, 0, z), state_of('spruce_planks'))
        p.children.append(Child('floor', Pattern('floor', Box((0, 0, 0), (w - 1, 0, d - 1)), tuple(floor.writes))))
        if not self.upper_open:
            p.children.append(Child('ceiling', Volume(Box((0, 6, 0), (w - 1, 6, d - 1)), state_of('spruce_planks'), 'ceiling')))
        walls = {'north': (Frame((0, 1, 0)), w), 'south': (Frame((w - 1, 1, d - 1), 2), w), 'east': (Frame((w - 1, 1, 1), 1), d - 2), 'west': (Frame((0, 1, d - 2), 3), d - 2)}
        for side, (frame, length) in walls.items():
            wallpath = ctx.path + '/wall-' + side
            p.children.append(Child('wall-' + side, Wall(length, 5), frame=frame))
            if side in self.doors:
                at = frame.compose(Frame((length // 2, 0, 0)))
                p.children.append(Child('door-' + side, Door(), frame=at, bindings=(Binding(wallpath, 'infill'),)))
            if side in self.windows:
                bays = (2, length - 4) if length >= 11 else ((1, length - 3) if length >= 9 else ((length - 2) // 2,))
                for i, bay in enumerate(bays):
                    if side in self.doors and abs(bay - length // 2) < 2:
                        continue
                    at = frame.compose(Frame((bay, 2, 0)))
                    p.children.append(Child(f'window-{side}-{i}', self.window_component or Window(), frame=at, bindings=(Binding(wallpath, 'infill'),)))
        # Furnish the opposite half to the flight; clipping stays inside the furnishing component's declared domain.
        furniture = Furniture(self.purpose, w, d)
        if self.stair_up or self.stair_down:
            furniture = Furniture(self.purpose, w - 3, d)
        p.children.append(Child('furniture', furniture, frame=Frame((0, 1, 0))))
        for i, (x, z) in enumerate(((2, d - 3), (w - 3, 2))):
            if self.stair_up and i == 1:
                x = w // 2
            p.children.append(Child(f'light-{i}', self.light_component or Light(), frame=Frame((x, 4, z))))
        from .landscape import FacadeGarden
        p.children.append(Child('facade-garden', FacadeGarden(w, d, tuple(s for s in self.windows if s not in self.doors))))
        if self.stair_up:
            p.children.append(Child('stairs', Stair(), frame=Frame((w - 4, 1, 2))))
        return p


@dataclass(frozen=True)
class Pattern:
    role: str
    envelope: Box
    writes: tuple

    def capability(self):
        return Capability('surface.' + self.role, (self.role,))

    def negotiate(self, ctx, parameters):
        return Contract(self.envelope)

    def realize(self, ctx, contract):
        return Plan(writes=list(self.writes))
