"""MROI-01: worker-owned rule engines; no inference, network, pixels or persistence."""
from collections import Counter
from dataclasses import replace
import time

from app.contracts.visualization import FrameResult, VisualizationCommand
from app.multi_roi.model import Snapshot
from app.multi_roi.results import tag, merge


class RuleRuntime:
    def __init__(self, value, *, clock=time.monotonic):
        from app.intrusion import Intrusion, IntrusionConfig
        from app.loitering import Loitering, LoiteringConfig
        from app.fall_runtime import FallScenario, FallSettings
        from app.fight_runtime import FightScenario, FightSettings
        from app.fire_config import FireConfig
        from app.fire_scenario import Fire
        from app.crowd_config import CrowdConfig
        from app.crowd_scenario import Crowd
        from app.crowd_result import CrowdResultProcessor
        from app.event_cooldown import EventCooldown
        self.snapshot = Snapshot.restore(value)
        self.areas = {a.roi_id: a for a in self.snapshot.areas}
        self.engines, self.gates = {}, {}
        self.metrics = Counter()
        self._frame_key, self._event_count = None, 0
        for r in self.snapshot.rules:
            if not r.enabled:
                continue
            area = self.areas[r.roi_id]
            cfg = r.settings
            if r.scenario == 'INTRUSION':
                e = Intrusion(IntrusionConfig.parse(cfg), clock=clock, contains=area.contains)
            elif r.scenario == 'LOITERING':
                e = Loitering(LoiteringConfig.parse(cfg), clock=clock, contains=area.contains)
            elif r.scenario == 'FALL':
                e = FallScenario(FallSettings.parse(cfg), contains=area.contains)
            elif r.scenario == 'FIGHT':
                e = FightScenario(FightSettings.parse(cfg), contains=area.contains)
            elif r.scenario == 'FIRE':
                e = Fire(FireConfig.parse(cfg).scenario, clock=clock, contains=area.contains)
            else:
                e = CrowdResultProcessor(Crowd(CrowdConfig.parse(cfg), clock=clock, contains=area.contains),
                                         camera_id=self.snapshot.camera_id)
            gate = EventCooldown(r.cooldown, clock=clock)
            self.gates[r.rule_id] = gate
            target = getattr(e, 'engine', e)
            target.event_admission = lambda scenario, g=gate: self._allow(g, scenario)
            self.engines[r.rule_id] = e

    def _begin(self, frame):
        key = (frame.run_id, frame.run_generation, frame.frame_seq)
        if key != self._frame_key:
            self._frame_key, self._event_count = key, 0

    def _allow(self, gate, scenario):
        if self._event_count >= 900:
            self.metrics['frame_event_capacity'] += 1
            return False
        if not gate.allow(scenario):
            return False
        self._event_count += 1
        return True

    def selected(self, scenarios):
        return [r for r in self.snapshot.rules if r.enabled and r.scenario in scenarios]

    def person(self, frame, observations, poses, ids, selected, *, gap_before=False,
               pose_config=None, display_threshold=.5):
        from app.region_scenario import alerted, event_ids, members
        self._begin(frame)
        parts = []
        selected_obs = tuple(observations[i] for i in selected)
        selected_ids = tuple(ids[i] for i in selected)
        for rule in self.selected(('INTRUSION', 'LOITERING', 'FALL', 'FIGHT')):
            engine = self.engines[rule.rule_id]
            values = tuple(poses[i] for i in selected) if rule.scenario in ('FALL', 'FIGHT') else selected_obs
            if any(v is None for v in values):
                raise ValueError('Pose required by rule.')
            rows, emitted = engine.evaluate(frame, values, selected_ids, gap_before=gap_before)
            commands = []
            for observation, pose, tid, row in zip(selected_obs, (poses[i] for i in selected), selected_ids, rows):
                visible = any(d.inside and (observation.score > d.confidence if rule.scenario == 'INTRUSION'
                                            else observation.score >= d.confidence) for d in members(row))
                commands.append(VisualizationCommand(observation, 'person',
                    color='#FF3030' if alerted(row) else '#00CC66',
                    visible=visible, track_id=tid,
                    event_ids=event_ids(row), scenario=row, pose=pose))
            part = FrameResult(frame, tuple(commands), tuple(emitted), pose_config=pose_config,
                               camera_id=self.snapshot.camera_id)
            parts.append(tag(part, rule, self.areas[rule.roi_id]))
        if not self.engines:
            commands = tuple(VisualizationCommand(observations[i], 'person',
                visible=observations[i].score > display_threshold, track_id=ids[i], pose=poses[i])
                for i in selected)
            return FrameResult(frame, commands, camera_id=self.snapshot.camera_id)
        return merge(frame, parts, camera_id=self.snapshot.camera_id)

    def provider(self, kind, frame, observations, *, gap_before=False):
        self._begin(frame)
        parts = []
        for rule in self.selected((kind.upper(),)):
            engine = self.engines[rule.rule_id]
            if kind == 'fire':
                from app.fire_result import evaluate_result
                part = evaluate_result(engine, frame, observations,
                                       camera_id=self.snapshot.camera_id, gap_before=gap_before)
            else:
                part = engine.process(frame, observations, gap_before=gap_before)
            if part is not None:
                parts.append(tag(part, rule, self.areas[rule.roi_id]))
        return merge(frame, parts, camera_id=self.snapshot.camera_id)

    def status(self):
        return dict(rules=len(self.engines), counts=dict(self.metrics),
                    cooldowns={str(k): g.status() for k, g in self.gates.items()})

    def close(self):
        for engine in self.engines.values():
            if hasattr(engine, 'close'):
                engine.close()
            else:
                engine.clear()
