#!/usr/bin/env python3

import json
import os
from pathlib import Path

import numpy as np
from natsort import natsorted

try:
    from docopt import docopt
    from marko.ext.gfm import gfm
    import pygal
    from pygal.style import Style, DefaultStyle
except ImportError as e:
    raise Exception('Some external dependencies not found, install them using: pip install -r requirements.txt') from e


def render():
    suffix = '.avg.checks.bench.json'
    suites = {}
    for filepath in Path('').glob(f'*{suffix}'):
        name = filepath.name[:-len(suffix)]
        print(f'Loading {filepath} as {name}.')
        with open(filepath) as fp:
            suites[name] = json.load(fp)

    names = natsorted(suites.keys())

    figure_filenames = render_figures(names, suites)

    out_filename = Path('bench-results.md')
    with open(out_filename, 'w') as out:
        cwd = os.getcwd().split(os.sep)[-2:]
        print(f'# Benchmark of {", ".join(names)} in {cwd}', file=out)

        notes_file = Path('notes.md')
        if notes_file.exists():
            print(f'Including {notes_file} in resulting Markdown.')
            with notes_file.open() as fp:
                out.write(fp.read())
        else:
            print(f'File {notes_file} does not exist, create it to include it in resulting Markdown.')

        # print('## General Info & Checks', file=out)
        # render_checks(names, suites, out)

        print('## Graphs', file=out)
        print('*The graphs are interactive, view the rendered HTML locally to enjoy it.*\n', file=out)
        for filename in figure_filenames:
            # Use HTML instead of Markdown image to specify the width
            print(f'<img type="image/svg+xml" src="{filename}" alt="{filename}" width="49%"/>', file=out)

    print(f'Markdown output written to {out_filename}.')

    render_html(out_filename, Path('bench-results.html'))


def render_checks(names, suites, out):
    print(f'|Check|{"|".join(names)}|', file=out)
    print(f'|{"|".join(["---"] * (len(names) + 1))}|', file=out)

    per_impl_checks = {name: suite['checks'] for name, suite in suites.items()}
    check_names = sorted(set().union(*(checks.keys() for checks in per_impl_checks.values())))

    def sanitize(value):
        if type(value) is float:
            value = float(f'{value:.3g}')  # round to 3 significant figures
            return str(int(value) if value >= 100 else value)
        return str(value)

    for check_name in check_names:
        values = [sanitize(per_impl_checks[name].get(check_name)) for name in names]
        if len(values) > 1 and len(set(values)) > 1:
            values = [f'**{value}**' for value in values]
        print(f'|{check_name}|{"|".join(values)}|', file=out)


FIGURE_FUNCS = []


def figure(func):
    """Simple decorator to mark a function as a figure generator."""
    FIGURE_FUNCS.append(func)
    return func


def render_figures(names, suites):
    filenames = []

    config = pygal.Config(legend_at_bottom=True, style=DefaultStyle)

    for figure_func in FIGURE_FUNCS:
        chart = figure_func(names, suites, config.copy())
        filename = f'bench-results.{figure_func.__name__}.svg'
        chart.render_to_file(filename)
        filenames.append(filename)

    return filenames


@figure
def startup_time_figure(names, suites, config):
    all_vals = [suites[name]['startup_max'] for name in names]
    mx = np.max(all_vals)

    config.range = (0, mx + 0.1)
    chart = pygal.Bar(config, value_formatter=lambda x: "{:0.2f}s".format(x))
    chart.title = 'Czas uruchomienia (s)'

    for name in names:
        vals = [{'value': suites[name]['startup_avg'],
                 'ci': {'low': suites[name]['startup_min'], 'high': suites[name]['startup_max']}}]
        # print(vals)
        chart.add(name, vals)
    return chart


@figure
def errors_vs_connections_figure(names, suites, config):
    all_vals = [suites[name]['stats'] for name in names]
    flat = [item for sublist in all_vals for item in sublist]
    print(flat)
    all_rates = [
        div_or_none(s['request_errors_new_avg'], s['request_errors_new_avg'] + s['requests_new_avg'], scale=100) for s
        in flat]
    mx = np.max(all_rates)

    config.range = (0, mx + mx * 0.1)
    chart = pygal.Line(config, value_formatter=lambda x: "{:0.2f}%".format(x))
    chart.title = 'Współczynnik liczby błędów względem liczby połączeń (%)'
    connections_x_labels(chart, suites, skip=0)
    for name in names:
        chart.add(name, [
            div_or_none(s['request_errors_new_avg'], s['request_errors_new_avg'] + s['requests_new_avg'], scale=100)
            for s in suites[name]['stats'][0:]])
    return chart


@figure
def requests_vs_connections_figure(names, suites, config):
    vals = [[x['requests_per_s_avg'] for x in suites[name]['stats']] for name in names]
    print(vals)
    mx = np.max(vals)

    config.range = (0, mx + mx * 0.1)
    config.min_scale = 6
    chart = pygal.Line(config, value_formatter=lambda x: "{:0.0f}".format(x))
    chart.title = 'Liczba sukcesów na sekundę względem liczby połączeń (Zapytań/s)'
    connections_x_labels(chart, suites, skip=0)
    for name in names:
        # print(suites[name]['stats'])
        # vals = [{'value': x['requests_per_s_avg'], 'ci': {'low': x['requests_per_s_min'], 'high': x['requests_per_s_max']}} for x in suites[name]['stats']]
        vals = [{'value': x['requests_per_s_avg']} for x in suites[name]['stats']]
        chart.add(name, vals)
    return chart


@figure
def latency_vs_connections_50_figure(names, suites, config):
    return latency_vs_connections_figure(50, names, suites, config)


@figure
def latency_vs_connections_90_figure(names, suites, config):
    return latency_vs_connections_figure(90, names, suites, config)


@figure
def latency_vs_connections_99_figure(names, suites, config):
    return latency_vs_connections_figure(99, names, suites, config)


def latency_vs_connections_figure(percentile, names, suites, config):
    all_vals = [[s[f'latency_{percentile}p_ms_avg'] for s in suites[name]['stats'][0:]] for name in names]
    mx = np.max(all_vals)
    mn = np.min(all_vals)

    config.range = (mn - mn * .5, mx + mx * .5)
    chart = pygal.Line(config, logarithmic=True, value_formatter=lambda x: "{:0.0f}".format(x))
    chart.title = f'{percentile}. centyl czasu odpowiedzi względem liczby połączeń (ms)'
    connections_x_labels(chart, suites, skip=0)
    for name in names:
        chart.add(name, [s[f'latency_{percentile}p_ms_avg']
                         for s in suites[name]['stats'][0:]])
    return chart


@figure
def max_mem_usage_figure(names, suites, config):
    all_vals = [[s['mem_usage_mb_avg'] for s in suites[name]['stats']] for name in names]
    mx = np.max(all_vals)

    config.range = (0, mx + .1 * mx)
    chart = pygal.Line(config, value_formatter=lambda x: "{:0.0f}".format(x))
    chart.title = 'Maksymalne zużycie pamięci względem liczby połączeń (MiB)'
    connections_x_labels(chart, suites)
    for name in names:
        chart.add(name, [s['mem_usage_mb_avg'] for s in suites[name]['stats']])
    return chart


@figure
def max_mem_usage_per_requests_figure(names, suites, config):
    all_vals = [[div_or_none(s['mem_usage_mb_avg'], s['requests_per_s_avg']) for s in suites[name]['stats'][0:]] for name in names]
    mx = np.max(all_vals)

    config.range = (0, mx + .1 * mx)
    config.min_scale = 6
    chart = pygal.Line(config, value_formatter=lambda x: "{:0.3f}".format(x))
    chart.title = 'Maksymalne zużycie pamięci per liczba sukcesów na sekundę (MiB-sekunda/Zapytanie)'
    connections_x_labels(chart, suites, skip=0)
    for name in names:
        chart.add(name,
                  [div_or_none(s['mem_usage_mb_avg'], s['requests_per_s_avg']) for s in suites[name]['stats'][0:]])
    return chart


@figure
def cpu_figure(names, suites, config):

    mx = np.max([[s['cpu_new_s_avg'] for s in suites[name]['stats'][0:]] for name in names])
    config.range = (0, mx + mx * 0.1)
    chart = pygal.Line(config, value_formatter=lambda x: "{:0.3f}".format(x))
    chart.title = 'Wykorzystanie czasu procesora w czasie rundy testów (sekundy CPU)'
    connections_x_labels(chart, suites, skip=0)
    for name in names:
        chart.add(name, [s['cpu_new_s_avg'] for s in suites[name]['stats'][0:]])
    return chart


@figure
def cpu_per_request_figure(names, suites, config):
    mx = np.max([[div_or_none(s['cpu_new_s_avg'], s['requests_new_avg'], scale=1000) for s in
                         suites[name]['stats'][0:]] for name in names])
    config.range = (0, mx + mx * 0.1)

    chart = pygal.Line(config, value_formatter=lambda x: "{:0.3f}".format(x))
    chart.title = 'Wykorzystanie czasu procesora per poprawna odpowiedź (milisekundy CPU/Req)'
    connections_x_labels(chart, suites, skip=0)
    for name in names:
        chart.add(name, [div_or_none(s['cpu_new_s_avg'], s['requests_new_avg'], scale=1000) for s in
                         suites[name]['stats'][0:]])
    return chart


@figure
def cpu_vs_requests_figure(names, suites, config):
    all_vls = [[s['requests_total_avg'] for s in suites[name]['stats']] for name in names]
    mx = np.max(all_vls)
    config.range = (0, mx + mx * 0.1)
    config.min_scale = 6

    chart = pygal.XY(config, value_formatter=lambda x: "{:0.0f}".format(x), series_formatter=lambda x: "{:0.2f}".format(x))
    chart.title = 'Skumulowana liczba poprawnych odpowiedzi względem skumulowanego czasu CPU'
    chart.x_title = 'sekundy CPU'
    chart.y_title = 'skumulowana liczba poprawnych odpowiedzi'
    for name in names:
        chart.add(name, [
            {'value': (s['cpu_total_s_avg'], s['requests_total_avg']),
             'label': f'After {s["connections"]} connections round.'}
            for s in suites[name]['stats']
        ])
    return chart


def connections_x_labels(chart, suites, skip=0):
    chart.x_labels = [f"{s['connections']} conn's" if s['connections'] else s['message']
                      for s in next(iter(suites.values()))['stats']][skip:]
    chart.x_label_rotation = -30


def div_or_none(numerator, denominator, scale=1):
    if not denominator:
        return None
    return scale * numerator / denominator


HTML_PREFIX = '''<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Benchmark Report</title>
  </head>
  <body>
'''
HTML_SUFFIX = '''  </body>
</html>
'''


def render_html(md_file, html_file):
    with open(md_file) as in_fp, open(html_file, 'w') as out_fp:
        rs = in_fp.read()
        html = gfm(rs)
        # Replace <img> by <embed> for pygal interactivity, http://www.pygal.org/en/latest/documentation/web.html
        html = html.replace('<img', '<embed')
        # Replace link to md with link to .html for better browsability at HTML level.
        html = html.replace('/README.md">full benchmark', '/README.html">full benchmark')
        out_fp.write(HTML_PREFIX)
        out_fp.write(html)
        out_fp.write(HTML_SUFFIX)
    print(f'HTML output written to {html_file.resolve().as_uri()}.')


if __name__ == '__main__':
    # args = docopt(__doc__)
    render()

[((32, 19, 32, 43), 'pathlib.Path', 'Path', ({(32, 24, 32, 42): '"""bench-results.md"""'}, {}), "('bench-results.md')", False, 'from pathlib import Path\n'), ((91, 13, 91, 68), 'pygal.Config', 'pygal.Config', (), '', False, 'import pygal\n'), ((105, 9, 105, 25), 'numpy.max', 'np.max', ({(105, 16, 105, 24): 'all_vals'}, {}), '(all_vals)', True, 'import numpy as np\n'), ((127, 9, 127, 26), 'numpy.max', 'np.max', ({(127, 16, 127, 25): 'all_rates'}, {}), '(all_rates)', True, 'import numpy as np\n'), ((144, 9, 144, 21), 'numpy.max', 'np.max', ({(144, 16, 144, 20): 'vals'}, {}), '(vals)', True, 'import numpy as np\n'), ((176, 9, 176, 25), 'numpy.max', 'np.max', ({(176, 16, 176, 24): 'all_vals'}, {}), '(all_vals)', True, 'import numpy as np\n'), ((177, 9, 177, 25), 'numpy.min', 'np.min', ({(177, 16, 177, 24): 'all_vals'}, {}), '(all_vals)', True, 'import numpy as np\n'), ((192, 9, 192, 25), 'numpy.max', 'np.max', ({(192, 16, 192, 24): 'all_vals'}, {}), '(all_vals)', True, 'import numpy as np\n'), ((206, 9, 206, 25), 'numpy.max', 'np.max', ({(206, 16, 206, 24): 'all_vals'}, {}), '(all_vals)', True, 'import numpy as np\n'), ((222, 9, 222, 92), 'numpy.max', 'np.max', ({(222, 16, 222, 91): "[[s['cpu_new_s_avg'] for s in suites[name]['stats'][0:]] for name in names]"}, {}), "([[s['cpu_new_s_avg'] for s in suites[name]['stats'][0:]] for name in\n    names])", True, 'import numpy as np\n'), ((250, 9, 250, 24), 'numpy.max', 'np.max', ({(250, 16, 250, 23): 'all_vls'}, {}), '(all_vls)', True, 'import numpy as np\n'), ((37, 21, 37, 37), 'pathlib.Path', 'Path', ({(37, 26, 37, 36): '"""notes.md"""'}, {}), "('notes.md')", False, 'from pathlib import Path\n'), ((56, 30, 56, 56), 'pathlib.Path', 'Path', ({(56, 35, 56, 55): '"""bench-results.html"""'}, {}), "('bench-results.html')", False, 'from pathlib import Path\n'), ((295, 15, 295, 22), 'marko.ext.gfm.gfm', 'gfm', ({(295, 19, 295, 21): 'rs'}, {}), '(rs)', False, 'from marko.ext.gfm import gfm\n'), ((22, 20, 22, 28), 'pathlib.Path', 'Path', ({(22, 25, 22, 27): '""""""'}, {}), "('')", False, 'from pathlib import Path\n'), ((26, 27, 26, 40), 'json.load', 'json.load', ({(26, 37, 26, 39): 'fp'}, {}), '(fp)', False, 'import json\n'), ((34, 14, 34, 25), 'os.getcwd', 'os.getcwd', ({}, {}), '()', False, 'import os\n')]