# Functional furnishing stations arranged around protected circulation in polygon interiors.
from dataclasses import dataclass
from hearth import Box, Frame, ContractError
from hearth.kernel import Capability, Contract, Domain, Port, Rule, Plan, Child
from hearth.blocks import state_of, stairs, passable
from .geometry import Polygon, perimeter, bounds2

FUNCTION_GROUPS = {'library': [['bookshelf'], ['lectern']], 'workshop': [['crafting_table'], ['smithing_table']], 'kitchen': [['smoker'], ['furnace']], 'bedroom': [['red_bed']], 'living': [['spruce_stairs']], 'storage': [['barrel']]}


@dataclass(frozen=True)
class WorkBay:
    purpose: str

    def capability(self):
        return Capability('extension.work-bay', ('furniture', self.purpose), offers=('interaction',), inputs={'purpose': Domain(choices=tuple(FUNCTION_GROUPS))}, guarantees=('functionally furnished', 'supported', 'clear interaction space'))

    def negotiate(self, ctx, parameters):
        interaction = ((0, 0, 2), (1, 0, 2))
        support = tuple((x, -1, z) for x in range(2) for z in range(3))
        clear = tuple((x, y, 2) for x in range(2) for y in range(2))
        area = Box((0, 0, 0), (1, 1, 1))
        return Contract(Box((0, -1, 0), (1, 1, 2)), ports=(Port('use', 'interaction', Frame(interaction[0]), Box(interaction[0], (1, 1, 2)), 4),), rules=(Rule('support', support), Rule('clear', clear), Rule('furnishing', box=area, data={'groups': FUNCTION_GROUPS[self.purpose]}, phase='complete')), atomic_object=True)

    def realize(self, ctx, contract):
        plan = Plan()
        barrel = state_of('barrel', facing='south', open='false')
        inventory = {'id': 'minecraft:barrel', 'Items': [{'Slot': 0, 'id': 'minecraft:bread', 'count': 6}, {'Slot': 1, 'id': 'minecraft:torch', 'count': 12}]}
        plan.block((1, 0, 0), barrel, 'supplies', nbt=inventory)
        if self.purpose == 'library':
            for y in range(2):
                plan.block((0, y, 0), state_of('bookshelf'), 'shelving')
            plan.block((0, 0, 1), state_of('lectern', facing='south', has_book='false', powered='false'), 'reading')
            plan.block((1, 0, 1), stairs('spruce', 'north'), 'seat')
        elif self.purpose in ('workshop', 'kitchen'):
            a, b = ('crafting_table', 'smithing_table') if self.purpose == 'workshop' else ('smoker', 'furnace')
            properties = {'facing': 'south', 'lit': 'false'} if self.purpose == 'kitchen' else {}
            plan.block((0, 0, 0), state_of(a, **properties), 'workstation')
            plan.block((0, 0, 1), state_of(b, **properties), 'workstation')
            plan.block((1, 0, 1), stairs('spruce', 'north'), 'seat')
        elif self.purpose == 'bedroom':
            for z, part in ((0, 'head'), (1, 'foot')):
                plan.block((0, 0, z), state_of('red_bed', facing='north', part=part, occupied='false'), 'bed')
            plan.block((1, 0, 1), state_of('red_carpet'), 'bedside')
        elif self.purpose == 'living':
            plan.block((0, 0, 0), stairs('spruce', 'south'), 'seat')
            plan.block((1, 0, 1), stairs('spruce', 'north'), 'seat')
            plan.block((0, 0, 1), state_of('spruce_fence', north='false', south='false', east='false', west='false', waterlogged='false'), 'table')
            plan.block((0, 1, 1), state_of('spruce_pressure_plate', powered='false'), 'table')
        else:
            for x, y, z in ((0, 0, 0), (0, 1, 0), (0, 0, 1)):
                plan.block((x, y, z), barrel, 'storage', nbt=inventory)
        return plan


@dataclass(frozen=True)
class HallInterior:
    footprint: Polygon
    purpose: str
    minimum: int = 2
    maximum: int = 8
    area_per_bay: int = 42

    def capability(self):
        return Capability('extension.hall-interior', ('interior', 'furniture', 'composite'), inputs={'purpose': Domain(choices=tuple(FUNCTION_GROUPS)), 'minimum': Domain(1, 8), 'maximum': Domain(1, 12), 'area_per_bay': Domain(16, 100)}, adaptations=('fit functional bays around a circulation cross',), guarantees=('accessible furnishing stations', 'protected three-cell central aisles'))

    def negotiate(self, ctx, parameters):
        footprint = set(self.footprint.cells())
        inside = footprint - set(perimeter(footprint, diagonal=True))
        width = max(x for x, z in footprint) + 1
        depth = max(z for x, z in footprint) + 1
        cx, cz = width // 2, depth // 2
        if self.minimum > self.maximum:
            raise ContractError('interior-domain', ctx.path, conditions='Minimum exceeds maximum')
        aisle = {(x, z) for x, z in inside if abs(x - cx) <= 1 or abs(z - cz) <= 1}
        candidates = []
        rng = ctx.rng('planning')
        for x, z in sorted(inside):
            for turn in range(4):
                frame = Frame((x, 0, z), turn)
                cells = {(frame.point((u, 0, v))[0], frame.point((u, 0, v))[2]) for u in range(2) for v in range(3)}
                body = {(frame.point((u, 0, v))[0], frame.point((u, 0, v))[2]) for u in range(2) for v in range(2)}
                if not cells <= inside or body & aisle:
                    continue
                front = frame.point((0, 0, 2))
                if (front[0] - x) * (cx - x) + (front[2] - z) * (cz - z) <= 0:
                    continue
                if not all(passable(ctx.state((u, y, v))) for u, v in cells for y in range(2)):
                    continue
                quadrant = (x < cx, z < cz)
                candidates.append((frame, cells, body, quadrant, rng.random()))
        wanted = min(self.maximum, max(self.minimum, len(inside) // self.area_per_bay))
        selected = []
        used = set()
        while candidates and len(selected) < wanted:

            def score(item):
                frame, cells, body, quadrant, tie = item
                x, _, z = frame.origin
                separation = min((abs(x - v[0].origin[0]) + abs(z - v[0].origin[2]) for v in selected), default=width + depth)
                balance = sum(v[3] == quadrant for v in selected)
                return (balance, -separation, min(x, z, width - 1 - x, depth - 1 - z), tie, frame.origin, frame.turn)

            choice = min(candidates, key=score)
            selected.append(choice)
            used |= choice[1]
            candidates = [v for v in candidates if not v[1] & used and all(abs(v[0].origin[0] - old[0].origin[0]) + abs(v[0].origin[2] - old[0].origin[2]) >= 4 for old in selected)]
        if len(selected) < self.minimum:
            raise ContractError('interior-capacity', ctx.path, conditions=f'{len(selected)} bays fit; {self.minimum} required')
        targets = ((cx, 0, cz), *(v[0].point((0, 0, 2)) for v in selected))
        clear = tuple((x, y, z) for x, z in sorted(aisle) for y in (0, 1))
        envelope = bounds2(footprint, -1, 2)
        return Contract(envelope, rules=(Rule('clear', clear), Rule('route', targets, envelope, phase='complete')), decisions={'frames': [(v[0].origin, v[0].turn) for v in selected], 'requested_bays': wanted, 'placed_bays': len(selected), 'minimum_bays': self.minimum, 'omitted_optional': wanted - len(selected), 'purpose': self.purpose})

    def realize(self, ctx, contract):
        return Plan(children=[Child(f'bay-{i}', WorkBay(self.purpose), frame=Frame(tuple(origin), turn)) for i, (origin, turn) in enumerate(contract.decisions['frames'])])
