{# SPDX-License-Identifier: MIT #}
{# Copyright © 2021 André Santos #}

{##############################################################################}
{# RENDERED CODE #}
{##############################################################################}

# SPDX-License-Identifier: MIT
# Copyright © 2023 André Santos

###############################################################################
# Imports
###############################################################################

import asyncio
from collections import deque, namedtuple
from copy import deepcopy
from functools import partial
import json
from math import (
    acos,
    asin,
    atan,
    atan2,
    ceil,
    cos,
    degrees,
    e as E,
    floor,
    log,
    log10,
    pi as PI,
    radians,
    sin,
    sqrt,
    tan,
)
from threading import Event as ThreadingEvent, Lock, Thread

###############################################################################
# Constants and Data Structures
###############################################################################

INF = float("inf")
NAN = float("nan")

COMPACT = (',', ':')

MsgRecord = namedtuple('MsgRecord', ('topic', 'timestamp', 'msg'))

Verdict = namedtuple('Verdict', ('value', 'monitor', 'timestamp', 'witness'))


###############################################################################
# Helper Functions
###############################################################################


def noop(*args, **kwargs):
    pass


def prod(iterable):
    x = 1
    for y in iterable:
        x = x * y
        if x == 0:
            return 0
    return x


def _verdict_to_json(verdict):
    witness = _witness_to_json(verdict.witness)
    return {
        'value': verdict.value,
        'monitor': verdict.monitor,
        'timestamp': verdict.timestamp,
        'witness': witness,
    }

def _witness_to_json(witness):
    data = []
    for record in witness:
        data.append({
            'topic': record.topic,
            'timestamp': record.timestamp,
            'message': repr(getattr(record.msg, '__dict__', record.msg)),
        })
    return data


###############################################################################
# Monitor Classes
###############################################################################


{{ monitor_classes|join('\n\n\n') }}


###############################################################################
# Monitor Manager
###############################################################################


class HplMonitorManager:
    def __init__(self, success_cb=noop, failure_cb=noop):
        self.on_monitor_success = success_cb
        self.on_monitor_failure = failure_cb
        self.monitors = [
            {# -#}
        {% for cname in class_names %}
            {{ cname }}(),
        {% endfor %}
        ]
        n = len(self.monitors)
        for i in range(n):
            mon = self.monitors[i]
            mon.on_success = partial(self._on_success, i)
            mon.on_violation = partial(self._on_failure, i)
        self.live_server = LiveMonitoringServer()
        self.live_server.monitor_report = self.build_status_report()

    def launch(self, timestamp):
        for mon in self.monitors:
            mon.on_launch(timestamp)

    def shutdown(self, timestamp):
        # self.live_server.shutdown_requested.set()
        self.live_server.shutdown()
        for mon in self.monitors:
            mon.on_shutdown(timestamp)

    def on_timer(self, timestamp):
        for mon in self.monitors:
            mon.on_timer(timestamp)
    {# -#}
{% for topic, indices in callbacks.items() %}

    {% set cbname = 'on_msg_' ~ topic.replace('/', '_') %}
    def {{ cbname }}(self, msg, timestamp):
        {% for i in indices %}
        self.monitors[{{ i }}].{{ cbname }}(msg, timestamp)
        {% endfor %}
{% endfor %}

    def _on_success(self, i, timestamp, witness):
        mon = self.monitors[i]
        assert mon.verdict is True
        self.live_server.on_monitor_success(i, timestamp, witness)
        self.on_monitor_success(mon, timestamp, witness)

    def _on_failure(self, i, timestamp, witness):
        mon = self.monitors[i]
        assert mon.verdict is False
        self.live_server.on_monitor_failure(i, timestamp, witness)
        self.on_monitor_failure(mon, timestamp, witness)

    def build_status_report(self):
        report = []
        for mon in self.monitors:
            report.append({
                'id': mon.PROP_ID,
                'title': mon.PROP_TITLE,
                'property': mon.HPL_PROPERTY,
                'verdict': mon.verdict,
            })
        return report


###############################################################################
# Live Monitoring
###############################################################################

# This is meant to be running on an async loop.
# You might want to run this on a separate thread,
# rather than the one used to run HplMonitorManager,
# if the message feed is built on a synchronous interface.

# Use `HplMonitorManager.build_status_report` to initialize
# the `monitor_report` attribute.


class LiveMonitoringServer:
    def __init__(self, host='127.0.0.1', port=4242):
        self.host = host
        self.port = port
        self.monitor_report = []
        self.has_started = ThreadingEvent()
        # self.shutdown_requested = ThreadingEvent()
        self._lock = Lock()
        self._event_loop = None
        self._clients = []

    def start_thread(self, timeout: float = None) -> Thread:
        thread = Thread(target=self.run, name='live update server', daemon=True)
        thread.start()
        self.has_started.wait(timeout=timeout)
        return thread

    def shutdown(self):
        # to be called from outside the event loop thread
        with self._lock:
            if self._event_loop is not None:
                coro = self._cancel_all_tasks()
                asyncio.run_coroutine_threadsafe(coro, self._event_loop)

    async def _cancel_all_tasks(self):
        await asyncio.sleep(0)
        tasks = set(asyncio.all_tasks()) - {asyncio.current_task()}
        for task in tasks:
            task.cancel()
        await asyncio.gather(*tasks, return_exceptions=True)

    def run(self):
        # to be called from the dedicated thread
        # self.shutdown_requested.clear()
        self.has_started.set()
        try:
            asyncio.run(self._run_server())
        except asyncio.CancelledError:
            pass

    async def _run_server(self):
        with self._lock:
            self._event_loop = asyncio.get_event_loop()
        try:
            server = await asyncio.start_server(self._handle_client, self.host, self.port)
            async with server:
                await server.serve_forever()
                #while not self.shutdown_requested.is_set():
                #    await asyncio.sleep(1.0)
            await self._push_update(None)  # poison pill
        finally:
            with self._lock:
                self._event_loop = None

    async def _handle_client(self, reader, writer):
        client = LiveMonitoringClient(reader, writer)
        self._clients.append(client)
        with self._lock:
            report = deepcopy(self.monitor_report)
        # if not self.shutdown_requested.is_set():
        async with client:
            await client.send_initial_report(report)
            await client.run_loop()

    def on_monitor_success(self, i, timestamp, witness):
        # to be called from outside the event loop
        v = Verdict(True, i, timestamp, witness)
        w = _witness_to_json(witness)
        with self._lock:
            self.monitor_report[i]['verdict'] = True
            self.monitor_report[i]['witness'] = w
            if self._event_loop is not None:
                coro = self._push_update(v)
                asyncio.run_coroutine_threadsafe(coro, self._event_loop)

    def on_monitor_failure(self, i, timestamp, witness):
        # to be called from outside the event loop
        v = Verdict(False, i, timestamp, witness)
        w = _witness_to_json(witness)
        with self._lock:
            self.monitor_report[i]['verdict'] = False
            self.monitor_report[i]['witness'] = w
            if self._event_loop is not None:
                coro = self._push_update(v)
                asyncio.run_coroutine_threadsafe(coro, self._event_loop)

    async def _push_update(self, verdict):
        for client in self._clients:
            await client.verdict_queue.put(verdict)


class LiveMonitoringClient:
    def __init__(self, reader, writer):
        self.reader = reader
        self.writer = writer
        self.verdict_queue = asyncio.Queue()

    async def __aenter__(self):
        return

    async def __aexit__(self, *args):
        await self.close()

    async def close(self):
        self.writer.close()
        await self.writer.wait_closed()

    async def send_initial_report(self, report):
        data = json.dumps(report, separators=COMPACT) + '\n'
        self.writer.write(data.encode('utf8'))
        await self.writer.drain()

    async def run_loop(self):
        verdict = await self.verdict_queue.get()
        while verdict:
            data = _verdict_to_json(verdict)
            data = json.dumps(data, separators=COMPACT) + '\n'
            self.writer.write(data.encode('utf8'))
            await self.writer.drain()
            self.verdict_queue.task_done()
            verdict = await self.verdict_queue.get()
        self.verdict_queue.task_done()
