#!/usr/bin/env python3
# This file is part of Tryton.  The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import curses
import datetime as dt
import math
import os
import sys

from collections import defaultdict

DIR = os.path.abspath(os.path.normpath(os.path.join(__file__,
    '..', '..', 'trytond')))
if os.path.isdir(DIR):
    sys.path.insert(0, os.path.dirname(DIR))

import trytond.commandline as commandline
from trytond.config import config

parser = commandline.get_parser_stat()
options = parser.parse_args()
config.update_etc(options.configfile)

import trytond.status as status


def main(stdscr):
    global reverse
    stdscr.nodelay(1)
    reverse = True
    processes = {}
    status_pad = curses.newpad(1, 1)
    cache_pad = curses.newpad(1, 1)

    def refresh_status():
        now = dt.datetime.now()
        height, width = stdscr.getmaxyx()

        def expired(process):
            return process['expire'] > now

        def format_status(since, pid, request):
            since = str(dt.timedelta(seconds=int(since)))
            return f"{pid:>5} {since:>18} {request}"
        status_pad.clear()
        status = [format_status(*i) for i in sorted(
                ((msg['since'], p['id'], msg['request'])
                    for p in filter(expired, processes.values())
                    for msg in p['status']),
                reverse=reverse)]
        prow = min(len(status) + 1, height // 2)
        pcol = max(max(map(len, status), default=0), width)
        status_pad.resize(len(status) + 1, pcol + 1)
        for i, line in enumerate(status, 1):
            status_pad.addnstr(i, 0, line.ljust(pcol), pcol)
        status_pad.addnstr(
            0, 0, "{pid:>5} {since:>18} {request} ({n})".format(
                pid="pid",
                since="TIME" + ('↑' if reverse else '↓'),
                request="request",
                n=len(status),
                ).upper().ljust(pcol), pcol, curses.A_REVERSE)
        status_pad.noutrefresh(0, 0, 0, 0, prow, width - 1)

        def ratio(cache):
            if cache['hit'] or cache['miss']:
                return cache['hit'] / (cache['hit'] + cache['miss'])
            return 0

        def format_cache(name, hit, miss, ratio, size):
            return f"{hit:{size}d} {miss:{size}d} {ratio * 100:6.2f} {name}"

        cache_pad.clear()
        cache_stats = defaultdict(lambda: defaultdict(lambda: 0))
        for p in filter(expired, processes.values()):
            for cache in p['caches']:
                stats = cache_stats[cache['name']]
                stats['name'] = cache['name']
                stats['hit'] += cache['hit']
                stats['miss'] += cache['miss']
        for cache in cache_stats.values():
            cache['ratio'] = ratio(cache)
        try:
            size = math.ceil(math.log10(
                    max(s['hit'] + s['miss'] for s in cache_stats.values())))
        except ValueError:
            size = 1
        size = max(size, 4)
        caches = [format_cache(size=size, **cache) for cache in sorted(
                cache_stats.values(), key=lambda c: (c['ratio'], c['miss']),
                reverse=reverse)]
        crow = max(len(caches) + 1, (height - prow))
        ccol = max(max(map(len, caches), default=0), width)
        cache_pad.resize(crow + 1, ccol + 1)
        for i, line in enumerate(caches, 1):
            cache_pad.addnstr(i, 0, line.ljust(ccol), ccol)
        cache_pad.addstr(
            0, 0,
            "{hit:>{size}} {miss:>{size}} {ratio:>6} {name} ({n})".format(
                size=size,
                hit="hit",
                miss="miss",
                ratio="% " + ('↑' if reverse else '↓'),
                name="name",
                n=len(caches),
                ).upper().ljust(ccol), curses.A_REVERSE)
        cache_pad.noutrefresh(0, 0, prow, 0, height - 1, width - 1)

    def refresh():
        global reverse
        refresh_status()
        stdscr.refresh()

        key = stdscr.getch()
        if key == ord('q'):
            sys.exit()
        elif key == ord('r'):
            reverse = not reverse
            refresh()

    def update(data=None):
        if data:
            pid = data['id']
            data['expire'] = dt.datetime.now() + dt.timedelta(seconds=10)
            processes[pid] = data
        refresh()
    refresh()
    status.listen(config.get('database', 'path'), update)


curses.wrapper(main)
