/* 
 * File:   mega_meg44pan_simulator.cpp
 * Author: Jan Hudec
 * 
 * Created on 16. května 2019, 17:20
 */

#include "mega_meg44pan_simulator.h"

// include necessary libraries
#include <fstream>
#include <iostream>
#include <sstream>
#include <time.h>
#include <math.h>

mega_meg44pan_simulator::mega_meg44pan_simulator()
{
    // seed random function
    srand(time(NULL));
    
    // initialize class variables
    this->_voltages = { };
    this->_interface = "ens33";
    this->_vlan_priority = 4;
    this->_vlan_id = 0;
    this->_sv_app_id = 0x4001;
    this->_sv_mac[0] = 0x00;
    this->_sv_mac[1] = 0x01;
    this->_sv_ratio = 100;
    this->_goose_app_id = 0x1001;
    this->_goose_mac[0] = 0x00;
    this->_goose_mac[1] = 0x01;
    this->_goose_time_min = 5;
    this->_goose_time_max = 100;
    this->_sensors = 1;
    this->_voltage_center = 230.0f;
    this->_voltage_inner_bound = 231.0f;
    this->_voltage_outer_bound = 235.0f;
    this->_inner_to_outer_ratio = 5;
    this->_time_factor = 1;
    this->_t = time(NULL);
    this->_ti = *localtime(&this->_t);
    this->_monitoring_time = 1;
    this->spike = false;
}

mega_meg44pan_simulator::~mega_meg44pan_simulator()
{
    SVPublisher_destroy(this->_sv_pub);
    GoosePublisher_destroy(this->_gp_pub);
    LinkedList_destroyDeep(this->_dataset, (LinkedListValueDeleteFunction) MmsValue_delete);
}

void mega_meg44pan_simulator::parse_config(std::string filename)
{
    // load config file
    std::ifstream file(filename);
    std::string str;
    std::vector<std::string> parse;

    // read content line by line
    while (std::getline(file, str))
    {
        // skip commented lines
        if (str[0] != '#')
        {
            // split to key and value by '=' char
            int pos = str.find('=');
            if (pos != -1)
            {
                parse.clear();
                parse.push_back(str.substr(0, pos));
                parse.push_back(str.substr(pos + 1, str.length()));

                // assign values to class variables
                if      (parse[0] == "interface")           this->_interface = parse[1];
                else if (parse[0] == "vlan_priority")       this->_vlan_priority = stoi(parse[1]);
                else if (parse[0] == "vlan_id")             this->_vlan_id = stoi(parse[1]);
                else if (parse[0] == "sv_app_id")           this->_sv_app_id = strtol(parse[1].c_str(), NULL, 16);
                else if (parse[0] == "sv_mac_5")            this->_sv_mac[0] = strtol(parse[1].c_str(), NULL, 16);
                else if (parse[0] == "sv_mac_6")            this->_sv_mac[1] = strtol(parse[1].c_str(), NULL, 16);
                else if (parse[0] == "sv_ratio")            this->_sv_ratio = stoi(parse[1]);
                else if (parse[0] == "goose_app_id")        this->_goose_app_id = strtol(parse[1].c_str(), NULL, 16);
                else if (parse[0] == "goose_mac_5")         this->_goose_mac[0] = strtol(parse[1].c_str(), NULL, 16);
                else if (parse[0] == "goose_mac_6")         this->_goose_mac[1] = strtol(parse[1].c_str(), NULL, 16);
                else if (parse[0] == "goose_time_min")      this->_goose_time_min = stoi(parse[1]);
                else if (parse[0] == "goose_time_max")      this->_goose_time_max = stoi(parse[1]);
                else if (parse[0] == "monitoring_time")     this->_monitoring_time = stoi(parse[1]);
                else if (parse[0] == "time_factor")         this->_time_factor = stoi(parse[1]);
                else if (parse[0] == "sensors")             this->_sensors = stoi(parse[1]);				
                else if (parse[0] == "voltage_center")      this->_voltage_center = stof(parse[1]);				
                else if (parse[0] == "voltage_inner_bound") this->_voltage_inner_bound = stof(parse[1]);
                else if (parse[0] == "voltage_outer_bound") this->_voltage_outer_bound = stof(parse[1]);
                else if (parse[0] == "inner_to_outer_ratio")this->_inner_to_outer_ratio = stof(parse[1]);
                else if (parse[0] == "year")                this->_ti.tm_year = stoi(parse[1]) - 1900;
                else if (parse[0] == "month")               this->_ti.tm_mon = stoi(parse[1]) - 1;
                else if (parse[0] == "day")                 this->_ti.tm_mday = stoi(parse[1]);
                else if (parse[0] == "hours")               this->_ti.tm_hour = stoi(parse[1]);
                else if (parse[0] == "minutes")             this->_ti.tm_min = stoi(parse[1]);
                else if (parse[0] == "seconds")             this->_ti.tm_sec = stoi(parse[1]);
            }
        }
    }

    // check monitoring_time value
    if(this->_monitoring_time < 1)
        throw std::runtime_error("Monitoring time value has to be at least 1 second!");

    // check time_factor value
    if (this->_time_factor < 1)
        throw std::runtime_error("Time factor value has to be at least 1!");

    // throw exception when more than 12 sensors
    if (this->_sensors > 3)
        throw std::runtime_error("Too many sensors!");
    
    // check for voltage values succession
    if (this->_voltage_center < this->_voltage_outer_bound) // ie. 230-231-235 
        if ( !(this->_voltage_center < this->_voltage_inner_bound && this->_voltage_inner_bound < this->_voltage_outer_bound) )
            throw std::runtime_error("Inner bound has to be between voltage value and outer bound!");
        else {}
    else // ie. 230-229-225
        if ( !(this->_voltage_outer_bound < this->_voltage_inner_bound && this->_voltage_inner_bound < this->_voltage_center) )
            throw std::runtime_error("Inner bound has to be between voltage value and outer bound!");
        
    // check time values
    if (70 <= this->_ti.tm_year && this->_ti.tm_year <= 138 &&
        0 <= this->_ti.tm_mon  && this->_ti.tm_mon  <= 11  &&
        1 <= this->_ti.tm_mday && this->_ti.tm_mday <= 31  &&
        0 <= this->_ti.tm_hour && this->_ti.tm_hour <= 23  &&
        0 <= this->_ti.tm_min  && this->_ti.tm_min  <= 59  &&
        0 <= this->_ti.tm_sec  && this->_ti.tm_sec  <= 60)
    {
        // update timestamp variable
        this->_t = mktime(&this->_ti);

        for (int i = 0; i < this->_sensors; i++)
        {
            if (7 >= this->_ti.tm_hour && this->_ti.tm_hour <= 15)
                this->_voltages.push_back({ std::pair<time_t, float>(0, 1) });
            else
                this->_voltages.push_back({ std::pair<time_t, float>(0, 0) });
        }
    }
    else
    {
        throw std::runtime_error("Wrong time values!");
    }
    
    init_iec();
    
    // create asdu for each sensor
    std::string name;
    for(int i = 0; i < this->_sensors; i++)
    {
        name = "MEg44PAN_" + std::to_string(i + 1);
        
        // create asdu
        SVPublisher_ASDU asdu = SVPublisher_addASDU(this->_sv_pub, name.c_str(), NULL, 1);
        this->_asdu_units.insert(std::pair<std::string, SVPublisher_ASDU>(name, asdu));
        this->_asdu_units_offsets[name] = {};
           
        // reserve memory for int
        this->_asdu_units_offsets[name].push_back(
            SVPublisher_ASDU_addINT8(this->_asdu_units[name])
        );
        
        // reserve memory for float
        this->_asdu_units_offsets[name].push_back(
            SVPublisher_ASDU_addFLOAT(this->_asdu_units[name])
        );
        
        // reserve memory for quality
        this->_asdu_units_offsets[name].push_back(
            SVPublisher_ASDU_addQuality(this->_asdu_units[name])
        );
        
        // reserve memory for timestamp
        this->_asdu_units_offsets[name].push_back(
            SVPublisher_ASDU_addTimestamp(this->_asdu_units[name])
        );
    }

    SVPublisher_setupComplete(this->_sv_pub);
}

void mega_meg44pan_simulator::init_iec()
{
    char* interface = strdup(this->_interface.data());
    interface[strlen(interface) - 1] = '\0';
    
    uint8_t sv_mac_4 = 0x04;
    uint8_t goose_mac_4 = 0x01;
    
    CommParameters cp;
    cp.vlanPriority = this->_vlan_priority;
    cp.vlanId = this->_vlan_id;
    cp.appId = this->_sv_app_id;
    cp.dstAddress[0] = 0x01;
    cp.dstAddress[1] = 0x0c;
    cp.dstAddress[2] = 0xcd;
    cp.dstAddress[3] = sv_mac_4;
    cp.dstAddress[4] = this->_sv_mac[0];
    cp.dstAddress[5] = this->_sv_mac[1];
    
    this->_sv_pub = SVPublisher_create(&cp, interface);
    
    cp.appId = this->_goose_app_id;
    cp.dstAddress[3] = goose_mac_4;
    cp.dstAddress[4] = this->_goose_mac[0];
    cp.dstAddress[5] = this->_goose_mac[1];
    
    this->_gp_pub = GoosePublisher_create(&cp, interface);
    
    GoosePublisher_setGoID(this->_gp_pub, "MEg44PANLD0/LLN0.Temperature");
    GoosePublisher_setGoCbRef(this->_gp_pub, "MEg44PANLD0/LLN0$GO$Temperature");
    GoosePublisher_setDataSetRef(this->_gp_pub, "MEg44PANLD0/LLN0$XARGGIO130");
    GoosePublisher_setTimeAllowedToLive(this->_gp_pub, (1.5 * this->_goose_time_max));
    GoosePublisher_setConfRev(this->_gp_pub, 1);
    GoosePublisher_setSimulation(this->_gp_pub, 0);
    GoosePublisher_setNeedsCommission(this->_gp_pub, 0);
    
    this->_goose_counter = 0;
    this->time_treshold = this->_goose_time_max;
    
    this->_dataset = LinkedList_create();
    this->_dataset_sp = LinkedList_create();
}

void mega_meg44pan_simulator::save_voltages(std::string filename)
{
    // create stringstream and write all values accordingly
    std::stringstream ss;
    ss.fixed;
    ss.precision(4);

    std::map<time_t, float>::iterator it;

    for (int i = 0; i < this->_sensors; i++)
    {
        ss << i + 1 << ":";

        for (it = ++this->_voltages[i].begin(); it != this->_voltages[i].end(); it++)
        {
            ss << it->first << "=" << it->second << ",";
        }

        // move write head one char back so last comma will be overwritten
        ss.seekp(-1, ss.cur);
        ss << std::endl;
    }
    std::string s = ss.str();
    //std::cout << s; // debug output of values

    // save output to file
    std::ofstream outfile(filename);
    outfile << ss.rdbuf();
}

void mega_meg44pan_simulator::generate_voltages()
{
    // assign low and high values for random function based on values from captured packets
    //rand_low = 0.08f;
    //rand_high = 0.54f;
    
    float growth,
        multiplier,
        voltage_new,
        voltage_spike;
    int chance;
    
    float inner_difference = std::abs(this->_voltage_center - this->_voltage_inner_bound);
    float outer_difference = std::abs(this->_voltage_center - this->_voltage_outer_bound);
    
    float differences[] =
    {
        0,
        std::min(inner_difference, outer_difference),
        std::max(inner_difference, outer_difference)
    }; // center voltage, inner bound, outer bound
    
    multiplier = differences[2] / differences[1];
    
    // remove old entries from GOOSE dataset
    LinkedList_destroy(this->_dataset);
    this->_dataset = LinkedList_create();
    
    if(this->spike)
    {
        // increase stNum and reset sqNum
        GoosePublisher_increaseStNum(this->_gp_pub);
    }
    
    for(int i = 0; i < this->_sensors; i++)
    {
        growth = static_cast<float>(rand()) / (static_cast<float>(RAND_MAX/differences[1]));
        chance = rand() % static_cast<int>(100 / this->_inner_to_outer_ratio);

        if(chance == 1) // chance inner_to_outer_ratio to get 1 in 100 tries
        {
            growth *= multiplier;
        }

        voltage_new = (rand() % 2 == 1) ? 230 + growth : 230 + (-growth);
        
        if(this->spike == true)
        {
            voltage_spike = static_cast<float>(rand()) / (static_cast<float>(RAND_MAX/(voltage_new * 0.2)));
            voltage_new = (rand() % 2 == 1) ? voltage_new + voltage_spike : voltage_new + (-voltage_spike);
        }
        
        this->_voltages[i].insert(std::pair<time_t, float>(this->_t, voltage_new));
        
        // prepare sample values
        std::string name = "MEg44PAN_" + std::to_string(i + 1);

        SVPublisher_ASDU_setINT8(
            this->_asdu_units[name],
            this->_asdu_units_offsets[name][0],
            i + 1
        );

        SVPublisher_ASDU_setFLOAT(
            this->_asdu_units[name],
            this->_asdu_units_offsets[name][1],
            voltage_new
        );

        SVPublisher_ASDU_setQuality(
            this->_asdu_units[name],
            this->_asdu_units_offsets[name][2],
            QUALITY_VALIDITY_GOOD
        );

        Timestamp ts;
        Timestamp_setTimeInSeconds(&ts, this->_t);
        SVPublisher_ASDU_setTimestamp(
            this->_asdu_units[name],
            this->_asdu_units_offsets[name][3],
            ts
        );
        
        // add values to GOOSE dataset
        LinkedList_add(this->_dataset, MmsValue_newIntegerFromInt8(i + 1));
        LinkedList_add(this->_dataset, MmsValue_newFloat(voltage_new));
        LinkedList_add(this->_dataset, MmsValue_newUtcTime(this->_t));
        
        // manual spike
        if(this->spike == true)
        {
            printf("\nAlert - manual spike in voltage!\n"
                   "  Spike value: %f\tID: %i\tTimestamp: %ld \n", voltage_new, i + 1, static_cast<long>(this->_t));
            
            // initialize retransmiting of GOOSE message
            retransmit_goose(i, voltage_new, this->_t);
        }
    }
    
    this->spike = false;
    
    // update timestamp and save it into tm struct
    this->_t += this->_monitoring_time * this->_time_factor;
    this->_ti = *localtime(&this->_t);
    
    // publish sv every x GOOSE messages
    if(this->_goose_counter % this->_sv_ratio == 0)
    {
        SVPublisher_publish(this->_sv_pub);
        
        // increment SmpCnt counter
        std::map<std::string, SVPublisher_ASDU>::iterator it2;
        for(it2 = this->_asdu_units.begin(); it2 != this->_asdu_units.end(); it2++)
        {
            SVPublisher_ASDU_increaseSmpCnt(it2->second);
        }
    }
    
    // publish GOOSE frame
    GoosePublisher_publish(this->_gp_pub, this->_dataset);
    
    // increment goose_counter
    this->_goose_counter += 1;
}

void mega_meg44pan_simulator::retransmit_goose(int i, float temp, time_t t)
{
    static int iterations;
    
    if(iterations == 0)
    {
        while(this->_goose_time_min * std::pow(2, iterations) < this->_goose_time_max)
        {
            iterations += 1;   
        }
    }
    
    for(int j = 0; j < iterations + 1; j++)
    {
        // remove old entries from GOOSE dataset_sp
        LinkedList_destroy(this->_dataset_sp);
        this->_dataset_sp = LinkedList_create();

        // prepare new GOOSE messages
        LinkedList_add(this->_dataset_sp, MmsValue_newIntegerFromInt8(i + 1));
        LinkedList_add(this->_dataset_sp, MmsValue_newFloat(temp));
        LinkedList_add(this->_dataset_sp, MmsValue_newUtcTime(t));

        // publish immediately
        GoosePublisher_publish(this->_gp_pub, this->_dataset_sp);
        
        // wait for double time each iteration
        if(j < iterations)
        {
            uint64_t time_old = Hal_getTimeInMs();
            while(Hal_getTimeInMs() - time_old < this->_goose_time_min * std::pow(2, j));
        }
    }
}