//---------------------------------------------------------------------------
//
//	SCSI Target Emulator RaSCSI Reloaded
//	for Raspberry Pi
//
//	Powered by XM6 TypeG Technology.
//	Copyright (C) 2016-2020 GIMONS
//
//---------------------------------------------------------------------------

#include "os.h"
#include "log.h"
#include "hal/gpiobus.h"
#include "hal/gpiobus_factory.h"
#include "rascsi_version.h"
#include <sys/time.h>
#include <climits>
#include <csignal>
#include <sstream>
#include <iostream>
#include <getopt.h>
#include <sched.h>
#include "monitor/sm_reports.h"
#include "monitor/data_sample.h"

using namespace std;

static const int _MAX_FNAME = 256;

//---------------------------------------------------------------------------
//
//	Variable declarations
//
//---------------------------------------------------------------------------
static volatile bool running; // Running flag
shared_ptr<GPIOBUS> bus;			      // GPIO Bus

uint32_t buff_size = 1000000;
data_capture *data_buffer;
uint32_t data_idx = 0;

double ns_per_loop;

bool print_help = false;
bool import_data = false;

// We don't really need to support 256 character file names - this causes
// all kinds of compiler warnings when the log filename can be up to 256
// characters. _MAX_FNAME/2 is just an arbitrary value.
char file_base_name[_MAX_FNAME / 2] = "log";
char vcd_file_name[_MAX_FNAME];
char json_file_name[_MAX_FNAME];
char html_file_name[_MAX_FNAME];
char input_file_name[_MAX_FNAME];

//---------------------------------------------------------------------------
//
//	Signal Processing
//
//---------------------------------------------------------------------------
void KillHandler(int)
{
    // Stop instruction
    running = false;
}

void parse_arguments(int argc, char *argv[])
{
    int opt;

    while ((opt = getopt(argc, argv, "-Hhb:i:")) != -1)
    {
        switch (opt)
        {
        // The three options below are kind of a compound option with two letters
        case 'h':
        case 'H':
            print_help = true;
            break;
        case 'b':
            buff_size = atoi(optarg);
            break;
        case 'i':
            strncpy(input_file_name, optarg, sizeof(input_file_name)-1);
            import_data = true;
            break;
        case 1:
            strncpy(file_base_name, optarg, sizeof(file_base_name) - 5);
            break;
        default:
            cout << "default: " << optarg << endl;
            break;
        }
    }

    /* Process any remaining command line arguments (not options). */
    if (optind < argc) {
        while (optind < argc)
            strncpy(file_base_name, argv[optind++], sizeof(file_base_name)-1);
    }

    strcpy(vcd_file_name, file_base_name);
    strcat(vcd_file_name, ".vcd");
    strcpy(json_file_name, file_base_name);
    strcat(json_file_name, ".json");
    strcpy(html_file_name, file_base_name);
    strcat(html_file_name, ".html");
}
//---------------------------------------------------------------------------
//
//	Copyright text
//
//---------------------------------------------------------------------------
void print_copyright_text(int, char *[])
{
    LOGINFO("SCSI Monitor Capture Tool - part of RaSCSI(*^..^*) ")
    LOGINFO("version %s (%s, %s)",
            rascsi_get_version_string().c_str(),
            __DATE__,
            __TIME__)
    LOGINFO("Powered by XM6 TypeG Technology ")
    LOGINFO("Copyright (C) 2016-2020 GIMONS")
    LOGINFO("Copyright (C) 2020-2022 Contributors to the RaSCSI project")
    LOGINFO(" ")
}

//---------------------------------------------------------------------------
//
//	Help text
//
//---------------------------------------------------------------------------
void print_help_text(int, char *argv[])
{
    LOGINFO("%s -i [input file json] -b [buffer size] [output file]", argv[0])
    LOGINFO("       -i [input file json] - scsimon will parse the json file instead of capturing new data")
    LOGINFO("                              If -i option is not specified, scsimon will read the gpio pins")
    LOGINFO("       -b [buffer size]     - Override the default buffer size of %d.", buff_size)
    LOGINFO("       [output file]        - Base name of the output files. The file extension (ex: .json)")
    LOGINFO("                              will be appended to this file name")
}

//---------------------------------------------------------------------------
//
//	Banner Output
//
//---------------------------------------------------------------------------
void Banner(int, char *[])
{
    if (import_data) {
        LOGINFO("Reading input file: %s", input_file_name)
    }
    else {
        LOGINFO("Reading live data from the GPIO pins")
        LOGINFO("    Connection type : %s", bus->GetConnectDesc().c_str())
    }
    LOGINFO("    Data buffer size: %u", buff_size)
    LOGINFO(" ")
    LOGINFO("Generating output files:")
    LOGINFO("   %s - Value Change Dump file that can be opened with GTKWave", vcd_file_name)
    LOGINFO("   %s - JSON file with raw data", json_file_name)
    LOGINFO("   %s - HTML file with summary of commands", html_file_name)
}

//---------------------------------------------------------------------------
//
//	Initialization
//
//---------------------------------------------------------------------------
bool Init()
{
    // Interrupt handler settings
    if (signal(SIGINT, KillHandler) == SIG_ERR) {
        return false;
    }
    if (signal(SIGHUP, KillHandler) == SIG_ERR) {
        return false;
    }
    if (signal(SIGTERM, KillHandler) == SIG_ERR) {
        return false;
    }

    // GPIO Initialization
    bus = GPIOBUS_Factory::Create();
    if (!bus->Init())
    {
        LOGERROR("Unable to intiailize the GPIO bus. Exiting....")
        return false;
    }

    // Bus Reset
    bus->Reset();

    // Other
    running = false;

    return true;
}

void Cleanup()
{
    if (!import_data) {
        LOGINFO("Stopping data collection....")
    }
    LOGINFO(" ")
    LOGINFO("Generating %s...", vcd_file_name)
    scsimon_generate_value_change_dump(vcd_file_name, data_buffer, data_idx);
    LOGINFO("Generating %s...", json_file_name)
    scsimon_generate_json(json_file_name, data_buffer, data_idx);
    LOGINFO("Generating %s...", html_file_name)
    scsimon_generate_html(html_file_name, data_buffer, data_idx);

    // Cleanup the Bus
    bus->Cleanup();
}

void Reset()
{
    // Reset the bus
    bus->Reset();
}

//---------------------------------------------------------------------------
//
//	Pin the thread to a specific CPU (Only applies to Linux)
//
//---------------------------------------------------------------------------
#ifdef __linux__
void FixCpu(int cpu)
{
    // Get the number of CPUs
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    sched_getaffinity(0, sizeof(cpu_set_t), &cpuset);
    int cpus = CPU_COUNT(&cpuset);

    // Set the thread affinity
    if (cpu < cpus)
    {
        CPU_ZERO(&cpuset);
        CPU_SET(cpu, &cpuset);
        sched_setaffinity(0, sizeof(cpu_set_t), &cpuset);
    }
}
#endif

#ifdef DEBUG
static uint32_t high_bits = 0x0;
static uint32_t low_bits = 0xFFFFFFFF;
#endif

//---------------------------------------------------------------------------
//
//	Main processing
//
//---------------------------------------------------------------------------
int main(int argc, char *argv[])
{

#ifdef DEBUG
    spdlog::set_level(spdlog::level::trace);
#else
    spdlog::set_level(spdlog::level::info);
#endif
    spdlog::set_pattern("%^[%l]%$ %v");

    print_copyright_text(argc, argv);
    parse_arguments(argc, argv);

#ifdef DEBUG
    uint32_t prev_high = high_bits;
    uint32_t prev_low = low_bits;
#endif
    ostringstream s;
    uint32_t prev_sample = 0xFFFFFFFF;
    uint32_t this_sample = 0;
    timeval start_time;
    timeval stop_time;
    uint64_t loop_count = 0;
    timeval time_diff;
    uint64_t elapsed_us;

    if (print_help)
    {
        print_help_text(argc, argv);
        exit(0);
    }

    // Output the Banner
    Banner(argc, argv);

    data_buffer = (data_capture *)calloc(buff_size, sizeof(data_capture_t));

    if (import_data)
    {
        data_idx = scsimon_read_json(input_file_name, data_buffer, buff_size);
        if (data_idx > 0)
        {
            LOGDEBUG("Read %d samples from %s", data_idx, input_file_name)
            Cleanup();
        }
        exit(0);
    }

    LOGINFO(" ")
    LOGINFO("Now collecting data.... Press CTRL-C to stop.")
    LOGINFO(" ")

    // Initialize
    int ret = 0;
    if (!Init()) {
        ret = EPERM;
        goto init_exit;
    }

    // Reset
    Reset();

#ifdef __linux__
    // Set the affinity to a specific processor core
    FixCpu(3);

    // Scheduling policy setting (highest priority)
    struct sched_param schparam;
    schparam.sched_priority = sched_get_priority_max(SCHED_FIFO);
    sched_setscheduler(0, SCHED_FIFO, &schparam);
#endif

    // Start execution
    running = true;
    bus->SetACT(false);

    (void)gettimeofday(&start_time, nullptr);

    // LOGDEBUG("ALL_SCSI_PINS %08X\n", ALL_SCSI_PINS)

    // Main Loop
    while (running)
    {
        // Work initialization
        this_sample = (bus->Acquire());// & ALL_SCSI_PINS);
        loop_count++;
        if (loop_count > LLONG_MAX - 1)
        {
            LOGINFO("Maximum amount of time has elapsed. SCSIMON is terminating.")
            running = false;
        }
        if (data_idx >= (buff_size - 2))
        {
            LOGINFO("Internal data buffer is full. SCSIMON is terminating.")
            running = false;
        }

        if (this_sample != prev_sample)
        {

#ifdef DEBUG
            // This is intended to be a debug check to see if every pin is set
            // high and low at some point.
            high_bits |= this_sample;
            low_bits &= this_sample;
            if ((high_bits != prev_high) || (low_bits != prev_low))
            {
                LOGDEBUG("   %08X    %08X\n", high_bits, low_bits)
            }
            prev_high = high_bits;
            prev_low = low_bits;
            if ((data_idx % 1000) == 0)
            {
                s.str("");
                s << "Collected " << data_idx << " samples...";
                LOGDEBUG("%s", s.str().c_str())
            }
#endif
            data_buffer[data_idx].data = this_sample;
            data_buffer[data_idx].timestamp = loop_count;
            data_idx++;
            prev_sample = this_sample;
        }

        continue;
    }

    // Collect one last sample, otherwise it looks like the end of the data was cut off
    if (data_idx < buff_size)
    {
        data_buffer[data_idx].data = this_sample;
        data_buffer[data_idx].timestamp = loop_count;
        data_idx++;
    }

    (void)gettimeofday(&stop_time, nullptr);

    timersub(&stop_time, &start_time, &time_diff);

    elapsed_us = ((time_diff.tv_sec * 1000000) + time_diff.tv_usec);
    s.str("");
    s << "Elapsed time: " << elapsed_us << " microseconds (" << elapsed_us / 1000000 << " seconds)";
    LOGINFO("%s", s.str().c_str())
    s.str("");
    s << "Collected " << data_idx << " changes";
    LOGINFO("%s", s.str().c_str())

    // Note: ns_per_loop is a global variable that is used by Cleanup() to printout the timestamps.
    ns_per_loop = (elapsed_us * 1000) / (double)loop_count;
    s.str("");
    s << "Read the SCSI bus " << loop_count << " times with an average of " << ns_per_loop << " ns for each read";
    LOGINFO("%s", s.str().c_str())

    Cleanup();

init_exit:
    exit(ret);
}
