// For std::unique_ptr
#include <memory>

// SystemC global header
#include <systemc>

// Include common routines
#include <verilated.h>
#include <verilated_vcd_sc.h>

#include <sys/stat.h>  // mkdir

// Include model header, generated from Verilating "isp.v"
#include "Visp.h"

// Handle file
#include <fstream>
#include <iostream>

#define IM_WIDTH 1936
#define IM_HEIGHT 1088
#define IM_SIZE (IM_WIDTH * IM_HEIGHT)

using namespace std;
using namespace sc_core;
using namespace sc_dt;

SC_MODULE (TB_ISP) {
    sc_in_clk clk;
    sc_in<bool> reset;
    
    sc_in<bool> data_que;
    sc_out<bool> data_en;
    sc_out<uint32_t> data_out[3];

    sc_in<bool> im_clk;
    sc_in<bool> im_en;
    sc_in<uint32_t> im_data;

    uint16_t image[IM_HEIGHT][IM_WIDTH];
    uint32_t out[IM_SIZE] = {0};
    uint32_t out_head = 0;

    SC_CTOR (TB_ISP) {
        SC_CTHREAD(send_Data, clk.pos());
        reset_signal_is(reset, true);

        SC_CTHREAD(read_Data, im_clk.pos());
    }

    void send_Data(void) {
        uint16_t pos_x = 0, pos_y = 0;
        while (true)
        {
            if (data_que.read() && pos_y < IM_HEIGHT - 2) {
                data_en.write(1);

                printf("x=%3d, y=%3d, data=0x%04x\t", pos_x, pos_y, image[pos_y + 0][pos_x]);
                printf("x=%3d, y=%3d, data=0x%04x\t", pos_x, pos_y, image[pos_y + 1][pos_x]);
                printf("x=%3d, y=%3d, data=0x%04x\n", pos_x, pos_y, image[pos_y + 2][pos_x]);

                data_out[0].write(image[pos_y + 0][pos_x]);
                data_out[1].write(image[pos_y + 1][pos_x]);
                data_out[2].write(image[pos_y + 2][pos_x]);

                if (pos_x++ >= IM_WIDTH) {
                    pos_x = 0;
                    pos_y++;
                }
            } else {
                data_en.write(0);
            }

            wait();
        }
        
    }

    void read_Data(void) {
        while (true)
        {
            if (im_en.read()) {
                out[out_head++] = im_data.read();
            }

            wait();
        }
    }
};

int sc_main(int argc, char* argv[]) {
    // Open image
    ifstream in_image;
    ofstream out_image;
    in_image.open("./Demosaic/sim/transform/test.bin", ios::in | ios::binary);
    out_image.open("./out.bin", ios::out | ios::binary);
    if (!in_image.is_open()) {
        cout << "Open image fail" << endl;
        exit(0);
    } else {
        cout << "Ready to sim" << endl;
    }

    // Read image
    uint8_t buf[IM_SIZE * 2] = {0};
    in_image.read((char*)buf, IM_SIZE * 2);
    in_image.close();
    // Reshape data
    uint16_t image[IM_HEIGHT][IM_WIDTH] = {0};
    uint32_t i = 0;
    for (int y = 0; y < IM_HEIGHT; y++) {
        for (int x = 0; x < IM_WIDTH; x++) {
            image[y][x] = (uint16_t)buf[i] + ((uint16_t)buf[i + 1] << 8);
            i++;
        }
    }
    cout << "Finish Reading data" << endl;

    // This is a more complicated example, please also see the simpler examples/make_hello_c.

    // Create logs/ directory in case we have traces to put under it
    Verilated::mkdir("logs");

    // Set debug level, 0 is off, 9 is highest presently used
    // May be overridden by commandArgs argument parsing
    Verilated::debug(0);

    // Randomization reset policy
    // May be overridden by commandArgs argument parsing
    Verilated::randReset(2);

    // Before any evaluation, need to know to calculate those signals only used for tracing
    Verilated::traceEverOn(true);

    // Pass arguments so Verilated code can see them, e.g. $value$plusargs
    // This needs to be called before you create any model
    Verilated::commandArgs(argc, argv);

    // General logfile
    std::ios::sync_with_stdio();

    // Define clocks
    sc_clock clk{"clk", 10, SC_NS, 0.5, 3, SC_NS, true};
    // Define interconnect
    sc_signal<bool> reset;

    sc_signal<bool> data_en;
    sc_signal<bool> data_que;
    sc_signal<uint32_t> data_in[3];

    sc_signal<bool> out_clk;
    sc_signal<bool> out_en;
    sc_signal<uint32_t> data_out;


    // Construct the Verilated model, from inside Visp.h
    // Using unique_ptr is similar to "Visp* isp = new Visp" then deleting at end
    const std::unique_ptr<Visp> isp{new Visp{"isp"}};
    // Attach Visp's signals to this upper model
    isp->clk(clk);
    isp->reset(reset);
    isp->data_en(data_en);
    isp->data_que(data_que);
    isp->data_in[0](data_in[0]);
    isp->data_in[1](data_in[1]);
    isp->data_in[2](data_in[2]);
    isp->out_clk(out_clk);
    isp->out_en(out_en);
    isp->data_out(data_out);

    // Construct testbench module
    TB_ISP tb_isp("tb_isp");
    tb_isp.clk(clk);
    tb_isp.reset(reset);
    tb_isp.data_que(data_que);
    tb_isp.data_en(data_en);
    tb_isp.data_out[0](data_in[0]);
    tb_isp.data_out[1](data_in[1]);
    tb_isp.data_out[2](data_in[2]);
    tb_isp.im_clk(out_clk);
    tb_isp.im_en(out_en);
    tb_isp.im_data(data_out);
    memcpy(tb_isp.image, image, sizeof(image));

    // You must do one evaluation before enabling waves, in order to allow
    // SystemC to interconnect everything for testing.
    sc_start(SC_ZERO_TIME);

    // If verilator was invoked with --trace argument,
    // and if at run time passed the +trace argument, turn on tracing
    VerilatedVcdSc* tfp = nullptr;
    const char* flag = Verilated::commandArgsPlusMatch("trace");
    if (flag && 0 == std::strcmp(flag, "+trace")) {
        std::cout << "Enabling waves into logs/vlt_dump.vcd...\n";
        tfp = new VerilatedVcdSc;
        isp->trace(tfp, 99);  // Trace 99 levels of hierarchy
        Verilated::mkdir("logs");
        tfp->open("logs/vlt_dump.vcd");
    }

    // Simulate until $finish
    while (!Verilated::gotFinish()) {
        // Flush the wave files each cycle so we can immediately see the output
        // Don't do this in "real" programs, do it in an abort() handler instead
        if (tfp) tfp->flush();

        // Apply inputs
        if (sc_time_stamp() < sc_time(10, SC_NS)) {
            reset.write(1);  // Assert reset
        } else {
            reset.write(0);  // Deassert reset
        }

        // Simulate 1ns
        sc_start(1, SC_NS);
    }

    // Final model cleanup
    isp->final();

    // Close trace if opened
    if (tfp) {
        tfp->close();
        tfp = nullptr;
    }

    // Save output image
    for (int y = 0; y < IM_HEIGHT; y++)
        for(int x = 0; x < IM_WIDTH; x++)
            out_image.write((const char *)&tb_isp.out[y * IM_WIDTH + x], sizeof(tb_isp.out[0]));
    out_image.close();

    // Return good completion status
    return 0;
}
