import argparse
import copy
import os
import re
import sys
import shutil
from collections import defaultdict
from glob import glob
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import yaml


def load_fluctuation_stat(file_name, sep=',', startline=None):
    fluct_stat = pd.read_csv(file_name, sep=sep)[startline:].reset_index()
    if 'mpduRxSuccess' in fluct_stat.columns:
        fluct_stat['lost'] = 1 - fluct_stat['mpduRxSuccess']
    elif 'mpduSucceeded' in fluct_stat.columns:
        fluct_stat['lost'] = 1 - fluct_stat['mpduSucceeded']

    return fluct_stat[['tx_time_us', 'rx_time_us', 'lost']]

def get_netfile_frame_stat(netfile, mtu_size, fps, startline=0, sep=','):
    if isinstance(netfile, str):
        fluct_stat = load_fluctuation_stat(netfile, startline=startline, sep=sep)
    else:
        fluct_stat = copy.deepcopy(netfile)

    fluct_stat['got']  = 1 - fluct_stat['lost']
    fluct_stat         = {key: np.array(value) for key, value in fluct_stat.to_dict('list').items()}
    fluct_index        = 0
    num_frames         = (fluct_stat['tx_time_us'].max() - fluct_stat['tx_time_us'].min()) // 1000000 * fps
    mspf               = 1e6 / fps
    start_frame_time   = fluct_stat['tx_time_us'][fluct_index]

    start_line_np   , start_time_np,     time_np                    = np.zeros((3, num_frames))
    bytes_mtu_scs_np, bytes_mtu_fail_np, thrpt_mtu_np, gdpt_mtu_np  = np.zeros((4, num_frames))

    stat = defaultdict(list)
    for i in range(num_frames):
        start_frame_index  = fluct_index
        start_line_np[i]   = start_frame_index
        start_time_np[i]   = start_frame_time

        if start_frame_time + mspf >= fluct_stat['rx_time_us'][fluct_index]:
            cur_time = start_frame_time
            while cur_time < start_frame_time + mspf:
                bytes_mtu_scs_np[i]  += fluct_stat['got'][fluct_index]  * mtu_size
                bytes_mtu_fail_np[i] += fluct_stat['lost'][fluct_index] * mtu_size
                fluct_index += 1
                cur_time = fluct_stat['rx_time_us'][fluct_index]

            stat['start_frame_line'].append(start_frame_index + 2)
            time_np[i]       = fluct_stat['rx_time_us'][fluct_index - 1] - start_frame_time
        else:
            stat['start_frame_line'].append(-1)
            time_np[i] = 0

        thrpt_mtu_np[i]  = (bytes_mtu_scs_np[i] + bytes_mtu_fail_np[i]) * 8 / mspf
        gdpt_mtu_np[i]   = bytes_mtu_scs_np[i] * 8 / mspf

        stat['num_frame'].append(i)
        stat['count_frame_lines'].append(fluct_index - start_frame_index)
        stat['start_frame_time_ms'].append(start_time_np[i]/1000)
        stat['frame_trans_time_ms'].append(time_np[i]/1000)
        stat['frame_throughput_Mbps'].append(thrpt_mtu_np[i])
        stat['frame_goodput_Mbps'].append(gdpt_mtu_np[i])
        start_frame_time += mspf

    return pd.DataFrame(stat)

def natural_sort_key(s):
    return [int(text) if text.isdigit() else text.lower()
            for text in re.split('([0-9]+)', s)]


class ArgsParser:

    class _ConfigParser:

        def __init__(self, config_path):
            self.config_path = config_path

        def parse(self):
            with open(self.config_path) as f:
                config = yaml.safe_load(f)
            return config

    class _CLIParser:

        def __init__(self, cliargs):
            self.parser   = argparse.ArgumentParser()
            self.cliargs  = cliargs

        def parse(self):
            self.parser.add_argument("--config", type=str, 
                                help="Path to yaml config.")
            self.parser.add_argument("--traces_folder", type=str, 
                                help="Folder with traces.")
            self.parser.add_argument("--output_folder", type=str, 
                                help="Output folder.", default='./')
            self.parser.add_argument("--bitrate", type=float, 
                                help="Bitrate of the abstract video.")
            
            self.args, self.unkwn = self.parser.parse_known_args(self.cliargs)
            return self.args.__dict__
        
    def __init__(self, cli_args, default_args={}):
        self.default_args   = copy.deepcopy(default_args)
        self.cli_parser     = self._CLIParser(cli_args)

    def parse(self):
        self.cliargs  = self.cli_parser.parse()
        self.cliargs  = self._remove_nones(self.cliargs)

        self.config_parser  = self._ConfigParser(self.cliargs['config'])
        self.configargs     = self.config_parser.parse()
        self.configargs     = self._remove_nones(self.configargs)

        self.args = copy.deepcopy(self.default_args)
        self.args.update(self.configargs)
        self.args.update(self.cliargs)

        self.args = {key.replace('-', '_'): val for key, val in self.args.items()}

        return self.args
    
    def _remove_nones(self, args):
        return {key:val for key, val in args.items() if val is not None}
    
    def pretty_print_args(self, args):
        for key, val in args.items():
            print(f'{key:40}: {val}')
    

class SelectionStartlines:

    def __init__(self, mtu_size, fps, bitrate, target_goodput_scale, hard_goodput_bound, 
                 soft_goodput_bound, num_soft_goodput_frames, min_distance, 
                 num_startlines, low_goodput_bound, low_goodput_ratio, 
                 num_frames, num_goodput_deviation_parts, goodput_deviation_scale, **kwargs):
            
            self.mtu_size                              = mtu_size
            self.fps                                   = fps
            self.bitrate                               = bitrate
            self.goodput_bounds                        = target_goodput_scale
            self.goodput_critical_bound0               = hard_goodput_bound
            self.num_critical_goodput_frames0          = 0
            self.goodput_critical_bound1               = soft_goodput_bound
            self.num_critical_goodput_frames1          = num_soft_goodput_frames
            self.min_distance                          = min_distance       
            self.num_selected_startlines               = num_startlines
            self.goodput_lower_bound                   = low_goodput_bound
            self.target_ratio_low_goodput_frames       = low_goodput_ratio
            self.video_num_frames                      = num_frames
            self.num_parts                             = num_goodput_deviation_parts
            self.goodput_deviation_on_parts_from_mean  = goodput_deviation_scale

    def plot_channel_capacity(self, df, fps, frames_slice, title, plot_names='', horizontal_lines=None):
        
        def plot_extra_lines(xlim, **kwargs):
            colors      = {0:'green', 1:'green', 2:'blue', 3:'indigo', 4:'orange'}
            line_style  = {0:'-', 1:'--', 2:'-', 3:'-', 4:'-'}

            for i, (key, val) in enumerate(kwargs.items()):
                val = val if hasattr(val, '__iter__') else [val]
                for j, v in enumerate(val):
                    label = '' if j > 0 else f'{key:30}= {round(v / self.bitrate, 1)};{v:10.1f} Mbps'
                    plt.plot(xlim, [v, v], line_style[i], label=label, color=colors[i])

        for ind, (first_frame, last_frame, step) in enumerate(frames_slice):
            plt.figure(figsize=(15, 5))
            plt.title(title[ind], fontsize = 'xx-large')
            plt.xlabel('time, s', fontsize = 'x-large')
            plt.ylabel('Mbps', fontsize = 'large')

            x = df['start_frame_time_ms'].values[first_frame: last_frame: step] / 1e3 + .5 / fps
            y = df['frame_goodput_Mbps'].values[first_frame: last_frame: step]
            plt.plot(x, y,
                     color = 'r', 
                     label = f'{"GoodPut / video bitrate":30}= {f"{np.mean(df.frame_goodput_Mbps.values[first_frame: last_frame: step]):4.1f} / {self.bitrate:4.1f}":>14} Mbps')

            if horizontal_lines:
                plot_extra_lines([x.min(), x.max()], **horizontal_lines)

            plt.grid(which='major', color='grey', linestyle='--', alpha = 0.7)
            plt.grid(which='minor', color='grey', linestyle=':',  alpha = 0.4)
            plt.legend()
            legend = plt.legend(loc='upper right', fontsize = 'medium')
            for text in legend.get_texts():
                text.set_family('monospace')

            plt.savefig(plot_names[ind])
            plt.close()

    def get_remaining_indices(self, data, delta):
        remaining_positions = []  
        forbidden_indices = set() 
        
        for pos, idx in enumerate(data):
            if all(abs(idx - forbidden_idx) > delta for forbidden_idx in forbidden_indices):
                remaining_positions.append(pos)
                forbidden_indices.add(idx)
        
        return remaining_positions

    def select_startlines(self, df):
        chosen = []
        for startframe in range(len(df) - 2* self.video_num_frames):
            dft = df[startframe: startframe + self.video_num_frames]
            goodput = dft['frame_goodput_Mbps'].mean()
            
            if sum(dft['frame_goodput_Mbps'] < self.goodput_critical_bound0 * self.bitrate) > self.num_critical_goodput_frames0:
                continue
            if sum(dft['frame_goodput_Mbps'] < self.goodput_critical_bound1 * self.bitrate) > self.num_critical_goodput_frames1:
                continue
            if not (self.goodput_bounds[0] * self.bitrate <= goodput <= self.goodput_bounds[1] * self.bitrate):
                continue
            necessary_conds = [self.goodput_deviation_on_parts_from_mean[0] * goodput <= part.mean() <= self.goodput_deviation_on_parts_from_mean[1] * goodput
                        for part in np.array_split(dft['frame_goodput_Mbps'], self.num_parts)]
            if not all(necessary_conds):
                continue

            chosen_elem = {}
            chosen_elem['ratio_low_goodput_frames']  = sum(dft['frame_goodput_Mbps'] < self.bitrate * self.goodput_lower_bound) / self.video_num_frames
            chosen_elem['dist_target_ratio']         = chosen_elem['ratio_low_goodput_frames'] - self.target_ratio_low_goodput_frames
            chosen_elem['std']                       = dft['frame_goodput_Mbps'].std()
            chosen_elem['startline']                 = dft['start_frame_line'].iloc[0]
            chosen_elem['frame_num']                 = dft['num_frame'].iloc[0]
            chosen_elem['goodput']                   = goodput
            chosen.append(chosen_elem)

        chosen = pd.DataFrame(chosen)
        if not chosen.empty:
            chosen = chosen.sort_values(['dist_target_ratio', 'startline'], ascending=True, key=lambda x: abs(x))
            chosen_indecies = self.get_remaining_indices(chosen['frame_num'], self.min_distance * self.fps)
            chosen = chosen.iloc[chosen_indecies]
        return chosen

    
    def run(self, netfiles, output_folder, args):
        if Path(output_folder).joinpath('bandwidth_points').exists():
            shutil.rmtree(Path(output_folder).joinpath('bandwidth_points'))

        for netfile in netfiles:
            fluct_stat = get_netfile_frame_stat(netfile, self.mtu_size, self.fps)
            chosen     = self.select_startlines(fluct_stat)
            
            save_plot_folder = os.path.join(output_folder, 'bandwidth_points', Path(netfile).stem)
            os.makedirs(save_plot_folder, exist_ok=True)

            output_text = f"\n {netfile}\n"
            if len(chosen) > 0:
                output_text += f'Displaying {min(len(chosen), self.num_selected_startlines)} out of {len(chosen)} found startlines\n'
                output_text += 'Startlines:\n\t'
                chosen = chosen[: self.num_selected_startlines]
                stlns_text = [f'{stln:7}' for stln in map(str, chosen['startline'].values)]
                output_text += ' '.join(stlns_text)
                output_text += '\nLow goodput ratio:\n\t'
                ratios_text = [f'{ratio:7}' for ratio in map(str, (100 * chosen['ratio_low_goodput_frames'].values).round(1))]
                output_text += ' '.join(ratios_text)

                frame_slices  = zip(*[chosen['frame_num'], chosen['frame_num'] + 300, [None] * len(chosen['frame_num'])])
                title         = [
                    f"Channel Capacity:     "\
                    f"Startline={row.startline};     " 
                    f"Low goodput ratio={row.ratio_low_goodput_frames:0.2f}"\
                    for row in chosen.itertuples()
                    ]
                plot_names    = [
                    os.path.join(save_plot_folder, f'{row.ratio_low_goodput_frames:0.2f}ratio_{row.startline}stln.png') 
                    for row in chosen.itertuples()
                    ]
                horizontal_lines = {
                    key: np.array(val) * self.bitrate
                    for key, val in args.items()
                    if key in ['hard_goodput_bound', 'soft_goodput_bound', 'low_goodput_bound']
                    }                
                self.plot_channel_capacity(fluct_stat, self.fps, frame_slices, title=title, plot_names=plot_names, horizontal_lines=horizontal_lines)
            else:
                output_text += 'NO STARTLINE\n'
            
            print(output_text)
            with open(os.path.join(save_plot_folder, 'output.log'), 'w') as f:
                log_header = ''
                for key, val in args.items():
                    log_header += f'{key:40}: {val}\n'
                f.write(log_header)
                f.write(output_text)
    

def main(args):
    args_parser = ArgsParser(args)
    args = args_parser.parse()
    args_parser.pretty_print_args(args)

    list_traces = glob(os.path.join(args['traces_folder'], '*.csv'), recursive=True)
    list_traces = sorted(list_traces, key=natural_sort_key)

    if list_traces:
        ss = SelectionStartlines(**args)
        ss.run(list_traces, args['output_folder'], args)
    else:
        print(f"\nThere is no cvs files in {args['traces_folder']}")


if __name__ == "__main__":
    main(sys.argv[1:])