#!/usr/bin/env python3
"""Разбор кольцевой трассировки TX-тракта (файлы txtrace-*.txt).

Дамп пишет сама программа, запущенная с EWSDR_TXTRACE=1: кольцо сбрасывается
на диск в конце каждой передачи в каталог настроек (~/.config/ewsdr/).

Зачем: pcap показывает ПАУЗУ в потоке DUC, но не показывает, кто её сделал.
Здесь видно, кто именно не наполнил очередь: продюсер звука (TCI/звуковая
карта), TX-поток DSP или сетевой поток приёма.

Запуск: txtrace_scan.py <файл> [--context N]
"""

import sys
import statistics
from collections import defaultdict

# Подушка отправителя DUC: 2000 отсчётов @192 кГц (HPSDRNetwork.pas).
CUSHION_MS = 2000 / 192.0


def parse(path):
    rows = []
    for line in open(path, encoding='utf-8', errors='replace'):
        if line.startswith('#') or not line.strip():
            continue
        f = line.split()
        if len(f) != 5:
            continue
        try:
            rows.append((float(f[0]), f[1], int(f[2]), int(f[3]), int(f[4])))
        except ValueError:
            continue
    return rows


def segments(rows):
    """Кольцо накапливается через всю сессию: файл второй передачи содержит и
    первую. Режем по фронтам PTT — сравнивать можно только передачу с
    передачей."""
    segs, cur = [], None
    for r in rows:
        if r[1] == 'PTT' and r[2] == 1:
            cur = [r]
            segs.append(cur)
            continue
        if cur is not None:
            cur.append(r)
            if r[1] == 'PTT' and r[2] == 0:
                cur = None
    return segs if segs else [rows]


def inflight_tail(rows, seg):
    """Маркеры, оставшиеся в полёте на снятии PTT: планировщик пишет это
    (NOTE A=-3) на СЛЕДУЮЩЕМ тике, уже за границей передачи. Без учёта этой
    метки разбор считает их потерянными клиентом."""
    end = seg[-1][0]
    for r in rows:
        if r[0] < end or r[1] != 'NOTE' or r[2] != -3:
            continue
        if r[0] - end > 200:
            break
        return r[3]
    return 0


def stats(name, vals, unit='мкс'):
    if not vals:
        return f'  {name:<22} —'
    vals = sorted(vals)
    p99 = vals[min(len(vals) - 1, int(len(vals) * 0.99))]
    return (f'  {name:<22} n={len(vals):<6} медиана={statistics.median(vals):>9.1f} '
            f'p99={p99:>9.1f} макс={vals[-1]:>9.1f} {unit}')


def main():
    if len(sys.argv) < 2:
        print(__doc__)
        return 1
    path = sys.argv[1]
    ctx = 8
    if '--context' in sys.argv:
        ctx = int(sys.argv[sys.argv.index('--context') + 1])

    all_rows = parse(path)
    if not all_rows:
        print('пусто: в файле нет записей')
        return 1

    segs = segments(all_rows)
    print(f'=== {path}')
    print(f'записей {len(all_rows)}, передач в кольце {len(segs)}')
    for seg_no, seg in enumerate(segs, 1):
        report(seg, seg_no, ctx, inflight_tail(all_rows, seg))
    return 0


def report(rows, seg_no, ctx, tail_frames=0):
    by_kind = defaultdict(list)
    for r in rows:
        by_kind[r[1]].append(r)

    span = rows[-1][0] - rows[0][0]
    # Источник берём с ЗАКРЫВАЮЩЕГО фронта PTT: на открывающем он ещё старый
    # (SetMOX ставит метку до того, как ApplyMOX выберет источник).
    src_code = rows[0][3]
    for r in reversed(rows):
        if r[1] == 'PTT' and r[2] == 0:
            src_code = r[3]
            break
    src = {0: 'микрофон радио', 1: 'звуковая карта',
           2: 'внешняя подача (web/TCI)'}.get(src_code, '?')
    print(f'\n===== ПЕРЕДАЧА {seg_no}: {span/1000.0:.2f} с, источник — {src}, '
          f'подушка отправителя {CUSHION_MS:.2f} мс\n')

    print('РАСПРЕДЕЛЕНИЯ')
    print(stats('Synchronize (HP)', [r[2] for r in by_kind['HP-SYNC']]))
    print(stats('обработчик приёма', [r[2] for r in by_kind['HANDLER']]))
    print(stats('пауза в приёме', [r[2] for r in by_kind['RECV-GAP']]))
    print(stats('зазор тика TX', [r[2] for r in by_kind['TX-TICK']]))
    print(stats('fexchange0', [r[2] for r in by_kind['TX-PHASE']]))
    print(stats('TX-анализатор', [r[3] for r in by_kind['TX-PHASE']]))
    print(stats('отдача IQ', [r[4] for r in by_kind['TX-PHASE']]))
    print(stats('период TX_CHRONO', [r[4] for r in by_kind['CHRONO']]))
    print(stats('период TX-аудио', [r[2] for r in by_kind['TX-AUDIO']]))
    print(stats('простой очереди DUC', [r[2] for r in by_kind['DUC-RUN']]))

    # Продюсер: сколько блоков TX-поток делает за тик и как часто ринг пуст.
    ticks = by_kind['TX-TICK']
    if ticks:
        dry = sum(1 for t in ticks if t[4] == 0)
        burst = [t[4] for t in ticks if t[4] > 1]
        print(f'\n  тиков TX всего {len(ticks)}, из них вхолостую (ринг пуст) '
              f'{dry} ({100.0*dry/len(ticks):.1f}%), '
              f'пачками >1 блока {len(burst)}')

    # Учёт каналов: length в шапке против моно-отсчётов после интерполяции.
    audio = by_kind['TX-AUDIO']
    if audio:
        pairs = {(a[3], a[4]) for a in audio}
        print(f'  TX-аудио: пары (length, отсчётов после интерполяции) = '
              f'{sorted(pairs)[:6]}')

    # ★Баланс подачи. Просим по часам, а списываем долг при ОТПРАВКЕ маркера:
    # каждый маркер без ответа = безвозвратно потерянный кусок звука, наверстать
    # его нечем. Дефицит съедает единственный запас (аванс + pre-roll), и когда
    # запас кончился — очередь пустеет на каждой порции.
    chrono = by_kind['CHRONO']
    # Предварительный маркер (NOTE A=-4) уходит до фронта PTT, вне бухгалтерии
    # и вне статистики периода — но в баланс он входит наравне с прочими.
    prewarm = [n for n in by_kind['NOTE'] if n[2] == -4]
    if chrono and audio:
        asked = sum(c[2] for c in chrono) + sum(p[3] for p in prewarm)
        got = sum(a[4] for a in audio)
        secs = span / 1000.0
        print(f'\n  БАЛАНС ПОДАЧИ: маркеров {len(chrono) + len(prewarm)} '
              f'(из них предварительных {len(prewarm)}), порций {len(audio)}, '
              f'проглочено {len(chrono) + len(prewarm) - len(audio)}')
        print(f'    запрошено {asked} отсч., получено {got} '
              f'({got/secs:.0f} отсч/с при номинале 48000)')
        print(f'    дефицит {asked-got} отсч. = {(asked-got)/48.0:.1f} мс звука, '
              f'которого в эфире не будет')
        # NOTE несёт три разных записи: A>=0 — счётчик выброшенного очередью,
        # A=-1 — прощение зависшего кредита, A=-2 — выдача аванса,
        # A=-3 — сколько маркеров осталось В ПОЛЁТЕ на снятии PTT.
        notes = by_kind['NOTE']
        drops = [n for n in notes if n[2] >= 0]
        if drops:
            lost = drops[-1][2] - drops[0][2]
            print(f'    из них выброшено НАШЕЙ очередью к клиенту: {lost} блок(ов)')
        if tail_frames > 0:
            q = chrono[-1][2] if chrono else 0
            n_tail = tail_frames // q if q else 0
            print(f'    ★из «проглоченных» {n_tail} — это маркеры в полёте на '
                  f'снятии PTT, а не потеря')
        lead = [n for n in notes if n[2] == -2]
        if lead:
            print(f'    аванс под зернистость клиента: {lead[-1][3]} кадров '
                  f'({lead[-1][3] / 48.0:.1f} мс), выдан {len(lead)} раз(а)')
        forg = [n for n in notes if n[2] == -1]
        if forg:
            print(f'    прощений зависшего кредита: {len(forg)}')

    # Главное: каждый простой очереди — с историей перед ним.
    print('\nПРОСТОИ ОЧЕРЕДИ DUC (кто молчал перед пустой очередью)')
    n_over = 0
    for i, r in enumerate(rows):
        if r[1] != 'DUC-DRY':
            continue
        dur = None
        for q in rows[i + 1:]:
            if q[1] == 'DUC-RUN':
                dur = q[2] / 1000.0
                break
            if q[1] == 'DUC-DRY':
                break
        # Простой короче одного пакета (240 отсчётов @192 кГц = 1.25 мс) —
        # не голодание: быстрее отправитель всё равно не умеет. Такие точки
        # ловятся на самом фронте PTT, когда очередь ещё пуста, а pre-roll в
        # неё только заливается.
        over = (dur is not None and dur > CUSHION_MS
                and dur > 1.25)
        if over:
            n_over += 1
        mark = '★ДЛИННЕЕ ПОДУШКИ' if over else ''
        print(f'\n  t={r[0]:.3f} мс  простой {dur if dur is None else round(dur,2)} мс '
              f'(FIFO={r[2]} отсч., в очереди {r[3]} пак.) {mark}')
        for q in rows[max(0, i - ctx):i]:
            print(f'      −{r[0]-q[0]:8.3f} мс  {q[1]:<9} {q[2]:>9} {q[3]:>9} {q[4]:>9}')

    tx_s = span / 1000.0
    if tx_s > 0:
        print(f'\n★ПРОСТОЕВ ДЛИННЕЕ ПОДУШКИ: {n_over} '
              f'({n_over/tx_s:.2f} на секунду передачи)')


if __name__ == '__main__':
    sys.exit(main())
