use paste::paste;

#[derive(Clone, Copy, Debug, Default)]
pub struct InstructionData {
    core_indx: i32,
    rd: i32,
    r1: i32,
    //r2 imm mbiw imm_core
    r2_or_imm: i32,
    //offset_select imm_relu ibiw
    generic1: i32,
    //offset_value imm_group obiw
    generic2: i32,
    //imm_len
    generic3: i32,
}

impl InstructionData {
    pub fn core_indx(&self) -> i32 {
        self.core_indx
    }

    pub fn rd(&self) -> i32 {
        self.rd
    }

    pub fn r1(&self) -> i32 {
        self.r1
    }

    pub fn r2(&self) -> i32 {
        self.r2_or_imm
    }

    pub fn imm(&self) -> i32 {
        self.r2_or_imm
    }

    pub fn mbiw(&self) -> i32 {
        self.r2_or_imm
    }

    pub fn offset_select(&self) -> i32 {
        self.generic1
    }

    pub fn offset_value(&self) -> i32 {
        self.generic2
    }

    pub fn get_core_rd_r1(&self) -> (i32, i32, i32) {
        (self.core_indx, self.rd, self.r1)
    }

    pub fn get_core_rd_r1_r2(&self) -> (i32, i32, i32, i32) {
        (self.core_indx, self.rd, self.r1, self.r2_or_imm)
    }

    pub fn get_core_rd_imm(&self) -> (i32, i32, i32) {
        (self.core_indx, self.rd, self.r2_or_imm)
    }

    pub fn get_core_rd_r1_imm(&self) -> (i32, i32, i32, i32) {
        (self.core_indx, self.rd, self.r1, self.r2_or_imm)
    }

    pub fn get_core_rd_r1_r2_immlen_offset(&self) -> (i32, i32, i32, i32, i32, i32, i32) {
        (
            self.core_indx,
            self.rd,
            self.r1,
            self.r2_or_imm,
            self.generic3,
            self.generic1,
            self.generic2,
        )
    }

    pub fn get_core_rd_r1_mbiw_immrelu_immgroup(&self) -> (i32, i32, i32, i32, i32, i32) {
        (
            self.core_indx,
            self.rd,
            self.r1,
            self.r2_or_imm,
            self.generic1,
            self.generic2,
        )
    }

    pub fn get_ibiw_obiw(&self) -> (i32, i32) {
        (self.generic1, self.generic2)
    }

    pub fn imm_len(&self) -> i32 {
        self.generic3
    }

    pub fn imm_core(&self) -> i32 {
        self.r2_or_imm
    }

    pub(crate) fn get_core_immcore(&self) -> (i32, i32) {
        (self.core_indx, self.r2_or_imm)
    }
}

#[derive(Clone, Copy, Debug)]
enum Fixer {
    Fix(i32),
    Edit(i32),
}

impl Fixer {
    fn to_fix(self) -> Self {
        match self {
            Fixer::Fix(x) => Fixer::Fix(x),
            Fixer::Edit(x) => Fixer::Fix(x),
        }
    }

    fn to_edit(self) -> Self {
        match self {
            Fixer::Fix(x) => Fixer::Edit(x),
            Fixer::Edit(x) => Fixer::Edit(x),
        }
    }

    fn set(self, val: i32) -> Self {
        match self {
            Fixer::Fix(_) => Fixer::Fix(val),
            Fixer::Edit(_) => Fixer::Edit(val),
        }
    }

    fn get(&self) -> i32 {
        match self {
            Fixer::Fix(x) => *x,
            Fixer::Edit(x) => *x,
        }
    }

    fn clear(self) -> Self {
        match self {
            Fixer::Fix(x) => Fixer::Fix(x),
            Fixer::Edit(_) => Fixer::Edit(0),
        }
    }
}

impl Default for Fixer {
    fn default() -> Self {
        Fixer::Edit(0)
    }
}

#[derive(Clone, Copy, Debug, Default)]
pub struct InstructionDataBuilder {
    core_indx: Fixer,
    rd: Fixer,
    r1: Fixer,
    r2: Fixer,
    imm: Fixer,
    offset_select: Fixer,
    offset_value: Fixer,
    imm_len: Fixer,
    ibiw: Fixer,
    obiw: Fixer,
    mbiw: Fixer,
    imm_relu: Fixer,
    imm_group: Fixer,
    imm_core: Fixer,
}

macro_rules! common_getter_setter {
    ($id:ident) => {
        paste! {
            pub fn [<fix_ $id>](&mut self) -> &mut Self {
                self.[<$id>] = self.[<$id>].to_fix();
                self
            }

            pub fn [<edit_ $id>](&mut self) -> &mut Self {
                self.[<$id>] = self.[<$id>].to_edit();
                self
            }

            pub fn [<set_ $id>](&mut self, val: i32) -> &mut Self{
                self.[<$id>] = self.[<$id>].set(val);
                self
            }

            pub fn [<get_ $id>](&self) -> i32{
                self.[<$id>].get()
            }

            pub fn [<clear_ $id>](&mut self){
                self.[<$id>] = self.[<$id>].clear();
            }
        }
    };
}

impl InstructionDataBuilder {
    common_getter_setter![core_indx];
    common_getter_setter![rd];
    common_getter_setter![r1];
    common_getter_setter![r2];
    common_getter_setter![imm];
    common_getter_setter![offset_select];
    common_getter_setter![offset_value];
    common_getter_setter![imm_len];
    common_getter_setter![ibiw];
    common_getter_setter![obiw];
    common_getter_setter![mbiw];
    common_getter_setter![imm_relu];
    common_getter_setter![imm_group];
    common_getter_setter![imm_core];

    pub fn new() -> Self {
        Self {
            core_indx: Fixer::Edit(0),
            rd: Fixer::Edit(0),
            r1: Fixer::Edit(0),
            r2: Fixer::Edit(0),
            imm: Fixer::Edit(0),
            offset_select: Fixer::Edit(0),
            offset_value: Fixer::Edit(0),
            imm_len: Fixer::Edit(0),
            ibiw: Fixer::Edit(0),
            obiw: Fixer::Edit(0),
            mbiw: Fixer::Edit(0),
            imm_relu: Fixer::Edit(0),
            imm_group: Fixer::Edit(0),
            imm_core: Fixer::Edit(0),
        }
    }

    pub fn clear(&mut self) {
        self.clear_core_indx();
        self.clear_rd();
        self.clear_r1();
        self.clear_r2();
        self.clear_imm();
        self.clear_offset_value();
        self.clear_offset_select();
        self.clear_imm_len();
        self.clear_ibiw();
        self.clear_obiw();
        self.clear_mbiw();
        self.clear_imm_relu();
        self.clear_imm_group();
        self.clear_imm_core();
    }

    fn check_sanity(&self) {
        assert!(!(self.get_r2() != 0 && self.get_imm() != 0 && self.get_mbiw() != 0 && self.get_imm_core() != 0));
        assert!(
            !(self.get_ibiw() != 0 && self.get_offset_select() != 0 && self.get_imm_relu() != 0)
        );
        assert!(
            !(self.get_obiw() != 0 && self.get_offset_value() != 0 && self.get_imm_group() != 0)
        );
    }

    pub fn build(&mut self) -> InstructionData {
        self.check_sanity();
        let inst_data = InstructionData {
            core_indx: self.get_core_indx(),
            rd: self.get_rd(),
            r1: self.get_r1(),
            r2_or_imm: self.get_r2() + self.get_imm() + self.get_mbiw() + self.get_imm_core(),
            generic1: self.get_offset_select() + self.get_ibiw() + self.get_imm_relu(),
            generic2: self.get_offset_value() + self.get_obiw() + self.get_imm_group(),
            generic3: self.get_imm_len(),
        };
        self.clear();
        inst_data
    }

    pub fn set_rdr1r2(&mut self, rd: i32, r1: i32, r2: i32) -> &mut Self {
        self.set_rd(rd).set_r1(r1).set_r2(r2)
    }

    pub fn set_offset_select_value(&mut self, offset_select: i32, offset_value: i32) -> &mut Self {
        self.set_offset_select(offset_select)
            .set_offset_value(offset_value)
    }

    pub fn set_rdr1imm(&mut self, rd: i32, r1: i32, imm: i32) -> &mut Self {
        self.set_rd(rd).set_r1(r1).set_imm(imm)
    }

    pub fn set_rdr1(&mut self, rd: i32, r1: i32) -> &mut Self {
        self.set_rd(rd).set_r1(r1)
    }

    pub fn set_rdimm(&mut self, rd: i32, imm: i32) -> &mut Self {
        self.set_rd(rd).set_imm(imm)
    }

    pub fn set_ibiw_obiw(&mut self, ibiw: i32, obiw: i32) -> &mut Self {
        self.set_ibiw(ibiw).set_obiw(obiw)
    }

    pub fn set_mbiw_immrelu_immgroup(
        &mut self,
        mbiw: i32,
        imm_relu: i32,
        imm_group: i32,
    ) -> &mut Self {
        self.set_mbiw(mbiw)
            .set_imm_relu(imm_relu)
            .set_imm_group(imm_group)
    }
}
