#!/usr/bin/env python3
"""Simulador de un cliente DASH: misma decisión que el cliente real, tiempos exactos.

La red es una lista de tramos (hasta qué segundo, a cuántos bit/s). Un segmento tarda lo que
indique esa lista, y el video avanza mientras tanto. Si el buffer se vacía, la imagen se congela.
Cada estrategia es una función choose(bitrates, samples, buffer_s) que devuelve la calidad.

  dash_sim.py [fixed|last|ruta/a/abr.py] [--oscillating]
"""
import importlib.util
import sys

BITRATES = [400_000, 1_000_000, 2_500_000, 5_000_000]
SEGMENT_S = 2
BUFFER_MAX_S = 10
# 40 s buenos, 30 s de caída, y se recupera.
NETWORK = [(40, 6_000_000), (70, 1_200_000), (float("inf"), 6_000_000)]
# Una red que sube y baja cada 3 s, sin llegar a caer.
OSCILLATING = [(3 * (i + 1), 6_000_000 if i % 2 == 0 else 3_000_000) for i in range(200)] + \
              [(float("inf"), 6_000_000)]


def fixed_top(bitrates, samples, buffer_s):
    """Siempre la mejor calidad: lo que pide quien no quiere pensar en la red."""
    del samples, buffer_s
    return len(bitrates) - 1


def last_sample(bitrates, samples, buffer_s):
    """La mejor calidad que cabe en la última medición, sin margen."""
    del buffer_s
    if not samples:
        return 0
    return max((i for i, rate in enumerate(bitrates) if rate <= samples[-1]), default=0)


def load_choose(path):
    """Carga la función choose de un archivo de Python."""
    spec = importlib.util.spec_from_file_location("abr_module", path)
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module.choose


def download_time(bits, start, network):
    """Avanza por los tramos de la red hasta terminar de bajar esos bits."""
    now = start
    left = bits
    for until, rate in network:
        if now >= until:
            continue
        possible = (until - now) * rate
        if possible >= left:
            return now + left / rate - start
        left -= possible
        now = until
    raise ValueError("la red no alcanza")


def simulate(choose, segments=45, network=None, bitrates=None):
    """Devuelve una fila por segmento y el total de segundos congelado."""
    network = network or NETWORK
    bitrates = bitrates or BITRATES
    now = 0.0
    buffer_s = 0.0
    samples = []
    rows = []
    frozen_total = 0.0
    for number in range(segments):
        # Con el buffer lleno se espera: no tiene sentido bajar medio video por adelantado.
        if buffer_s > BUFFER_MAX_S - SEGMENT_S:
            pause = buffer_s - (BUFFER_MAX_S - SEGMENT_S)
            now += pause
            buffer_s -= pause
        quality = choose(bitrates, list(samples), buffer_s)
        bits = bitrates[quality] * SEGMENT_S
        took = download_time(bits, now, network)
        frozen = max(0.0, took - buffer_s) if number > 0 else 0.0
        frozen_total += frozen
        buffer_s = max(0.0, buffer_s - took) + SEGMENT_S
        now += took
        samples.append(bits / took)
        rows.append({"segment": number, "start": now - took, "bitrate": bitrates[quality], "took": took,
                     "sample": samples[-1], "buffer": buffer_s, "frozen": frozen})
    return rows, frozen_total


def score(rows):
    """Bitrate promedio y número de cambios de calidad."""
    rates = [row["bitrate"] for row in rows]
    switches = sum(1 for a, b in zip(rates, rates[1:]) if a != b)
    return sum(rates) / len(rates), switches


def show(rows, frozen):
    print(" seg  inicio  calidad  tarda  medido    buffer  congelado")
    for row in rows:
        print(f"{row['segment']:4} {row['start']:6.1f}s {row['bitrate'] // 1000:6}k {row['took']:5.1f}s "
              f"{row['sample'] / 1e6:5.2f}M {row['buffer']:7.1f}s {row['frozen']:8.1f}s")
    average, switches = score(rows)
    print(f"bitrate promedio: {average / 1e6:.2f} Mbit/s; cambios: {switches}; congelado: {frozen:.1f} s")


def main():
    args = [arg for arg in sys.argv[1:] if arg != "--oscillating"]
    name = args[0] if args else "fixed"
    choose = {"fixed": fixed_top, "last": last_sample}.get(name) or load_choose(name)
    network = OSCILLATING if "--oscillating" in sys.argv else NETWORK
    rows, frozen = simulate(choose, network=network)
    show(rows, frozen)


if __name__ == "__main__":
    main()
