
import os 
import sys
import io
import re

import yaml

import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.ticker import EngFormatter

import colorsys

SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))

class Re(object):
  def __init__(self):
    self.last_match = None
  def match(self,pattern,text):
    self.last_match = re.match(pattern,text)
    return self.last_match
  def search(self,pattern,text):
    self.last_match = re.search(pattern,text)
    return self.last_match


def setup_step(plot_data, key):
    if(key == None):
        key = "default";
    
    if(key in plot_data):
        return;

    next_step = {
        "step" : key
    };

    plot_data[key] = next_step;
    plot_data['steps'].append(next_step);

def read_simplecsv(filename, plot_config):
    print(f"Reading CSV file {filename}...");

    series_values = plot_config.get('load_values', ['x', 'y']);
    series_tags = plot_config.get('load_tags', None);

    plot_data = {
        'steps': []
    };

    with io.open(filename, mode='r') as file:
        column_names = file.readline().rstrip("\n").split(',');

        for line in file:
            line = line.rstrip("\n").split(',');

            line_data = {};
            for entry in enumerate(line):
                column = column_names[entry[0]];

                if(column in series_values):
                    line_data[column] = float(entry[1]);
                elif(column in (series_tags or [])):
                    line_data[column] = entry[1];

            series_name = "default"
            if(not (series_tags is None)):
                series_name = ""
                for tag in series_tags:
                    series_name = series_name + ' ' + line_data[tag];
            
            if(not (series_name in plot_data)):
                new_series = { "step": series_name };
                for value_name in series_values:
                    new_series[value_name] = [];

                plot_data[series_name] = new_series;
                plot_data['steps'].append(new_series);

            series = plot_data[series_name];

            for value_name in series_values:
                series[value_name].append(line_data[value_name]);

    return plot_data

def read_multicsv(plot_config):
    series_collection = {
        'steps': []
    }

    print("Reading multiple CSV files...")
    print(plot_config['load'])

    plot_list = plot_config['load']

    for plot_key in plot_list:
        single_plot_data = read_simplecsv(os.path.join(YAML_DIR, plot_list[plot_key]), plot_config)
        single_plot_data = single_plot_data['steps'][0]

        single_plot_data['step'] = plot_key

        series_collection[plot_key] = single_plot_data
        series_collection['steps'].append(single_plot_data)

    return series_collection

def read_ltspice_file(filename, plot_config):
    print(f"Reading LTSpice .txt file {filename}...");

    series = [];
    plot_data  = {
        'steps': []
    };
    current_step = None;

    with io.open(filename, mode="r", encoding="ISO8859") as file:
        header = file.readline();
        lines = header.rstrip("\n").split("\t");
        
        for line in file:
            line = line.rstrip("\n");
            lre = Re();
            
            if lre.search("^Step Information: ([^=]*)=([\d\.\+-]+)(\w?)  \(Step: .*\)$", line):
                step_param = plot_config.get('step_parameter', lre.last_match[1]);

                value = f"{round(float(lre.last_match[2]), 2):.2f}".replace('.', ',');
                unit = f"{lre.last_match[3]}{plot_config.get('step_unit', '')}";

                current_step = "{value:>6s} {unit:s}".format(value=value, unit=unit);
            else:
                setup_step(plot_data, current_step);
                step = plot_data[current_step];
                
                elements = line.split("\t");
                for idx, element in enumerate(elements):
                    lname = lines[idx];
                    ere = Re();
                    if ere.search("^([\d\.e\+-]+)$", element):
                        if(not lname in step):
                            step[lname] = []
                        step[lname].append(float(ere.last_match[1]));
                    elif ere.search("^\(([\d\.e\+-]+)dB,([\d\.e\+-]+)", element):
                        if(not (lname+" dB") in step):
                            step[lname+" dB"] = []
                            step[lname+" deg"] = []
                        step[lname+" dB"].append(float(ere.last_match[1]));
                        step[lname+" deg"].append(float(ere.last_match[2]));
                    else:
                        raise RuntimeError("Unknown/Not configured parsing element!");
    
    return plot_data;

def decorate_ax(ax, plot_config):
    if(plot_config.get('show_title', False)):
        ax.set_title(plot_config['title']);

    ax.set_xlabel(plot_config['xlabel']);
    ax.set_ylabel(plot_config['ylabel']);
    
    if('yscale' in plot_config):
        ax.set_yscale(plot_config['yscale']);
    if('xscale' in plot_config):
        ax.set_xscale(plot_config['xscale']);

    if('xmin' in plot_config):
        ax.set_xlim(left=plot_config['xmin'])
    if('xmax' in plot_config):
        ax.set_xlim(right=plot_config['xmax'])

    if('ymin' in plot_config):
        ax.set_ylim(bottom=plot_config['ymin']);
    if('ymax' in plot_config):
        ax.set_ylim(top=plot_config['ymax']);

    if('xformatter' in plot_config):
        if('engineering' == plot_config['xformatter']):
            formatter = EngFormatter(places=plot_config.get('xplaces', 0), sep="\N{THIN SPACE}")
            ax.xaxis.set_major_formatter(formatter)

    if('yformatter' in plot_config):
        if('engineering' == plot_config['yformatter']):
            formatter = EngFormatter(places=plot_config.get('yplaces', 0))
            ax.yaxis.set_major_formatter(formatter)

    legend = ax.legend();
    hp = legend._legend_box.get_children()[1]
    for vp in hp.get_children():
        for row in vp.get_children():
            row.set_width(350)  # need to adapt this manually
            row.mode= "expand"
            row.align="right"

    ax.grid(True);

def plot_single_graph(fig, plot_config, plot_data):
    ax = fig.add_subplot();
    
    x_key = plot_config['x_key'] or 'x'
    y_key = plot_config['y_key'] or 'y'

    plot_data = plot_data['steps'][0]

    x_data = plot_data[x_key];
    y_data = plot_data[y_key];

    ax.plot(x_data, y_data);

    if(not 'xformatter' in plot_config):
        plot_config['xformatter'] = 'engineering';

    decorate_ax(ax, plot_config)

def plot_lt_sweep(fig, plot_config, plot_data):
    step_keys = plot_data.keys();

    ax = fig.add_subplot();
    ax.set_xscale('log');

    x_key = "Freq.";
    if("x_key" in plot_config):
        x_key = plot_config['x_key'];
    
    y_key = None;
    if('y_key' in plot_config):
        y_key = plot_config['y_key'];
    
    if(y_key == None):
        raise RuntimeError("No Y-Data Key (`y_key`) specified for plot!")


    if(plot_config.get('colourmap', 'coolwarm') == 'coolwarm'):
        num_steps = len(plot_data['steps'])
        cmap = plt.cm.coolwarm

        for idx, step in enumerate(plot_data['steps']):
            ax.plot(step[x_key], step[y_key], color=cmap(idx/(num_steps-1)), label=step['step']);
    else:
        for idx, step in enumerate(plot_data['steps']):
            ax.plot(step[x_key], step[y_key], label=step['step']);

    if(not 'xformatter' in plot_config):
        plot_config['xformatter'] = 'engineering';

    if(not 'xmin' in plot_config):
        plot_config['xmin'] = np.min(plot_data['steps'][0][x_key]);
    if(not 'xmax' in plot_config):
        plot_config['xmax'] = np.max(plot_data['steps'][0][x_key]);

    ax.legend();

    decorate_ax(ax, plot_config);

def perform_bandwidth_normalization(plot_data, plot_config):
    print("Normalizing bandwidth for all steps...")

    for step in plot_data['steps']:
        y_data = step[plot_config['y_key']]
        new_y_data = []

        for datapoint in y_data:
            new_y_data.append(datapoint - y_data[0])

        step[plot_config['y_key']] = new_y_data

def perform_processing_step(data_process_step, plot_data, plot_config):
    if(data_process_step == 'normalize_bandwidth'):
        perform_bandwidth_normalization(plot_data, plot_config)

def generate_plot(plot_config):
    global YAML_DIR;

    plot_data = None;
    if("load" in plot_config):
        if(not "loadtype" in plot_config):
            raise RuntimeError("Missing load type (`loadtype`) for plot config");
        
        if(plot_config['loadtype'] == 'ltspice'):
            plot_data = read_ltspice_file(os.path.join(YAML_DIR, plot_config['load']), plot_config);

        if(plot_config['loadtype'] == 'simplecsv'):
            plot_data = read_simplecsv(os.path.join(YAML_DIR, plot_config['load']), plot_config);

        if(plot_config['loadtype'] == 'multicsv'):
            plot_data = read_multicsv(plot_config)

    for data_process_step in plot_config.get('data_processing_steps', []):
        perform_processing_step(data_process_step, plot_data, plot_config)

    fig = plt.figure();

    if(plot_config['type'] == 'lt_sweep'):
        plot_lt_sweep(fig, plot_config, plot_data);

    if(plot_config['type'] == 'single'):
        plot_single_graph(fig, plot_config, plot_data);

    fig.subplots_adjust(0.15, 0.12, 0.96, 0.9)

    fig.savefig(os.path.join(YAML_DIR, plot_config['ofile']), dpi=plot_config.get('dpi', 300));

    matplotlib.pyplot.close()
    


INPUT_YAML_FILE = SCRIPT_DIR + "/plots.yml" if (len(sys.argv) <= 1) else sys.argv[1];
YAML_DIR = os.path.dirname(INPUT_YAML_FILE);
PLOT_CONFIG = None;

print(f"Reading YAML config {INPUT_YAML_FILE}");

with open(INPUT_YAML_FILE, "r") as file:
    PLOT_CONFIG = yaml.load(file, yaml.Loader);


for plot in PLOT_CONFIG['plots']:
    plot = {**PLOT_CONFIG['defaults'], **plot};

    generate_plot(plot);