# Read-only, domain-aware environmental observations.
from dataclasses import dataclass
from types import MappingProxyType
import copy
from hearth.blocks import AIR, solid, name_of, passable
from hearth.kernel.errors import ContractError
from hearth.space import Box


@dataclass(frozen=True)
class Observation:
    version: int
    region: Box
    elevation: int
    water: int | None
    material: str
    slope: int
    support_depth: int


class ReadTrace:
    """Coalesce actual environmental reads and interface dependencies for one candidate."""

    def __init__(self):
        self.low = None
        self.high = None
        self.sources = {}

    def point(self, p):
        if self.low is None:
            self.low = list(p)
            self.high = list(p)
        else:
            for i in range(3):
                self.low[i] = min(self.low[i], p[i])
                self.high[i] = max(self.high[i], p[i])

    def region(self, box):
        self.point(box.lo)
        self.point(box.hi)

    @property
    def bounds(self):
        return None if self.low is None else Box(tuple(self.low), tuple(self.high))


class View:

    def __init__(self, scene, trace=None):
        self.__scene = scene
        self.__trace = trace

    @property
    def version(self):
        return self.__scene.version

    @property
    def domain(self):
        return self.__scene.domain

    @property
    def style(self):
        return MappingProxyType(self.__scene.style)

    def preference(self, name, point):
        if self.domain and not self.domain.contains(point):
            raise ContractError("unknown-space", position=point)
        field = self.__scene.preferences.get(name)
        return None if field is None else field(point[0], point[2])

    def state(self, p):
        p = tuple(p)
        if self.__trace:
            self.__trace.point(p)
        if self.domain and not self.domain.contains(p):
            raise ContractError('unknown-space', position=p, conditions='Outside explicit environment domain')
        record = self.__scene.blocks.get(p)
        return record.state if record else AIR

    def owner(self, p):
        if self.__trace:
            self.__trace.point(p)
        record = self.__scene.blocks.get(tuple(p))
        return record.owner if record else None

    def supports(self, p):
        return solid(self.state(p))

    def clear(self, box):
        return all(passable(self.state(p)) for p in box.cells())

    def elevation(self, x, z, maximum=None):
        if self.domain is None:
            raise ContractError('unknown-space', position=(x, 0, z), conditions='No environmental domain')
        for y in range(self.domain.hi[1] if maximum is None else min(maximum, self.domain.hi[1]), self.domain.lo[1] - 1, -1):
            if self.supports((x, y, z)):
                return y
        raise ContractError('missing-substrate', position=(x, self.domain.lo[1], z), conditions='No actual solid substrate')

    def water(self, x, z):
        if self.domain is None:
            raise ContractError('unknown-space', position=(x, 0, z))
        for y in range(self.domain.hi[1], self.domain.lo[1] - 1, -1):
            if name_of(self.state((x, y, z))) == 'water':
                return y
        return None

    def sample(self, x, z):
        y = self.elevation(x, z)
        heights = [self.elevation(x + dx, z + dz) for dx, dz in ((1, 0), (-1, 0), (0, 1), (0, -1)) if self.domain.contains((x + dx, y, z + dz))]
        depth = 0
        for yy in range(y, self.domain.lo[1] - 1, -1):
            if not self.supports((x, yy, z)):
                break
            depth += 1
        return Observation(self.version, Box((x, self.domain.lo[1], z), (x, self.domain.hi[1], z)), y, self.water(x, z), self.state((x, y, z)), max([abs(h - y) for h in heights] or [0]), depth)

    def port(self, host, key):
        node = self.__scene.nodes[host]
        for p in node.contract.ports:
            if p.key == key:
                if self.__trace:
                    self.__trace.sources[host] = p.region
                return copy.deepcopy(p)
        raise ContractError('missing-port', host, conditions=key)

    def cost(self, path, tag=None):
        paths = [key for key, n in self.__scene.nodes.items() if (key == path or key.startswith(path + '/')) and (tag is None or (n.capability and tag in n.capability.tags))]
        occupied = sum(len(self.__scene.reverse.get(key, ())) for key in paths)
        replaced = sum(len(self.__scene.displaced.get(key, ())) for key in paths)
        return {'occupied': occupied, 'replaced': replaced, 'instances': len(paths)}

    def offers(self, host, kind=None):
        return tuple(copy.deepcopy(p) for p in self.__scene.nodes[host].contract.ports if kind is None or p.kind == kind)

    def capability(self, host):
        return copy.deepcopy(self.__scene.nodes[host].capability)

    def resolved_port(self, host, key):
        seen = set()
        while True:
            if (host, key) in seen:
                raise ContractError('port-cycle', host, conditions=key)
            seen.add((host, key))
            port = self.port(host, key)
            if not port.delegate:
                return host, port
            child, key = port.delegate
            host = host + '/' + child

    def contract(self, path):
        if self.__trace:
            self.__trace.sources[path] = self.__scene.nodes[path].contract.envelope
        return copy.deepcopy(self.__scene.nodes[path].contract)

    def protected(self, box):
        if self.__trace:
            self.__trace.region(box)
        return any(r.kind in ('clear', 'protected') and any(box.contains(p) for p in r.cells) for n in self.__scene.nodes.values() for r in n.contract.rules)

    def obstacle_distance(self, p, radius=8):
        best = radius + 1
        for q in Box(tuple(v - radius for v in p), tuple(v + radius for v in p)).cells():
            if q in self.__scene.blocks and not passable(self.__scene.blocks[q].state):
                best = min(best, max(abs(a - b) for a, b in zip(p, q)))
        return best
