# Public furnishing policy with bounded actual-space fitting and protected circulation.
from dataclasses import dataclass
from hearth import Box, Frame, ContractError
from hearth.kernel import Capability, Contract, Domain, Rule, Plan, Child
from hearth.navigation import floor_path
from hearth.blocks import name_of
from .furnishing import FurnitureGroup, GROUPS
from .domestic import DomesticSuite
from .hospitality import DiningSet, Hearth, FloorTextiles


@dataclass(frozen=True)
class InteriorDesign:
    minimum_stations: int = 2
    maximum_stations: int = 5
    table_length: int = 4
    hearth: bool = True

    def component(self, purpose, width, depth, stair):
        return FittedInterior(purpose, width, depth, stair, self)


@dataclass(frozen=True)
class FittedInterior:
    purpose: str
    width: int
    depth: int
    stair: bool = False
    design: InteriorDesign = InteriorDesign()

    def capability(self):
        return Capability('furniture.fitted.' + self.purpose, ('furniture', 'composite'), inputs={'purpose': Domain(choices=tuple(GROUPS)), 'width': Domain(choices=(9, 11, 13)), 'depth': Domain(choices=(9, 11, 13)), 'design.minimum_stations': Domain(2, 5), 'design.maximum_stations': Domain(2, 6), 'design.table_length': Domain(2, 4)}, guarantees=('locked room function', 'reachable stations', 'protected circulation cross'), adaptations=('station orientation', 'compatible dining lengths', 'optional extra stations'))

    def units(self):
        primary = FurnitureGroup(self.purpose)
        suites = {'living': ('gathering', 'study'), 'bedroom': ('cabinet', 'study'), 'library': ('study', 'gathering'), 'kitchen': ('cooking', 'cabinet'), 'workshop': ('craft', 'cabinet'), 'storage': ('cabinet', 'craft')}
        choices = [primary]
        if self.design.hearth and self.purpose in ('living', 'library'):
            choices.append(Hearth())
        if self.purpose in ('living', 'kitchen', 'library'):
            choices.extend(DiningSet(n) for n in range(self.design.table_length, 1, -1))
        choices.extend(DomesticSuite(kind, 3) for kind in suites[self.purpose])
        choices.extend((FurnitureGroup('storage'), DomesticSuite(suites[self.purpose][0], 2)))
        return choices

    def negotiate(self, ctx, parameters):
        if self.design.minimum_stations > self.design.maximum_stations:
            raise ContractError('furnishing-count-domain', ctx.path)
        w, d = self.width, self.depth
        area = Box((1, 0, 1), (w - 2, 0, d - 2))
        center = (w // 2, 0, d // 2)
        stair_cells = {(x, y, z) for x in range(w - 4, w - 1) for z in range(1, d - 1) for y in range(3)} if self.stair else set()
        cross = {(x, y, z) for x in range(1, w - 1) for z in range(1, d - 1) if x == w // 2 or z == d // 2 for y in (0, 1)} - stair_cells
        clear, blocked, reserved = set(cross), set(stair_cells), set()
        placements, omitted = [], []
        dining_placed = False
        units = self.units()
        for index, component in enumerate(units):
            if len(placements) >= self.design.maximum_stations:
                omitted.append((index, 'station budget'))
                continue
            if 'seating' in component.capability().tags and dining_placed:
                omitted.append((index, 'compatible dining alternative already placed'))
                continue
            occupied, apron, target = component.placement_geometry()
            candidates = []
            rng = ctx.rng('station-' + str(index))
            for x in range(1, w - 1):
                for z in range(1, d - 1):
                    for turn in range(4):
                        frame = Frame((x, 0, z), turn)
                        body, use = frame.box(occupied), frame.box(apron)
                        if not all(area.contains((q[0], 0, q[2])) for q in (*body.corners(), *use.corners())):
                            continue
                        cells, use_cells = set(body.cells()), set(use.cells())
                        if cells & (blocked | clear | reserved) or use_cells & blocked:
                            continue
                        if any(name_of(ctx.state(q)) != 'air' for q in cells | use_cells):
                            continue
                        if not all(ctx.view.supports(ctx.world((q[0], -1, q[2]))) for q in cells | use_cells):
                            continue
                        wall_distance = min(body.lo[0] - 1, w - 2 - body.hi[0], body.lo[2] - 1, d - 2 - body.hi[2])
                        candidates.append((wall_distance, rng.random(), frame, cells, use_cells))
            selected = None
            for _, _, frame, cells, use_cells in sorted(candidates, key=lambda q: (q[0], q[1])):
                approach = frame.point(target)
                route = floor_path(ctx.view, ctx.frame, center, approach, area, blocked | cells)
                if route:
                    selected = (frame, cells, use_cells, approach, route)
                    break
            if selected is None:
                if index == 0:
                    raise ContractError('principal-furnishing-fit', ctx.path, ctx.world(center))
                omitted.append((index, 'no clear supported station and route'))
                continue
            frame, cells, use_cells, approach, route = selected
            placements.append((index, frame.origin, frame.turn, approach))
            blocked |= cells
            reserved |= use_cells
            clear |= {p for x, y, z in route for p in ((x, y, z), (x, y + 1, z))}
            dining_placed |= 'seating' in component.capability().tags
        if len(placements) < self.design.minimum_stations:
            raise ContractError('furnishing-count', ctx.path, ctx.world(center), f'Require {self.design.minimum_stations}, fitted {len(placements)}')
        targets = (center, *(row[3] for row in placements))
        box = Box((1, 0, 1), (w - 2, 3, d - 2))
        return Contract(box, rules=(Rule('clear', tuple(sorted(clear))), Rule('route', targets, box, phase='complete'), Rule('furnishing', box=box, data={'groups': GROUPS[self.purpose]}, phase='complete')), decisions={'purpose': self.purpose, 'placements': placements, 'stations': len(placements), 'omitted_optional_stations': omitted, 'textile_exclusions': sorted(stair_cells)})

    def realize(self, ctx, contract):
        p = Plan()
        units = self.units()
        for index, origin, turn, _ in contract.decisions['placements']:
            p.children.append(Child('station-' + str(index), units[index], frame=Frame(tuple(origin), turn)))
        centers = tuple(tuple(row[3]) for row in contract.decisions['placements'])
        p.children.append(Child('textiles', FloorTextiles(self.width, self.depth, centers, tuple(tuple(q) for q in contract.decisions['textile_exclusions']))))
        return p
