# Copyright (c) 2018, Kevin Spiteri
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
#    list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import argparse
import json
import math
import sys
import string
import os
from importlib.machinery import SourceFileLoader
from collections import namedtuple
from enum import Enum

# Units used throughout:
#     size     : bits
#     time     : ms
#     size/time: bits/ms = kbit/s


# global variables:
#     video manifest
#     buffer contents
#     buffer first segment consumed
#     throughput estimate
#     latency estimate
#     rebuffer event count
#     rebuffer total time
#     session info


def load_json(path):
    with open(path) as file:
        obj = json.load(file)
    return obj

ManifestInfo = namedtuple('ManifestInfo', 'segment_time bitrates utilities segments')
NetworkPeriod = namedtuple('NetworkPeriod', 'time bandwidth latency')

DownloadProgress = namedtuple('DownloadProgress',
                              'index quality '
                              'size downloaded '
                              'time time_to_first_bit '
                              'abandon_to_quality')

def get_buffer_level():
    global manifest
    global buffer_contents
    global buffer_fcc

    return manifest.segment_time * len(buffer_contents) - buffer_fcc

def deplete_buffer(time):
    global manifest
    global buffer_contents
    global buffer_fcc
    global rebuffer_event_count
    global rebuffer_time
    global played_utility
    global played_bitrate
    global total_play_time
    global total_bitrate_change
    global total_log_bitrate_change
    global last_played

    global rampup_origin
    global rampup_time
    global rampup_threshold
    global sustainable_quality

    if len(buffer_contents) == 0:
        rebuffer_time += time
        total_play_time += time
        return

    if buffer_fcc > 0:
        # first play any partial chunk left

        if time + buffer_fcc < manifest.segment_time:
            buffer_fcc += time
            total_play_time += time
            return

        time -= manifest.segment_time - buffer_fcc
        total_play_time += manifest.segment_time - buffer_fcc
        buffer_contents.pop(0)
        buffer_fcc = 0

    # buffer_fcc == 0 if we're here

    while time > 0 and len(buffer_contents) > 0:

        quality = buffer_contents[0]
        played_utility += manifest.utilities[quality]
        played_bitrate += manifest.bitrates[quality]
        if quality != last_played and last_played != None:
            total_bitrate_change += abs(manifest.bitrates[quality] -
                                        manifest.bitrates[last_played])
            total_log_bitrate_change += abs(math.log(manifest.bitrates[quality] /
                                                     manifest.bitrates[last_played]))
        last_played = quality

        if rampup_time == None:
            rt = sustainable_quality if rampup_threshold == None else rampup_threshold
            if quality >= rt:
                rampup_time = total_play_time - rampup_origin

        # bookkeeping to track reaction time to increased bandwidth
        for p in pending_quality_up:
            if len(p) == 2 and quality >= p[1]:
                p.append(total_play_time)

        if time >= manifest.segment_time:
            buffer_contents.pop(0)
            total_play_time += manifest.segment_time
            time -= manifest.segment_time
        else:
            buffer_fcc = time
            total_play_time += time
            time = 0

    if time > 0:
        rebuffer_time += time
        total_play_time += time
        rebuffer_event_count += 1

    process_quality_up(total_play_time)

def playout_buffer():
    global buffer_contents
    global buffer_fcc

    deplete_buffer(get_buffer_level())

    # make sure no rounding error
    del buffer_contents[:]
    buffer_fcc = 0

def process_quality_up(now):
    global max_buffer_size
    global pending_quality_up
    global total_reaction_time

    # check which switches can be processed

    cutoff = now - max_buffer_size
    while len(pending_quality_up) > 0 and pending_quality_up[0][0] < cutoff:
        p = pending_quality_up.pop(0)
        if len(p) == 2:
            reaction = max_buffer_size
        else:
            reaction = min(max_buffer_size, p[2] - p[0])
        #print('\n[%d] reaction time: %d' % (now, reaction))
        #print(p)
        total_reaction_time += reaction

def advertize_new_network_quality(quality, previous_quality):
    global max_buffer_size
    global network_total_time
    global pending_quality_up
    global buffer_contents

    # bookkeeping to track reaction time to increased bandwidth

    # process any previous quality up switches that have "matured"
    process_quality_up(network_total_time)

    # mark any pending switch up done if new quality switches back below its quality
    for p in pending_quality_up:
        if len(p) == 2 and p[1] > quality:
            p.append(network_total_time)
    #pending_quality_up = [p for p in pending_quality_up if p[1] >= quality]

    # filter out switches which are not upwards (three separate checks)
    if quality <= previous_quality:
        return
    for q in buffer_contents:
        if quality <= q:
            return
    for p in pending_quality_up:
        if quality <= p[1]:
            return

    # valid quality up switch
    pending_quality_up.append([network_total_time, quality])

class NetworkModel:

    min_progress_size = 12000
    min_progress_time = 50

    def __init__(self, network_trace):
        global sustainable_quality
        global network_total_time

        sustainable_quality = None
        network_total_time = 0
        self.trace = network_trace
        self.index = -1
        self.time_to_next = 0
        self.next_network_period()

    def next_network_period(self):
        global manifest
        global sustainable_quality
        global network_total_time

        self.index += 1
        if self.index == len(self.trace):
            self.index = 0
        self.time_to_next = self.trace[self.index].time

        latency_factor = 1 - self.trace[self.index].latency / manifest.segment_time
        effective_bandwidth = self.trace[self.index].bandwidth * latency_factor

        previous_sustainable_quality = sustainable_quality
        sustainable_quality = 0
        for i in range(1, len(manifest.bitrates)):
            if manifest.bitrates[i] > effective_bandwidth:
                break
            sustainable_quality = i
        if (sustainable_quality != previous_sustainable_quality and
            previous_sustainable_quality != None):
            advertize_new_network_quality(sustainable_quality, previous_sustainable_quality)

        if verbose:
            print('[%d] Network: %d,%d  (q=%d: bitrate=%d)' %
                  (round(network_total_time),
                   self.trace[self.index].bandwidth, self.trace[self.index].latency,
                   sustainable_quality, manifest.bitrates[sustainable_quality]))

    # return delay time
    def do_latency_delay(self, delay_units):
        global network_total_time

        total_delay = 0
        while delay_units > 0:
            current_latency = self.trace[self.index].latency
            time = delay_units * current_latency
            if time <= self.time_to_next:
                total_delay += time
                network_total_time += time
                self.time_to_next -= time
                delay_units = 0
            else:
                # time > self.time_to_next implies current_latency > 0
                total_delay += self.time_to_next
                network_total_time += self.time_to_next
                delay_units -= self.time_to_next / current_latency
                self.next_network_period()
        return total_delay

    # return download time
    def do_download(self, size):
        global network_total_time
        total_download_time = 0
        while size > 0:
            current_bandwidth = self.trace[self.index].bandwidth
            if size <= self.time_to_next * current_bandwidth:
                # current_bandwidth > 0
                time = size / current_bandwidth
                total_download_time += time
                network_total_time += time
                self.time_to_next -= time
                size = 0
            else:
                total_download_time += self.time_to_next
                network_total_time += self.time_to_next
                size -= self.time_to_next * current_bandwidth
                self.next_network_period()
        return total_download_time

    def do_minimal_latency_delay(self, delay_units, min_time):
        global network_total_time
        total_delay_units = 0
        total_delay_time = 0
        while delay_units > 0 and min_time > 0:
            current_latency = self.trace[self.index].latency
            time = delay_units * current_latency
            if time <= min_time and time <= self.time_to_next:
                units = delay_units
                self.time_to_next -= time
                network_total_time += time
            elif min_time <= self.time_to_next:
                # time > 0 implies current_latency > 0
                time = min_time
                units = time / current_latency
                self.time_to_next -= time
                network_total_time += time
            else:
                time = self.time_to_next
                units = time / current_latency
                network_total_time += time
                self.next_network_period()
            total_delay_units += units
            total_delay_time += time
            delay_units -= units
            min_time -= time
        return (total_delay_units, total_delay_time)

    def do_minimal_download(self, size, min_size, min_time):
        global network_total_time
        total_size = 0
        total_time = 0
        while size > 0 and (min_size > 0 or min_time > 0):
            current_bandwidth = self.trace[self.index].bandwidth
            if current_bandwidth > 0:
                min_bits = max(min_size, min_time * current_bandwidth)
                bits_to_next = self.time_to_next * current_bandwidth
                if size <= min_bits and size <= bits_to_next:
                    bits = size
                    time = bits / current_bandwidth
                    self.time_to_next -= time
                    network_total_time += time
                elif min_bits <= bits_to_next:
                    bits = min_bits
                    time = bits / current_bandwidth
                    # make sure rounding error does not push while loop into endless loop
                    min_size = 0
                    min_time = 0
                    self.time_to_next -= time
                    network_total_time += time
                else:
                    bits = bits_to_next
                    time = self.time_to_next
                    network_total_time += time
                    self.next_network_period()
            else: # current_bandwidth == 0
                bits = 0
                if min_size > 0 or min_time > self.time_to_next:
                    time = self.time_to_next
                    network_total_time += time
                    self.next_network_period()
                else:
                    time = min_time
                    self.time_to_next -= time
                    network_total_time += time
            total_size += bits
            total_time += time
            size -= bits
            min_size -= bits
            min_time -= time
        return (total_size, total_time)

    def delay(self, time):
        global network_total_time
        while time > self.time_to_next:
            time -= self.time_to_next
            network_total_time += self.time_to_next
            self.next_network_period()
        self.time_to_next -= time
        network_total_time += time

    def download(self, size, idx, quality, buffer_level, check_abandon = None):
        if size <= 0:
            return DownloadProgress(index = idx, quality = quality,
                                    size = 0, downloaded = 0,
                                    time = 0, time_to_first_bit = 0,
                                    abandon_to_quality = None)

        if not check_abandon or (NetworkModel.min_progress_time <= 0 and
                                 NetworkModel.min_progress_size <= 0):
            latency = self.do_latency_delay(1)
            time = latency + self.do_download(size)
            return DownloadProgress(index = idx, quality = quality,
                                    size = size, downloaded = size,
                                    time = time, time_to_first_bit = latency,
                                    abandon_to_quality = None)

        total_download_time = 0
        total_download_size = 0
        min_time_to_progress = NetworkModel.min_progress_time
        min_size_to_progress = NetworkModel.min_progress_size

        if NetworkModel.min_progress_size > 0:
            latency = self.do_latency_delay(1)
            total_download_time += latency
            min_time_to_progress -= total_download_time
            delay_units = 0
        else:
            latency = None
            delay_units = 1

        abandon_quality = None
        while total_download_size < size and abandon_quality == None:

            if delay_units > 0:
                # NetworkModel.min_progress_size <= 0
                (units, time) = self.do_minimal_latency_delay(delay_units, min_time_to_progress)
                total_download_time += time
                delay_units -= units
                min_time_to_progress -= time
                if delay_units <= 0:
                    latency = total_download_time

            if delay_units <= 0:
                # don't use else to allow fall through
                (bits, time) = self.do_minimal_download(size - total_download_size,
                                                        min_size_to_progress, min_time_to_progress)
                total_download_time += time
                total_download_size += bits
                # no need to upldate min_[time|size]_to_progress - reset below

            dp = DownloadProgress(index = idx, quality = quality,
                                  size = size, downloaded = total_download_size,
                                  time = total_download_time, time_to_first_bit = latency,
                                  abandon_to_quality = None)
            if total_download_size < size:
                abandon_quality = check_abandon(dp, max(0, buffer_level - total_download_time))
                if abandon_quality != None:
                    if verbose:
                        print('%d abandoning %d->%d' % (idx, quality, abandon_quality))
                        print('%d/%d %d(%d)' %
                              (dp.downloaded, dp.size, dp.time, dp.time_to_first_bit))
                min_time_to_progress = NetworkModel.min_progress_time
                min_size_to_progress = NetworkModel.min_progress_size

        return DownloadProgress(index = idx, quality = quality,
                                size = size, downloaded = total_download_size,
                                time = total_download_time, time_to_first_bit = latency,
                                abandon_to_quality = abandon_quality)

class ThroughputHistory:
    def __init__(self, config):
        pass
    def push(self, time, tput, lat):
        raise NotImplementedError

class SessionInfo:

    def __init__(self):
        pass

    def get_throughput(self):
        global throughput
        return throughput

    def get_buffer_contents(self):
        global buffer_contents
        return buffer_contents[:]

session_info = SessionInfo()

class Abr:

    session = session_info

    def __init__(self, config):
        pass
    def get_quality_delay(self, segment_index):
        raise NotImplementedError
    def get_first_quality(self):
        return 0
    def report_delay(self, delay):
        pass
    def report_download(self, metrics, is_replacment):
        pass
    def report_seek(self, where):
        pass
    def check_abandon(self, progress, buffer_level):
        return None

    def quality_from_throughput(self, tput):
        global manifest
        global throughput
        global latency

        p = manifest.segment_time

        quality = 0
        while (quality + 1 < len(manifest.bitrates) and
               latency + p * manifest.bitrates[quality + 1] / tput <= p):
            quality += 1
        return quality

class Replacement:

    session = session_info

    def check_replace(self, quality):
        return None
    def check_abandon(self, progress, buffer_level):
        return None

average_list = {}
abr_list = {}

class SlidingWindow(ThroughputHistory):

    default_window_size = [3]
    max_store = 20

    def __init__(self, config):
        global throughput
        global latency

        if 'window_size' in config and config['window_size'] != None:
            self.window_size = config['window_size']
        else:
            self.window_size = SlidingWindow.default_window_size

        # TODO: init somewhere else?
        throughput = None
        latency = None

        self.last_throughputs = []
        self.last_latencies = []

    def push(self, time, tput, lat):
        global throughput
        global latency

        self.last_throughputs += [tput]
        self.last_throughputs = self.last_throughputs[-SlidingWindow.max_store:]

        self.last_latencies += [lat]
        self.last_latencies = self.last_latencies[-SlidingWindow.max_store:]

        tput = None
        lat = None
        for ws in self.window_size:
            sample = self.last_throughputs[-ws:]
            t = sum(sample) / len(sample)
            tput = t if tput == None else min(tput, t) # conservative min
            sample = self.last_latencies[-ws:]
            l = sum(sample) / len(sample)
            lat = l if lat == None else max(lat, l) # conservative max
        throughput = tput
        latency = lat

average_list['sliding'] = SlidingWindow

class Ewma(ThroughputHistory):

    # for throughput:
    default_half_life = [8000, 3000]

    def __init__(self, config):
        global throughput
        global latency

        # TODO: init somewhere else?
        throughput = None
        latency = None

        if 'half_life' in config and config['half_life'] != None:
            self.half_life = [h * 1000 for h in config['half_life']]
        else:
            self.half_life = Ewma.default_half_life

        self.latency_half_life = [h / manifest.segment_time for h in self.half_life]

        self.throughput = [0] * len(self.half_life)
        self.weight_throughput = 0
        self.latency = [0] * len(self.half_life)
        self.weight_latency = 0

    def push(self, time, tput, lat):
        global throughput
        global latency

        for i in range(len(self.half_life)):
            alpha = math.pow(0.5, time / self.half_life[i])
            self.throughput[i] = alpha * self.throughput[i] + (1 - alpha) * tput
            alpha = math.pow(0.5, 1 / self.latency_half_life[i])
            self.latency[i] = alpha * self.latency[i] + (1 - alpha) * lat

        self.weight_throughput += time
        self.weight_latency += 1

        tput = None
        lat = None
        for i in range(len(self.half_life)):
            zero_factor = 1 - math.pow(0.5, self.weight_throughput / self.half_life[i])
            t = self.throughput[i] / zero_factor
            tput = t if tput == None else min(tput, t)  # conservative case is min
            zero_factor = 1 - math.pow(0.5, self.weight_latency / self.latency_half_life[i])
            l = self.latency[i] / zero_factor
            lat = l if lat == None else max(lat, l) # conservative case is max
        throughput = tput
        latency = lat

average_list['ewma'] = Ewma
average_default = 'ewma'

class Bola(Abr):

    def __init__(self, config):
        global verbose
        global manifest

        utility_offset = -math.log(manifest.bitrates[0]) # so utilities[0] = 0
        self.utilities = [math.log(b) + utility_offset for b in manifest.bitrates]

        self.gp = config['gp']
        self.buffer_size = config['buffer_size']
        self.abr_osc = config['abr_osc']
        self.abr_basic = config['abr_basic']
        self.Vp = (self.buffer_size - manifest.segment_time) / (self.utilities[-1] + self.gp)

        self.last_seek_index = 0 # TODO
        self.last_quality = 0

        if verbose:
            for q in range(len(manifest.bitrates)):
                b = manifest.bitrates[q]
                u = self.utilities[q]
                l = self.Vp * (self.gp + u)
                if q == 0:
                    print('%d %d' % (q, l))
                else:
                    qq = q - 1
                    bb = manifest.bitrates[qq]
                    uu = self.utilities[qq]
                    ll = self.Vp * (self.gp + (b * uu - bb * u) / (b - bb))
                    print('%d %d    <- %d %d' % (q, l, qq, ll))

    def quality_from_buffer(self):
        level = get_buffer_level()
        quality = 0
        score = None
        for q in range(len(manifest.bitrates)):
            s = ((self.Vp * (self.utilities[q] + self.gp) - level) / manifest.bitrates[q])
            if score == None or s > score:
                quality = q
                score = s
        return quality

    def get_quality_delay(self, segment_index):
        global manifest
        global throughput

        if not self.abr_basic:
            t = min(segment_index - self.last_seek_index, len(manifest.segments) - segment_index)
            t = max(t / 2, 3)
            t = t * manifest.segment_time
            buffer_size = min(self.buffer_size, t)
            self.Vp = (buffer_size - manifest.segment_time) / (self.utilities[-1] + self.gp)

        quality = self.quality_from_buffer()
        delay = 0

        if quality > self.last_quality:
            quality_t = self.quality_from_throughput(throughput)
            if quality <= quality_t:
                delay = 0
            elif self.last_quality > quality_t:
                quality = self.last_quality
                delay = 0
            else:
                if not self.abr_osc:
                    quality = quality_t + 1
                    delay = 0
                else:
                    quality = quality_t
                    # now need to calculate delay
                    b = manifest.bitrates[quality]
                    u = self.utilities[quality]
                    #bb = manifest.bitrates[quality + 1]
                    #uu = self.utilities[quality + 1]
                    #l = self.Vp * (self.gp + (bb * u - b * uu) / (bb - b))
                    l = self.Vp * (self.gp + u) ##########
                    delay = max(0, get_buffer_level() - l)
                    if quality == len(manifest.bitrates) - 1:
                        delay = 0
                    # delay = 0 ###########

        self.last_quality = quality
        return (quality, delay)

    def report_seek(self, where):
        # TODO: seek properly
        global manifest
        self.last_seek_index = math.floor(where / manifest.segment_time)

    def check_abandon(self, progress, buffer_level):
        global manifest

        if self.abr_basic:
            return None

        remain = progress.size - progress.downloaded
        if progress.downloaded <= 0 or remain <= 0:
            return None

        abandon_to = None
        score = (self.Vp * (self.gp + self.utilities[progress.quality]) - buffer_level) / remain
        if score < 0:
            return # TODO: check

        for q in range(progress.quality):
            other_size = progress.size * manifest.bitrates[q] / manifest.bitrates[progress.quality]
            other_score = (self.Vp * (self.gp + self.utilities[q]) - buffer_level) / other_size
            if other_size < remain and other_score > score:
                # check size: see comment in BolaEnh.check_abandon()
                score = other_score
                abandon_to = q

        if abandon_to != None:
            self.last_quality = abandon_to

        return abandon_to

abr_list['bola'] = Bola

class BolaEnh(Abr):

    minimum_buffer = 10000
    minimum_buffer_per_level = 2000
    low_buffer_safety_factor = 0.5
    low_buffer_safety_factor_init = 0.9

    class State(Enum):
        STARTUP = 1
        STEADY = 2

    def __init__(self, config):
        global verbose
        global manifest

        config_buffer_size = config['buffer_size']
        self.abr_osc = config['abr_osc']
        self.no_ibr = config['no_ibr']

        utility_offset = 1 - math.log(manifest.bitrates[0]) # so utilities[0] = 1
        self.utilities = [math.log(b) + utility_offset for b in manifest.bitrates]

        if self.no_ibr:
            self.gp = config['gp'] - 1 # to match BOLA Basic
            buffer = config['buffer_size']
            self.Vp = (buffer - manifest.segment_time) / (self.utilities[-1] + self.gp)
        else:
            buffer = BolaEnh.minimum_buffer
            buffer += BolaEnh.minimum_buffer_per_level * len(manifest.bitrates)
            buffer = max(buffer, config_buffer_size)
            print(buffer)
            self.gp = (self.utilities[-1] - 1) / (buffer / BolaEnh.minimum_buffer - 1)
            self.Vp = BolaEnh.minimum_buffer / self.gp
            #equivalently:
            #self.Vp = (buffer - BolaEnh.minimum_buffer) / (math.log(manifest.bitrates[-1] / manifest.bitrates[0]))
            #self.gp = BolaEnh.minimum_buffer / self.Vp

        self.state = BolaEnh.State.STARTUP
        self.placeholder = 0
        self.last_quality = 0

        if verbose:
            for q in range(len(manifest.bitrates)):
                b = manifest.bitrates[q]
                u = self.utilities[q]
                l = self.Vp * (self.gp + u)
                if q == 0:
                    print('%d %d' % (q, l))
                else:
                    qq = q - 1
                    bb = manifest.bitrates[qq]
                    uu = self.utilities[qq]
                    ll = self.Vp * (self.gp + (b * uu - bb * u) / (b - bb))
                    print('%d %d    <- %d %d' % (q, l, qq, ll))

    def quality_from_buffer(self, level):
        if level == None:
            level = get_buffer_level()
        quality = 0
        score = None
        for q in range(len(manifest.bitrates)):
            s = ((self.Vp * (self.utilities[q] + self.gp) - level) / manifest.bitrates[q])
            if score == None or s > score:
                quality = q
                score = s
        return quality

    def quality_from_buffer_placeholder(self):
        return self.quality_from_buffer(get_buffer_level() + self.placeholder)

    def min_buffer_for_quality(self, quality):
        global manifest

        bitrate = manifest.bitrates[quality]
        utility = self.utilities[quality]

        level = 0
        for q in range(quality):
            # for each bitrates[q] less than bitrates[quality],
            # BOLA should prefer bitrates[quality]
            # (unless bitrates[q] has higher utility)
            if self.utilities[q] < self.utilities[quality]:
                b = manifest.bitrates[q]
                u = self.utilities[q]
                l = self.Vp * (self.gp + (bitrate * u - b * utility) / (bitrate - b))
                level = max(level, l)
        return level

    def max_buffer_for_quality(self, quality):
        return self.Vp * (self.utilities[quality] + self.gp)

    def get_quality_delay(self, segment_index):
        global buffer_contents
        global buffer_fcc
        global throughput

        buffer_level = get_buffer_level()

        if self.state == BolaEnh.State.STARTUP:
            if throughput == None:
                return (self.last_quality, 0)
            self.state = BolaEnh.State.STEADY
            self.ibr_safety = BolaEnh.low_buffer_safety_factor_init
            quality = self.quality_from_throughput(throughput)
            self.placeholder = self.min_buffer_for_quality(quality) - buffer_level
            self.placeholder = max(0, self.placeholder)
            return (quality, 0)

        quality = self.quality_from_buffer_placeholder()
        quality_t = self.quality_from_throughput(throughput)
        if quality > self.last_quality and quality > quality_t:
            quality = max(self.last_quality, quality_t)
            if not self.abr_osc:
                quality += 1

        max_level = self.max_buffer_for_quality(quality)

        ################
        if quality > 0:
            q = quality
            b = manifest.bitrates[q]
            u = self.utilities[q]
            qq = q - 1
            bb = manifest.bitrates[qq]
            uu = self.utilities[qq]
            #max_level = self.Vp * (self.gp + (b * uu - bb * u) / (b - bb))
        ################

        delay = buffer_level + self.placeholder - max_level
        if delay > 0:
            if delay <= self.placeholder:
                self.placeholder -= delay
                delay = 0
            else:
                delay -= self.placeholder
                self.placeholder = 0
        else:
            delay = 0

        if quality == len(manifest.bitrates) - 1:
            delay = 0

        # insufficient buffer rule
        if not self.no_ibr:
            safe_size = self.ibr_safety * (buffer_level - latency) * throughput
            self.ibr_safety *= BolaEnh.low_buffer_safety_factor_init
            self.ibr_safety = max(self.ibr_safety, BolaEnh.low_buffer_safety_factor)
            for q in range(quality):
                if manifest.bitrates[q + 1] * manifest.segment_time > safe_size:
                    #print('InsufficientBufferRule %d -> %d' % (quality, q))
                    quality = q
                    delay = 0
                    min_level = self.min_buffer_for_quality(quality)
                    max_placeholder = max(0, min_level - buffer_level)
                    self.placeholder = min(max_placeholder, self.placeholder)
                    break

        #print('ph=%d' % self.placeholder)
        return (quality, delay)

    def report_delay(self, delay):
        self.placeholder += delay

    def report_download(self, metrics, is_replacment):
        global manifest
        self.last_quality = metrics.quality
        level = get_buffer_level()

        if metrics.abandon_to_quality == None:

            if is_replacment:
                self.placeholder += manifest.segment_time
            else:
                # make sure placeholder is not too large relative to download
                level_was = level + metrics.time
                max_effective_level = self.max_buffer_for_quality(metrics.quality)
                max_placeholder = max(0, max_effective_level - level_was)
                self.placeholder = min(self.placeholder, max_placeholder)

                # make sure placeholder not too small (can happen when decision not taken by BOLA)
                if level > 0:
                    # we don't want to inflate placeholder when rebuffering
                    min_effective_level = self.min_buffer_for_quality(metrics.quality)
                    # min_effective_level < max_effective_level
                    min_placeholder = min_effective_level - level_was
                    self.placeholder = max(self.placeholder, min_placeholder)
                # else: no need to deflate placeholder for 0 buffer - empty buffer handled

        elif not is_replacment: # do nothing if we abandoned a replacement
            # abandonment indicates something went wrong - lower placeholder to conservative level
            if metrics.abandon_to_quality > 0:
                want_level = self.min_buffer_for_quality(metrics.abandon_to_quality)
            else:
                want_level = BolaEnh.minimum_buffer
            max_placeholder = max(0, want_level - level)
            self.placeholder = min(self.placeholder, max_placeholder)

    def report_seek(self, where):
        # TODO: seek properly
        self.state = BolaEnh.State.STARTUP

    def check_abandon(self, progress, buffer_level):
        global manifest

        remain = progress.size - progress.downloaded
        if progress.downloaded <= 0 or remain <= 0:
            return None

        # abandon leads to new latency, so estimate what current status is after latency
        bl = max(0, buffer_level + self.placeholder - progress.time_to_first_bit)
        tp = progress.downloaded / (progress.time - progress.time_to_first_bit)
        sz = remain - progress.time_to_first_bit * tp
        if sz <= 0:
            return None

        abandon_to = None
        score = (self.Vp * (self.gp + self.utilities[progress.quality]) - bl) / sz

        for q in range(progress.quality):
            other_size = progress.size * manifest.bitrates[q] / manifest.bitrates[progress.quality]
            other_score = (self.Vp * (self.gp + self.utilities[q]) - bl) / other_size
            if other_size < sz and other_score > score:
                # check size:
                # if remaining bits in this download are less than new download, why switch?
                # IMPORTANT: this check is NOT subsumed in score check:
                # if sz < other_size and bl is large, original score suffers larger penalty
                #print('abandon bl=%d=%d+%d-%d %d->%d score:%d->%s' % (progress.quality, bl, buffer_level, self.placeholder, progress.time_to_first_bit, q, score, other_score))
                score = other_score
                abandon_to = q

        return abandon_to

abr_list['bolae'] = BolaEnh
abr_default = 'bolae'

class ThroughputRule(Abr):

    safety_factor = 0.9
    low_buffer_safety_factor = 0.5
    low_buffer_safety_factor_init = 0.9
    abandon_multiplier = 1.8
    abandon_grace_time = 500

    def __init__(self, config):
        self.ibr_safety = ThroughputRule.low_buffer_safety_factor_init
        self.no_ibr = config['no_ibr']

    def get_quality_delay(self, segment_index):
        global manifest

        quality = self.quality_from_throughput(throughput * ThroughputRule.safety_factor)

        if not self.no_ibr:
            # insufficient buffer rule
            safe_size = self.ibr_safety * (get_buffer_level() - latency) * throughput
            self.ibr_safety *= ThroughputRule.low_buffer_safety_factor_init
            self.ibr_safety = max(self.ibr_safety, ThroughputRule.low_buffer_safety_factor)
            for q in range(quality):
                if manifest.bitrates[q + 1] * manifest.segment_time > safe_size:
                    quality = q
                    break

        return (quality, 0)

    def check_abandon(self, progress, buffer_level):
        global manifest

        quality = None # no abandon

        dl_time = progress.time - progress.time_to_first_bit
        if progress.time >= ThroughputRule.abandon_grace_time and dl_time > 0:
            tput = progress.downloaded / dl_time
            size_left = progress.size - progress.downloaded
            estimate_time_left = size_left / tput
            if (progress.time + estimate_time_left >
                ThroughputRule.abandon_multiplier * manifest.segment_time):
                quality = self.quality_from_throughput(tput * ThroughputRule.safety_factor)
                estimate_size = (progress.size *
                                 manifest.bitrates[quality] / manifest.bitrates[progress.quality])
                if quality >= progress.quality or estimate_size >= size_left:
                    quality = None

        return quality

abr_list['throughput'] = ThroughputRule

class Dynamic(Abr):

    low_buffer_threshold = 10000

    def __init__(self, config):
        global manifest

        self.bola = Bola(config)
        self.tput = ThroughputRule(config)

        self.is_bola = False

    def get_quality_delay(self, segment_index):
        level = get_buffer_level()

        b = self.bola.get_quality_delay(segment_index)
        t = self.tput.get_quality_delay(segment_index)

        if self.is_bola:
            if level < Dynamic.low_buffer_threshold and b[0] < t[0]:
                self.is_bola = False
        else:
            if level > Dynamic.low_buffer_threshold and b[0] >= t[0]:
                self.is_bola = True

        return b if self.is_bola else t

    def get_first_quality(self):
        if self.is_bola:
            return self.bola.get_first_quality()
        else:
            return self.tput.get_first_quality()

    def report_delay(self, delay):
        self.bola.report_delay(delay)
        self.tput.report_delay(delay)

    def report_download(self, metrics, is_replacment):
        self.bola.report_download(metrics, is_replacment)
        self.tput.report_download(metrics, is_replacment)
        if is_replacment:
            self.is_bola = False

    def check_abandon(self, progress, buffer_level):
        if False and self.is_bola:
            return self.bola.check_abandon(progress, buffer_level)
        else:
            return self.tput.check_abandon(progress, buffer_level)

abr_list['dynamic'] = Dynamic

class DynamicDash(Abr):

    def __init__(self, config):
        global manifest

        self.bola = BolaEnh(config)
        self.tput = ThroughputRule(config)

        buffer_size = config['buffer_size']
        self.low_threshold = (buffer_size - manifest.segment_time) / 2
        self.high_threshold = (buffer_size - manifest.segment_time) - 100
        self.low_threshold = 5000
        self.high_threshold = 10000
        ######################## TODO
        self.is_bola = False

    def get_quality_delay(self, segment_index):
        level = get_buffer_level()
        if self.is_bola and level < self.low_threshold:
            self.is_bola = False
        elif not self.is_bola and level > self.high_threshold:
            self.is_bola = True

        if self.is_bola:
            return self.bola.get_quality_delay(segment_index)
        else:
            return self.tput.get_quality_delay(segment_index)

    def get_first_quality(self):
        if self.is_bola:
            return self.bola.get_first_quality()
        else:
            return self.tput.get_first_quality()

    def report_delay(self, delay):
        self.bola.report_delay(delay)
        self.tput.report_delay(delay)

    def report_download(self, metrics, is_replacment):
        self.bola.report_download(metrics, is_replacment)
        self.tput.report_download(metrics, is_replacment)

    def check_abandon(self, progress, buffer_level):
        if self.is_bola:
            return self.bola.check_abandon(progress, buffer_level)
        else:
            return self.tput.check_abandon(progress, buffer_level)

abr_list['dynamicdash'] = DynamicDash

class Bba(Abr):

    def __init__(self, config):
        pass

    def get_quality_delay(self, segment_index):
        raise NotImplementedError

    def report_delay(self, delay):
        pass

    def report_download(self, metrics, is_replacment):
        pass

    def report_seek(self, where):
        pass


abr_list['bba'] = Bba

class NoReplace(Replacement):
        pass

# TODO: different classes instead of strategy
class Replace(Replacement):

    def __init__(self, strategy):
        self.strategy = strategy
        self.replacing = None
        # self.replacing is either None or -ve index to buffer_contents

    def check_replace(self, quality):
        global manifest
        global buffer_contents
        global buffer_fcc

        self.replacing = None

        if self.strategy == 0:

            skip = math.ceil(1.5 + buffer_fcc / manifest.segment_time)
            #print('skip = %d  fcc = %d' % (skip, buffer_fcc))
            for i in range(skip, len(buffer_contents)):
                if buffer_contents[i] < quality:
                    self.replacing = i - len(buffer_contents)
                    break

            #if self.replacing == None:
            #    print('no repl:  0/%d' % len(buffer_contents))
            #else:
            #    print('replace: %d/%d' % (self.replacing, len(buffer_contents)))

        elif self.strategy == 1:

            skip = math.ceil(1.5 + buffer_fcc / manifest.segment_time)
            #print('skip = %d  fcc = %d' % (skip, buffer_fcc))
            for i in range(len(buffer_contents) - 1, skip - 1, -1):
                if buffer_contents[i] < quality:
                    self.replacing = i - len(buffer_contents)
                    break

            #if self.replacing == None:
            #    print('no repl:  0/%d' % len(buffer_contents))
            #else:
            #    print('replace: %d/%d' % (self.replacing, len(buffer_contents)))

        else:
            pass


        return self.replacing

    def check_abandon(self, progress, buffer_level):
        global manifest
        global buffer_contents
        global buffer_fcc

        if self.replacing == None:
            return None
        if buffer_level + manifest.segment_time * self.replacing <= 0:
            return -1
        return None


class AbrInput(Abr):

    def __init__(self, path, config):
        self.name = os.path.splitext(os.path.basename(path))[0]
        self.abr_module = SourceFileLoader(self.name, path).load_module()
        self.abr_class = getattr(self.abr_module, self.name)
        self.abr_class.session = session_info
        self.abr = self.abr_class(config)

    def get_quality_delay(self, segment_index):
        return self.abr.get_quality_delay(segment_index)
    def get_first_quality(self):
        return self.abr.get_first_quality()
    def report_delay(self, delay):
        self.abr.report_delay(delay)
    def report_download(self, metrics, is_replacment):
        self.abr.report_download(metrics, is_replacment)
    def report_seek(self, where):
        self.abr.report_seek(where)
    def check_abandon(self, progress, buffer_level):
        return self.abr.check_abandon(progress, buffer_level)

class ReplacementInput(Replacement):

    def __init__(self, path):
        self.name = os.path.splitext(os.path.basename(path))[0]
        self.replacement_module = SourceFileLoader(self.name, path).load_module()
        self.replacement_class = getattr(self.replacement_module, self.name)
        self.replacement_class.session = session_info
        self.replacement = self.replacement_class()

    def check_replace(self, quality):
        return self.replacement.check_replace(quality)
    def check_abandon(self, progress, buffer_level):
        return self.replacement.check_abandon(progress, buffer_level)

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description = 'Simulate an ABR session.',
                                     formatter_class = argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument('-n', '--network', metavar = 'NETWORK', default = 'network.json',
                        help = 'Specify the .json file describing the network trace.')
    parser.add_argument('-nm', '--network-multiplier', metavar = 'MULTIPLIER',
                        type = float, default = 1,
                        help = 'Multiply throughput by MULTIPLIER.')
    parser.add_argument('-m', '--movie', metavar = 'MOVIE', default = 'movie.json',
                        help = 'Specify the .json file describing the movie chunks.')
    parser.add_argument('-ml', '--movie-length', metavar = 'LEN', type = float, default = None,
                        help = 'Specify the movie length in seconds (use MOVIE length if None).')
    parser.add_argument('-a', '--abr', metavar = 'ABR',
                        #choices = abr_list.keys(),
                        default = abr_default,
                        help = 'Choose ABR algorithm from predefined list (%s), or specify .py module to import.' % ', '.join(abr_list.keys()))
    parser.add_argument('-ab', '--abr-basic', action = 'store_true',
                        help = 'Set ABR to BASIC (ABR strategy dependant).')
    parser.add_argument('-ao', '--abr-osc', action = 'store_true',
                        help = 'Set ABR to minimize oscillations.')
    parser.add_argument('-gp', '--gamma-p', metavar = 'GAMMAP', type = float, default = 5,
                        help = 'Specify the (gamma p) product in seconds.')
    parser.add_argument('-noibr', '--no-insufficient-buffer-rule', action = 'store_true',
                        help = 'Disable Insufficient Buffer Rule.')
    parser.add_argument('-ma', '--moving-average', metavar = 'AVERAGE',
                        choices = average_list.keys(), default = average_default,
                        help = 'Specify the moving average strategy (%s).' %
                        ', '.join(average_list.keys()))
    parser.add_argument('-ws', '--window-size', metavar = 'WINDOW_SIZE',
                        nargs = '+', type = int, default = [3],
                        help = 'Specify sliding window size.')
    parser.add_argument('-hl', '--half-life', metavar = 'HALF_LIFE',
                        nargs = '+', type = float, default = [3, 8],
                        help = 'Specify EWMA half life.')
    parser.add_argument('-s', '--seek', nargs = 2, metavar = ('WHEN', 'SEEK'),
                        type = float, default = None,
                        help = 'Specify when to seek in seconds and where to seek in seconds.')
    choices = ['none', 'left', 'right']
    parser.add_argument('-r', '--replace', metavar = 'REPLACEMENT',
                        #choices = choices,
                        default  =  'none',
                        help = 'Set replacement strategy from predefined list (%s), or specify .py module to import.' % ', '.join(choices))
    parser.add_argument('-b', '--max-buffer', metavar = 'MAXBUFFER', type = float, default = 25,
                        help = 'Specify the maximum buffer size in seconds.')
    parser.add_argument('-noa', '--no-abandon', action = 'store_true',
                        help = 'Disable abandonment.')
    parser.add_argument('-rmp', '--rampup-threshold', metavar = 'THRESHOLD',
                        type = int, default = None,
                        help = 'Specify at what quality index we are ramped up (None matches network).')
    parser.add_argument('-v', '--verbose', action = 'store_true',
                        help = 'Run in verbose mode.')
    args = parser.parse_args()

    verbose = args.verbose

    buffer_contents = []
    buffer_fcc = 0
    pending_quality_up = []
    reaction_metrics = []

    rebuffer_event_count = 0
    rebuffer_time = 0
    played_utility = 0
    played_bitrate = 0
    total_play_time = 0
    total_bitrate_change = 0
    total_log_bitrate_change = 0
    total_reaction_time = 0
    last_played = None

    overestimate_count = 0
    overestimate_average = 0
    goodestimate_count = 0
    goodestimate_average = 0
    estimate_average = 0

    rampup_origin = 0
    rampup_time = None
    rampup_threshold = args.rampup_threshold

    max_buffer_size = args.max_buffer * 1000

    manifest = load_json(args.movie)
    bitrates = manifest['bitrates_kbps']
    utility_offset = 0 - math.log(bitrates[0]) # so utilities[0] = 0
    utilities = [math.log(b) + utility_offset for b in bitrates]
    if args.movie_length != None:
        l1 = len(manifest['segment_sizes_bits'])
        l2 = math.ceil(args.movie_length * 1000 / manifest['segment_duration_ms'])
        manifest['segment_sizes_bits'] *= math.ceil(l2 / l1)
        manifest['segment_sizes_bits'] = manifest['segment_sizes_bits'][0:l2]
    manifest = ManifestInfo(segment_time = manifest['segment_duration_ms'],
                            bitrates     = bitrates,
                            utilities    = utilities,
                            segments     = manifest['segment_sizes_bits'])
    SessionInfo.manifest = manifest

    network_trace = load_json(args.network)
    network_trace = [NetworkPeriod(time      = p['duration_ms'],
                                   bandwidth = p['bandwidth_kbps'] * args.network_multiplier,
                                   latency   = p['latency_ms'])
                     for p in network_trace]


    buffer_size = args.max_buffer * 1000
    gamma_p = args.gamma_p

    config = {'buffer_size': buffer_size,
              'gp': gamma_p,
              'abr_osc': args.abr_osc,
              'abr_basic': args.abr_basic,
              'no_ibr': args.no_insufficient_buffer_rule}
    if args.abr[-3:] == '.py':
        abr = AbrInput(args.abr, config)
    else:
        abr_list[args.abr].use_abr_o = args.abr_osc
        abr_list[args.abr].use_abr_u = not args.abr_osc
        abr = abr_list[args.abr](config)
    network = NetworkModel(network_trace)

    if args.replace[-3:] == '.py':
        replacer = ReplacementInput(args.replace)
    if args.replace == 'left':
        replacer = Replace(0)
    elif args.replace == 'right':
        replacer = Replace(1)
    else:
        replacer = NoReplace()

    config = {'window_size': args.window_size, 'half_life': args.half_life}
    throughput_history = average_list[args.moving_average](config)

    # download first segment
    quality = abr.get_first_quality()
    size = manifest.segments[0][quality]
    download_metric = network.download(size, 0, quality, 0)
    download_time = download_metric.time - download_metric.time_to_first_bit
    startup_time = download_time
    buffer_contents.append(download_metric.quality)
    t = download_metric.size / download_time
    l = download_metric.time_to_first_bit
    throughput_history.push(download_time, t, l)
    #print('%d,%d -> %d,%d' % (t, l, throughput, latency))
    total_play_time += download_metric.time

    if verbose:
        print('[%d-%d]  %d: q=%d s=%d/%d t=%d=%d+%d bl=0->0->%d' %
              (0, round(download_metric.time), 0, download_metric.quality,
               download_metric.downloaded, download_metric.size,
               download_metric.time, download_metric.time_to_first_bit,
               download_metric.time - download_metric.time_to_first_bit,
               get_buffer_level()))

     # download rest of segments
    next_segment = 1
    abandoned_to_quality = None
    while next_segment < len(manifest.segments):

        # TODO: BEGIN TODO: reimplement seeking - currently only proof-of-concept hack
        if args.seek != None:
            if next_segment * manifest.segment_time >= 1000 * args.seek[0]:
                next_segment = math.floor(1000 * args.seek[1] / manifest.segment_time)
                buffer_contents = []
                buffer_fcc = 0
                abr.report_seek(1000 * args.seek[1])
                args.seek = None
                rampup_origin = total_play_time
                rampup_time = None
        # TODO:  END TODO:  reimplement seeking - currently only proof-of-concept hack

        # do we have space for a new segment on the buffer?
        full_delay = get_buffer_level() + manifest.segment_time - buffer_size
        if full_delay > 0:
            deplete_buffer(full_delay)
            network.delay(full_delay)
            abr.report_delay(full_delay)
            if verbose:
                print('full buffer delay %d bl=%d' % (full_delay, get_buffer_level()))

        if abandoned_to_quality == None:
            (quality, delay) = abr.get_quality_delay(next_segment)
            replace = replacer.check_replace(quality)
        else:
            (quality, delay) = (abandoned_to_quality, 0)
            replace = None
            abandon_to_quality = None

        if replace != None:
            delay = 0
            current_segment = next_segment + replace
            check_abandon = replacer.check_abandon
        else:
            current_segment = next_segment
            check_abandon = abr.check_abandon
        if args.no_abandon:
            check_abandon = None

        size = manifest.segments[current_segment][quality]

        if delay > 0:
            deplete_buffer(delay)
            network.delay(delay)
            if verbose:
                print('abr delay %d bl=%d' % (delay, get_buffer_level()))

        #print('size %d, current_segment %d, quality %d, buffer_level %d' %
        #      (size, current_segment, quality, get_buffer_level()))

        download_metric = network.download(size, current_segment, quality,
                                           get_buffer_level(), check_abandon)

        #print('index %d, quality %d, downloaded %d/%d, time %d=%d+.' %
        #      (download_metric.index, download_metric.quality,
        #       download_metric.downloaded, download_metric.size,
        #       download_metric.time, download_metric.time_to_first_bit))

        if verbose:
            print('[%d-%d]  %d: q=%d s=%d/%d t=%d=%d+%d ' %
                  (round(total_play_time), round(total_play_time + download_metric.time),
                   current_segment, download_metric.quality,
                   download_metric.downloaded, download_metric.size,
                   download_metric.time, download_metric.time_to_first_bit,
                   download_metric.time - download_metric.time_to_first_bit),
                  end = '')
            if replace == None:
                if download_metric.abandon_to_quality == None:
                    print('bl=%d' % get_buffer_level(), end = '')
                else:
                    print(' ABANDONED to %d - %d/%d bits in %d=%d+%d ttfb+ttdl  bl=%d' %
                          (download_metric.abandon_to_quality,
                           download_metric.downloaded, download_metric.size,
                           download_metric.time, download_metric.time_to_first_bit,
                           download_metric.time - download_metric.time_to_first_bit,
                           get_buffer_level()),
                          end = '')
            else:
                if download_metric.abandon_to_quality == None:
                    print(' REPLACEMENT  bl=%d' % get_buffer_level(), end = '')
                else:
                    print(' REPLACMENT ABANDONED after %d=%d+%d ttfb+ttdl  bl=%d' %
                          (download_metric.time, download_metric.time_to_first_bit,
                           download_metric.time - download_metric.time_to_first_bit,
                           get_buffer_level()),
                          end = '')

        #print('deplete buffer %d' % download_metric.time)
        deplete_buffer(download_metric.time)
        if verbose:
            print('->%d' % get_buffer_level(), end='')

        # update buffer with new download
        if replace == None:
            if download_metric.abandon_to_quality == None:
                buffer_contents += [quality]
                next_segment += 1
            else:
                abandon_to_quality = download_metric.abandon_to_quality
        else:
            # abandon_to_quality == None
            if download_metric.abandon_to_quality == None:
                if get_buffer_level() + manifest.segment_time * replace >= 0:
                    buffer_contents[replace] = quality
                else:
                    print('WARNING: too late to replace')
                    pass
            else:
                pass
            # else: do nothing because segment abandonment does not suggest new download

        #if rampup_time == None and download_metric.abandon_to_quality == None:
        #    if rampup_threshold == None:
        #        if download_metric.quality >= sustainable_quality:
        #            rampup_time = download_metric.index * manifest.segment_time
        #    else:
        #        if download_metric.quality >= rampup_threshold:
        #            rampup_time = download_metric.index * manifest.segment_time

        if verbose:
            print('->%d' % get_buffer_level())

        abr.report_download(download_metric, replace != None)

        # calculate throughput and latency
        download_time = download_metric.time - download_metric.time_to_first_bit
        t = download_metric.downloaded / download_time
        l = download_metric.time_to_first_bit

        # check accuracy of throughput estimate
        if throughput > t:
            overestimate_count += 1
            overestimate_average += (throughput - t - overestimate_average) / overestimate_count
        else:
            goodestimate_count += 1
            goodestimate_average += (t - throughput - goodestimate_average) / goodestimate_count
        estimate_average += ((throughput - t - estimate_average) /
                             (overestimate_count + goodestimate_count))

        # update throughput estimate
        if download_metric.abandon_to_quality == None:
            throughput_history.push(download_time, t, l)

        # loop while next_segment < len(manifest.segments)

    playout_buffer()

    # multiply by to_time_average to get per/chunk average
    to_time_average = 1 / (total_play_time / manifest.segment_time)
    count = len(manifest.segments)
    time = count * manifest.segment_time + rebuffer_time + startup_time
    print('buffer size: %d' % buffer_size)
    print('total played utility: %f' % played_utility)
    print('time average played utility: %f' % (played_utility * to_time_average))
    print('total played bitrate: %f' % played_bitrate)
    print('time average played bitrate: %f' % (played_bitrate * to_time_average))
    print('total play time: %f' % (total_play_time / 1000))
    print('total play time chunks: %f' % (total_play_time / manifest.segment_time))
    print('total rebuffer: %f' % (rebuffer_time / 1000))
    print('rebuffer ratio: %f' % (rebuffer_time / total_play_time))
    print('time average rebuffer: %f' % (rebuffer_time / 1000 * to_time_average))
    print('total rebuffer events: %f' % rebuffer_event_count)
    print('time average rebuffer events: %f' % (rebuffer_event_count * to_time_average))
    print('total bitrate change: %f' % total_bitrate_change)
    print('time average bitrate change: %f' % (total_bitrate_change * to_time_average))
    print('total log bitrate change: %f' % total_log_bitrate_change)
    print('time average log bitrate change: %f' % (total_log_bitrate_change * to_time_average))
    print('time average score: %f' %
          (to_time_average * (played_utility -
                              args.gamma_p * rebuffer_time / manifest.segment_time)))
    if overestimate_count == 0:
        print('over estimate count: 0')
        print('over estimate: 0')
    else:
        print('over estimate count: %d' % overestimate_count)
        print('over estimate: %f' % overestimate_average)
    if goodestimate_count == 0:
        print('leq estimate count: 0')
        print('leq estimate: 0')
    else:
        print('leq estimate count: %d' % goodestimate_count)
        print('leq estimate: %f' % goodestimate_average)
    print('estimate: %f' % estimate_average)
    if rampup_time == None:
        print('rampup time: %f' % (len(manifest.segments) * manifest.segment_time / 1000))
    else:
        print('rampup time: %f' % (rampup_time / 1000))
    print('total reaction time: %f' % (total_reaction_time / 1000))