// Filename: emulator.rs
// Author:	 Kai Rese
// Version:	 0.3
// Date:	 12-07-2021 (DD-MM-YYYY)
// Library:  gpcas_arschitek_zero
//
// Copyright (c) 2021 Kai Rese
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this program. If not, see
// <https://www.gnu.org/licenses/>.

//! The Arschitek zero emulator.

use gpcas_base::instruction_type;
use gpcas_isa::{instruction_flags, Emulator, Instruction, MAX_INPUTS};
use std::convert::TryInto;
use std::fmt::Write;
use std::sync::atomic::{AtomicUsize, Ordering};

/// Emulates the given program and feeds data to the simulator.
pub struct ArschitekZeroEmulator {
    /// The current instruction pointer value.
    ip: usize,
    /// The count of emulation calls.
    decoded_instruction_count: AtomicUsize,
    /// The count of emulated and retired instructions.
    retired_instruction_count: AtomicUsize,
    /// If the emulation is still running or has been terminated.
    running: bool,
    /// The memory data of the program image.
    memory: Vec<u8>,
    /// The architectural register state.
    registers: [u16; 16],
    /// Buffers the formatted emulator state for later retrieval.
    output_buffer: String,
}

impl ArschitekZeroEmulator {
    /// Creates a new instance of the emulator.
    pub fn new(executable_data: Vec<u8>) -> Self {
        let mut executable_data = executable_data;
        let mut registers = [0; 16];
        executable_data.resize(1 << (u16::BITS * 2), 0);
        registers[14] = u16::MAX;
        ArschitekZeroEmulator {
            ip: 0,
            decoded_instruction_count: AtomicUsize::new(0),
            retired_instruction_count: AtomicUsize::new(0),
            running: true,
            memory: executable_data,
            registers,
            output_buffer: String::new(),
        }
    }
}

impl Emulator for ArschitekZeroEmulator {
    #[inline]
    fn emulate_instruction(&mut self, address: usize) -> Option<Instruction> {
        if self.running {
            let raw_instruction_data =
                u16::from_le_bytes(self.memory[address..address + 2].try_into().unwrap());
            let template_data = TemplateData::parse(raw_instruction_data);
            let base_values = &BASE_VALUES[template_data.opcode as usize];
            let mut instruction = Instruction {
                address,
                memory_address: 0,
                branch_address: 0,
                instr_type: base_values.instr_type,
                memory_operand_size: 2,
                flags: base_values.flags,
                inputs: [0; MAX_INPUTS],
                input_register_count: base_values.input_register_count,
                memory_register_count: base_values.memory_register_count,
                output: template_data.reg0,
                size: 2,
            };

            self.decoded_instruction_count
                .fetch_add(1, Ordering::Relaxed);
            // is hard wired, gets reset so output checks can be omitted
            self.registers[15] = 0;

            match template_data.opcode {
                // Load IP-relative
                0x0 => {
                    instruction.memory_address = (address as u16)
                        .wrapping_add(2)
                        .wrapping_add(template_data.imm8 << 1)
                        as usize;
                    if address == self.ip {
                        self.retired_instruction_count
                            .fetch_add(1, Ordering::Relaxed);
                        self.ip += 2;
                        self.registers[template_data.reg0 as usize] = u16::from_le_bytes(
                            self.memory[instruction.memory_address..instruction.memory_address + 2]
                                .try_into()
                                .unwrap(),
                        );
                    }
                }
                // Store IP-relative
                0x1 => {
                    instruction.inputs[0] = template_data.reg0;
                    instruction.memory_address = (address as u16)
                        .wrapping_add(2)
                        .wrapping_add(template_data.imm8 << 1)
                        as usize;
                    if address == self.ip {
                        self.retired_instruction_count
                            .fetch_add(1, Ordering::Relaxed);
                        self.ip += 2;
                        self.memory[instruction.memory_address..instruction.memory_address + 2]
                            .copy_from_slice(
                                &self.registers[template_data.reg0 as usize].to_le_bytes()[..],
                            );
                    }
                }
                // Jump IP-relative
                0x2 => {
                    instruction.branch_address = (address as u16)
                        .wrapping_add(2)
                        .wrapping_add(template_data.imm12 << 1)
                        as usize;
                    if address == self.ip {
                        self.retired_instruction_count
                            .fetch_add(1, Ordering::Relaxed);
                        self.ip = instruction.branch_address;
                    }
                }
                // Branch equal zero
                0x3 => {
                    instruction.inputs[0] = template_data.reg0;
                    instruction.branch_address = (address as u16)
                        .wrapping_add(2)
                        .wrapping_add(template_data.imm8 << 1)
                        as usize;
                    let branch;
                    if self.registers[template_data.reg0 as usize] == 0 {
                        instruction.flags |= instruction_flags::BR_TAKEN;
                        branch = true;
                    } else {
                        branch = false;
                    }

                    if address == self.ip {
                        self.retired_instruction_count
                            .fetch_add(1, Ordering::Relaxed);
                        if branch {
                            self.ip = instruction.branch_address;
                        } else {
                            self.ip += 2;
                        }
                    }
                }
                // Load base + offset
                0x4 => {
                    instruction.inputs[0] = template_data.reg1;
                    instruction.memory_address = (self.registers[template_data.reg1 as usize]
                        .wrapping_add(sign_extend_u4(template_data.reg2_imm4))
                        as usize) << 1;
                    if address == self.ip {
                        self.retired_instruction_count
                            .fetch_add(1, Ordering::Relaxed);
                        self.ip += 2;
                        self.registers[template_data.reg0 as usize] = u16::from_le_bytes(
                            self.memory[instruction.memory_address..instruction.memory_address + 2]
                                .try_into()
                                .unwrap(),
                        );
                    }
                }
                // Store base + offset
                0x5 => {
                    instruction.inputs[0] = template_data.reg0;
                    instruction.inputs[1] = template_data.reg1;
                    instruction.memory_address = (self.registers[template_data.reg1 as usize]
                        .wrapping_add(sign_extend_u4(template_data.reg2_imm4) << 1)
                        as usize) << 1;
                    if address == self.ip {
                        self.retired_instruction_count
                            .fetch_add(1, Ordering::Relaxed);
                        self.ip += 2;
                        self.memory[instruction.memory_address..instruction.memory_address + 2]
                            .copy_from_slice(
                                &self.registers[template_data.reg0 as usize].to_le_bytes()[..],
                            );
                    }
                }
                // Jump base + offset
                0x6 => {
                    instruction.inputs[0] = template_data.reg0;
                    instruction.branch_address = (self.registers[template_data.reg0 as usize]
                        .wrapping_add(template_data.imm8)
                        as usize) << 1;
                    if address == self.ip {
                        self.retired_instruction_count
                            .fetch_add(1, Ordering::Relaxed);
                        self.ip = instruction.branch_address;
                    }
                }
                // Branch negative
                0x7 => {
                    instruction.inputs[0] = template_data.reg0;
                    instruction.branch_address = (address as u16)
                        .wrapping_add(2)
                        .wrapping_add(template_data.imm8 << 1)
                        as usize;
                    let branch;
                    if (self.registers[template_data.reg0 as usize] as i16) < 0 {
                        instruction.flags |= instruction_flags::BR_TAKEN;
                        branch = true;
                    } else {
                        branch = false;
                    }

                    if address == self.ip {
                        self.retired_instruction_count
                            .fetch_add(1, Ordering::Relaxed);
                        if branch {
                            self.ip = instruction.branch_address;
                        } else {
                            self.ip += 2;
                        }
                    }
                }
                // Add register
                0x8 => {
                    instruction.inputs[0] = template_data.reg1;
                    instruction.inputs[1] = template_data.reg2_imm4;
                    if address == self.ip {
                        self.retired_instruction_count
                            .fetch_add(1, Ordering::Relaxed);
                        self.ip += 2;
                        self.registers[template_data.reg0 as usize] = self.registers
                            [template_data.reg1 as usize]
                            .wrapping_add(self.registers[template_data.reg2_imm4 as usize]);
                    }
                }
                // Add immediate
                0x9 => {
                    instruction.inputs[0] = template_data.reg0;
                    if address == self.ip {
                        self.retired_instruction_count
                            .fetch_add(1, Ordering::Relaxed);
                        self.ip += 2;
                        self.registers[template_data.reg0 as usize] = self.registers
                            [template_data.reg0 as usize]
                            .wrapping_add(template_data.imm8);
                    }
                }
                // Subtract register
                0xa => {
                    instruction.inputs[0] = template_data.reg1;
                    instruction.inputs[1] = template_data.reg2_imm4;
                    if address == self.ip {
                        self.retired_instruction_count
                            .fetch_add(1, Ordering::Relaxed);
                        self.ip += 2;
                        self.registers[template_data.reg0 as usize] = self.registers
                            [template_data.reg1 as usize]
                            .wrapping_sub(self.registers[template_data.reg2_imm4 as usize]);
                    }
                }
                // Set to target
                0xb => {
                    if address == self.ip {
                        self.retired_instruction_count
                            .fetch_add(1, Ordering::Relaxed);
                        self.ip += 2;
                        self.registers[template_data.reg0 as usize] =
                            (self.ip as u16 >> 1).wrapping_add(template_data.imm8);
                    }
                }
                // Shift left
                0xc => {
                    instruction.inputs[0] = template_data.reg1;
                    if address == self.ip {
                        self.retired_instruction_count
                            .fetch_add(1, Ordering::Relaxed);
                        self.ip += 2;
                        self.registers[template_data.reg0 as usize] = self.registers
                            [template_data.reg1 as usize]
                            .wrapping_shl(template_data.reg2_imm4 as u32);
                    }
                }
                // Shift right
                0xd => {
                    instruction.inputs[0] = template_data.reg1;
                    if address == self.ip {
                        self.retired_instruction_count
                            .fetch_add(1, Ordering::Relaxed);
                        self.ip += 2;
                        self.registers[template_data.reg0 as usize] = self.registers
                            [template_data.reg1 as usize]
                            .wrapping_shr(template_data.reg2_imm4 as u32);
                    }
                }
                // Boolean table
                0xe => {
                    instruction.inputs[0] = template_data.reg0;
                    instruction.inputs[1] = template_data.reg1;
                    if address == self.ip {
                        self.retired_instruction_count
                            .fetch_add(1, Ordering::Relaxed);
                        self.ip += 2;
                        let first_operand = self.registers[template_data.reg0 as usize];
                        let second_operand = self.registers[template_data.reg1 as usize];
                        let first_permutation = (!first_operand & !second_operand)
                            * (template_data.reg2_imm4 as u16 & 0x1);
                        let second_permutation = (!first_operand & second_operand)
                            * ((template_data.reg2_imm4 as u16 >> 1) & 0x1);
                        let third_permutation = (first_operand & !second_operand)
                            * ((template_data.reg2_imm4 as u16 >> 2) & 0x1);
                        let fourth_permutation = (first_operand & second_operand)
                            * ((template_data.reg2_imm4 as u16 >> 3) & 0x1);
                        self.registers[template_data.reg0 as usize] = first_permutation
                            | second_permutation
                            | third_permutation
                            | fourth_permutation;
                    }
                }
                // Stop execution
                0xf => {
                    if address == self.ip {
                        self.retired_instruction_count
                            .fetch_add(1, Ordering::Relaxed);
                        self.running = false;
                        writeln!(
                            self.output_buffer,
                            "Stopped execution at address {:#X}.",
                            self.ip
                        )
                        .unwrap();
                        writeln!(self.output_buffer, "Register |  !sign | signed |    hex").unwrap();
                        writeln!(self.output_buffer, "---------+--------+--------+-------").unwrap();
                        for (i, &reg) in self.registers.iter().enumerate() {
                            writeln!(
                                self.output_buffer,
                                "{0:#3X}      | {1:6} | {2:6} | {1:#6X}",
                                i, reg, reg as i16
                            )
                            .unwrap();
                        }
                    }
                }
                _ => unreachable!(),
            }

            Some(instruction)
        } else {
            None
        }
    }

    #[inline]
    fn get_decoded_instruction_count(&self) -> usize {
        self.decoded_instruction_count.load(Ordering::Relaxed)
    }

    #[inline]
    fn get_retired_instruction_count(&self) -> usize {
        self.retired_instruction_count.load(Ordering::Relaxed)
    }

    #[inline]
    fn get_current_ip(&self) -> usize {
        self.ip
    }

    #[inline]
    fn get_printed_output(&self) -> &str {
        self.output_buffer.as_str()
    }

    #[inline]
    fn get_start_address(&self) -> usize {
        0
    }
}

/// Extends a signed 4 bit value to 16 bit.
#[inline]
fn sign_extend_u4(input: u8) -> u16 {
    input as u16 | (0xfff0 * (input as u16 >> 3))
}

/// Values that differ for each instruction, but are only dependent on the instruction type.
struct InstructionBaseValues {
    /// An ID for the instruction class, see [`gpcas_base::instruction_type`]. Determines execution
    /// time in the ALU, as well as possible restrictions for execution pipe choice in the model.
    instr_type: u16,
    /// Bitflags for different properties of the instruction, see [`gpcas_isa::instruction_flags`].
    flags: u16,
    /// How many register inputs the instruction uses in total.
    input_register_count: u8,
    /// How many register inputs are used for memory operand calculation.
    memory_register_count: u8,
}

/// Every possible field of an instruction template.
struct TemplateData {
    /// The opcode of an instruction.
    opcode: u8,
    /// The first register, if the template has one.
    reg0: u8,
    /// The second register, if the template has it.
    reg1: u8,
    /// The third register or a 4 bit unsigned immediate operand, depending on the template.
    reg2_imm4: u8,
    /// A signed 8 bit immediate operand, if the template has it.
    imm8: u16,
    /// A signed 12 bit immediate operand, if the template has it.
    imm12: u16,
}

impl TemplateData {
    /// Parse all possible fields of an instruction.
    #[inline]
    pub fn parse(raw_data: u16) -> Self {
        TemplateData {
            opcode: ((raw_data & 0xf000) >> 12) as u8,
            reg0: ((raw_data & 0x0f00) >> 8) as u8,
            reg1: ((raw_data & 0x00f0) >> 4) as u8,
            reg2_imm4: (raw_data & 0x000f) as u8,
            imm8: (raw_data & 0x00ff) as i8 as i16 as u16,
            imm12: if (raw_data & 0x0800) > 0 {
                raw_data | 0xf000
            } else {
                raw_data & 0x0fff
            },
        }
    }
}

/// Values for each different opcode.
const BASE_VALUES: [InstructionBaseValues; 16] = [
    // 0: Load IP-relative
    InstructionBaseValues {
        instr_type: instruction_type::MOVE,
        flags: instruction_flags::REG_OUT | instruction_flags::MEM_IN,
        input_register_count: 0,
        memory_register_count: 0,
    },
    // 1: Store IP-relative
    InstructionBaseValues {
        instr_type: instruction_type::MOVE,
        flags: instruction_flags::MEM_OUT,
        input_register_count: 1,
        memory_register_count: 0,
    },
    // 2: Jump IP-relative
    InstructionBaseValues {
        instr_type: instruction_type::JUMP,
        flags: instruction_flags::IS_BR | instruction_flags::BR_TAKEN,
        input_register_count: 0,
        memory_register_count: 0,
    },
    // 3: Branch equal zero
    InstructionBaseValues {
        instr_type: instruction_type::BRANCH,
        flags: instruction_flags::IS_BR,
        input_register_count: 1,
        memory_register_count: 0,
    },
    // 4: Load base + offset
    InstructionBaseValues {
        instr_type: instruction_type::MOVE,
        flags: instruction_flags::REG_OUT | instruction_flags::MEM_IN,
        input_register_count: 1,
        memory_register_count: 1,
    },
    // 5: Store base + offset
    InstructionBaseValues {
        instr_type: instruction_type::MOVE,
        flags: instruction_flags::MEM_OUT,
        input_register_count: 2,
        memory_register_count: 1,
    },
    // 6: Jump base + offset
    InstructionBaseValues {
        instr_type: instruction_type::BRANCH,
        flags: instruction_flags::IS_BR | instruction_flags::BR_TAKEN,
        input_register_count: 1,
        memory_register_count: 0,
    },
    // 7: Branch negative
    InstructionBaseValues {
        instr_type: instruction_type::BRANCH,
        flags: instruction_flags::IS_BR,
        input_register_count: 1,
        memory_register_count: 0,
    },
    // 8: Add register
    InstructionBaseValues {
        instr_type: instruction_type::SIMPLE,
        flags: instruction_flags::REG_OUT,
        input_register_count: 2,
        memory_register_count: 0,
    },
    // 9: Add immediate
    InstructionBaseValues {
        instr_type: instruction_type::SIMPLE,
        flags: instruction_flags::REG_OUT,
        input_register_count: 1,
        memory_register_count: 0,
    },
    // a: Subtract register
    InstructionBaseValues {
        instr_type: instruction_type::SIMPLE,
        flags: instruction_flags::REG_OUT,
        input_register_count: 2,
        memory_register_count: 0,
    },
    // b: Set to target
    InstructionBaseValues {
        instr_type: instruction_type::SIMPLE,
        flags: instruction_flags::REG_OUT,
        input_register_count: 0,
        memory_register_count: 0,
    },
    // c: Shift left
    InstructionBaseValues {
        instr_type: instruction_type::INT_SHIFT,
        flags: instruction_flags::REG_OUT,
        input_register_count: 1,
        memory_register_count: 0,
    },
    // d: Shift right
    InstructionBaseValues {
        instr_type: instruction_type::INT_SHIFT,
        flags: instruction_flags::REG_OUT,
        input_register_count: 1,
        memory_register_count: 0,
    },
    // e: Bool table
    InstructionBaseValues {
        instr_type: instruction_type::SIMPLE,
        flags: instruction_flags::REG_OUT,
        input_register_count: 2,
        memory_register_count: 0,
    },
    // f: Stop
    InstructionBaseValues {
        instr_type: instruction_type::REGISTER_MOVE,
        flags: instruction_flags::TERMINATE,
        input_register_count: 0,
        memory_register_count: 0,
    },
];
