#include <systemc>

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

/*
 * RTL implementation 
 * one clock cycle per iteration
 * using 1 combined signal for x_sel and y_sel called xy_sel
 */

#define trace
#ifdef trace
// global trace file pionter
auto tf = sc_create_vcd_trace_file("trace");
#endif

// simple RTL components in SystemC needed by gcd

template<unsigned int N>
SC_MODULE(sub) {
    sc_in<sc_uint<N>> a, b;
    sc_out<sc_uint<N>> s;
    SC_CTOR(sub) {
        SC_METHOD(on_input_change);
        sensitive << a << b;
    }
private:
    void on_input_change() {
        s.write(a.read() - b.read());
    }
};

template<unsigned int N>
SC_MODULE(is_ne) {
    sc_in<sc_uint<N>> a, b;
    sc_out<bool> ne;
    SC_CTOR(is_ne) {
        SC_METHOD(on_input_change);
        sensitive << a << b;
    }
private:
    void on_input_change() {
        ne.write(a.read() != b.read());
    }
};

template<unsigned int N>
SC_MODULE(is_hi) {
    sc_in<sc_uint<N>> a, b;
    sc_out<bool> hi;
    SC_CTOR(is_hi) {
        SC_METHOD(on_input_change);
        sensitive << a << b;
    }
private:
    void on_input_change() {
        hi.write(a.read() > b.read());
    }
};

template<unsigned int N>
SC_MODULE(mux) {
    sc_in<bool> sel;
    sc_in<sc_uint<N>> i1, i0;
    sc_out<sc_uint<N>> o;
    SC_CTOR(mux) {
        SC_METHOD(on_input_change);
        sensitive << i1 << i0 << sel;
    }
private:
    void on_input_change() {
        if (sel.read())
            o.write(i1.read());
        else
            o.write(i0.read());
    }
};

template<unsigned int N>
SC_MODULE(reg) {
    sc_in_clk clk;
    sc_in<bool> reset, ld;
    sc_in<sc_uint<N>> i;
    sc_out<sc_uint<N>> q;
    SC_CTOR(reg) {
        SC_METHOD(on_clk_pos_or_reset_pos);
        sensitive << clk.pos() << reset.pos();
    }
private:
    void on_clk_pos_or_reset_pos() {
        if (reset.read())
            q.write(0);
        else
            if (ld.read())
                q.write(i.read());
    }
};

// gcd

template<unsigned int N>
SC_MODULE(gcd_datapath) {
    sc_in_clk clk;
    sc_in<bool> reset, xy_sel, x_ld, y_ld, r_ld;
    sc_in<sc_uint<N>> x_i, y_i;
    sc_out<sc_uint<N>> r_o;
    sc_out<bool> x_ne_y, x_hi_y;
private:
    mux<N> x_mux, y_mux;
    reg<N> x_reg, y_reg, r_reg;
    is_ne<N> x_is_ne_y;
    is_hi<N> x_is_hi_y;
    sub<N> x_sub, y_sub;
    sc_signal<sc_uint<N>> x_mux_o, y_mux_o, x_reg_o, y_reg_o, x_sub_o, y_sub_o;
public:
    SC_CTOR(gcd_datapath):
            x_mux("x_mux"), y_mux("y_mux"),
            x_reg("x_reg"), y_reg("y_reg"), r_reg("r_reg"),
            x_is_ne_y("x_is_ne_y"), x_is_hi_y("x_is_hi_y"),
            x_sub("x_sub"), y_sub("y_sub") {
        x_mux.i0(x_i);
        x_mux.i1(x_sub_o);
        x_mux.sel(xy_sel);
        x_mux.o(x_mux_o);
        y_mux.i0(y_i);
        y_mux.i1(y_sub_o);
        y_mux.sel(xy_sel);
        y_mux.o(y_mux_o);
        x_reg.clk(clk);
        x_reg.reset(reset);
        x_reg.ld(x_ld);
        x_reg.i(x_mux_o);
        x_reg.q(x_reg_o);
        y_reg.clk(clk);
        y_reg.reset(reset);
        y_reg.ld(y_ld);
        y_reg.i(y_mux_o);
        y_reg.q(y_reg_o);
        x_is_ne_y.a(x_reg_o);
        x_is_ne_y.b(y_reg_o);
        x_is_ne_y.ne(x_ne_y);
        x_is_hi_y.a(x_reg_o);
        x_is_hi_y.b(y_reg_o);
        x_is_hi_y.hi(x_hi_y);
        x_sub.a(x_reg_o);
        x_sub.b(y_reg_o);
        x_sub.s(x_sub_o);
        y_sub.a(y_reg_o);
        y_sub.b(x_reg_o);
        y_sub.s(y_sub_o);
        r_reg.clk(clk);
        r_reg.reset(reset);
        r_reg.ld(r_ld);
        r_reg.i(x_reg_o);
        r_reg.q(r_o);
#ifdef trace
        sc_trace(tf, x_reg_o, "x_reg");
        sc_trace(tf, y_reg_o, "y_reg");
#endif
    }
};

SC_MODULE(gcd_control){
    sc_in_clk clk;
    sc_in<bool> reset, go_i, x_ne_y, x_hi_y;
    sc_out<bool> done_o, xy_sel, x_ld, y_ld, r_ld;
    SC_CTOR(gcd_control) {
        SC_METHOD(on_clk_pos_or_reset_pos);
        sensitive << clk.pos() << reset.pos();
        SC_METHOD(on_control_input);
        sensitive << go_i << x_ne_y << x_hi_y << state;
#ifdef trace
        sc_trace(tf, x_ne_y, "x_ne_y");
        sc_trace(tf, x_hi_y, "x_hi_y");
        sc_trace(tf, xy_sel, "xy_sel");
        sc_trace(tf, x_ld, "x_ld");
        sc_trace(tf, y_ld, "y_ld");
        sc_trace(tf, r_ld, "r_ld");
        sc_trace(tf, state, "state");
        sc_trace(tf, nextstate, "nextstate");
#endif
    }
private:
    sc_signal<sc_uint<2>> state;
    sc_uint<2> nextstate;
    void on_clk_pos_or_reset_pos() {
        state.write(reset.read() ? 0 : nextstate);
    }
    void on_control_input() {
        xy_sel.write(false);
        x_ld.write(false);
        y_ld.write(false);
        r_ld.write(false);
        done_o.write(false);
        switch (state.read()) {
            case 0:
                if (go_i.read()) {
                    x_ld.write(true);
                    y_ld.write(true);
                    nextstate = 1;
                }
                else {
                    nextstate = 0;
                }
                break;
            case 1:
                if (x_ne_y.read()) {
                    if (x_hi_y.read()) {
                        x_ld.write(true);
                    }
                    else {
                        y_ld.write(true);
                    }
                    xy_sel.write(true);
                    nextstate = go_i.read() ? 1 : 0;
                }
                else {
                    r_ld.write(true);
                    nextstate = go_i.read() ? 2 : 0;
                }
                break;
            case 2:
                done_o.write(true);
                nextstate = go_i.read() ? 2 : 0;
                break;
            case 3:
                // should not happen
                nextstate = 0;
                break;
        }
    }
};

template<unsigned int N>
SC_MODULE(gcd) {
    sc_in_clk clk;
    sc_in<bool> reset, go_i;
    sc_in<sc_uint<N>> x_i, y_i;
    sc_out<bool> done_o;
    sc_out<sc_uint<N>> r_o;
private:
    gcd_datapath<N> datapath;
    gcd_control control;
    sc_signal<bool> xy_sel, x_ld, y_ld, r_ld, x_ne_y, x_hi_y;
public:
    SC_CTOR(gcd):
            datapath("datapath"), control("control") {
        datapath.clk(clk);
        datapath.reset(reset);
        datapath.x_i(x_i);
        datapath.y_i(y_i);
        datapath.r_o(r_o);
        datapath.xy_sel(xy_sel);
        datapath.x_ld(x_ld);
        datapath.y_ld(y_ld);
        datapath.r_ld(r_ld);
        datapath.x_ne_y(x_ne_y);
        datapath.x_hi_y(x_hi_y);

        control.clk(clk);
        control.reset(reset);
        control.done_o(done_o);
        control.go_i(go_i);
        control.xy_sel(xy_sel);
        control.x_ld(x_ld);
        control.y_ld(y_ld);
        control.r_ld(r_ld);
        control.x_ne_y(x_ne_y);
        control.x_hi_y(x_hi_y);
    }
};

SC_MODULE(tb_gcd) {
    sc_signal<sc_uint<32>> x_i, y_i, r_o;
    sc_clock clk;
    sc_signal<bool> reset, go_i, done_o;
    SC_CTOR(tb_gcd): clk("clk", 10, SC_NS, 0.5), gcd("gcd") {
        gcd.clk(clk);
        gcd.reset(reset);
        gcd.go_i(go_i);
        gcd.x_i(x_i);
        gcd.y_i(y_i);
        gcd.r_o(r_o);
        gcd.done_o(done_o);
        SC_THREAD(main);
        sensitive << done_o;
#ifdef trace
        sc_trace(tf, clk, "clk");
        sc_trace(tf, reset, "reset");
        sc_trace(tf, x_i, "x_i");
        sc_trace(tf, y_i, "y_i");
        sc_trace(tf, go_i, "go_i");
        sc_trace(tf, r_o, "r_o");
        sc_trace(tf, done_o, "done_o");
#endif
    }
private:
    gcd<32> gcd;
    void check(const sc_uint<32>& x, const sc_uint<32>& y, const sc_uint<32>& r) {
        auto start_time_stamp = sc_time_stamp();
        x_i.write(x);
        y_i.write(y);
        go_i.write(true);
        wait();
        assert(r_o.read() == r);
        auto end_time_stamp = sc_time_stamp();
        cout << "@: " << sc_time_stamp() 
             << ": gcd(" << x << "," << y << ") = " << r 
             << " duration: " << end_time_stamp - start_time_stamp << endl;
        go_i.write(false);
        wait();
    }
    void main() {
        check(0, 0, 0);
        check(234, 96, 6);
        check(12345, 67891, 1);
        check(12345, 67890, 15);
        check(12345, 12345, 12345);

        x_i.write(0);
        y_i.write(0);
        sc_stop();
    }
};

int sc_main(int argc, char* argv[]) {
    tb_gcd tb_gcd("tb_gcd");

#ifdef trace
    tf->set_time_unit(1, SC_NS);
#endif

    sc_start();

#ifdef trace
    sc_close_vcd_trace_file(tf);
#endif
    
    cin.get();
    return 0;
}
