use byteorder::{ByteOrder, LittleEndian};
use bytes::{BufMut, Bytes, BytesMut};
use ckb_vm::{
    memory::{FLAG_EXECUTABLE, FLAG_WXORX_BIT},
    registers::A7,
    Error, Memory, Register, SupportMachine, Syscalls, RISCV_PAGES, RISCV_PAGESIZE,
};
use std::fs::File;
use std::io::Write;
pub struct ElfDumper {
    dump_file_name: String,
    syscall_number: u64,
    maximum_zero_gap: u64,
}
impl Default for ElfDumper {
    fn default() -> ElfDumper {
        ElfDumper {
            dump_file_name: "dump.bin".to_string(),
            syscall_number: 4097,
            maximum_zero_gap: 64,
        }
    }
}
impl ElfDumper {
    pub fn new(dump_file_name: String, syscall_number: u64, maximum_zero_gap: u64) -> Self {
        ElfDumper {
            dump_file_name,
            syscall_number,
            maximum_zero_gap,
        }
    }
}
#[derive(Clone)]
struct Segment {
    start: u64,
    data: Bytes,
    executable: bool,
}
impl Segment {
    fn first_page(&self) -> u64 {
        self.start / RISCV_PAGESIZE as u64
    }
    fn first_page_address(&self) -> u64 {
        self.first_page() * RISCV_PAGESIZE as u64
    }
    fn last_page(&self) -> u64 {
        (self.start + self.data.len() as u64 - 1) / RISCV_PAGESIZE as u64
    }
}
impl<Mac: SupportMachine> Syscalls<Mac> for ElfDumper {
    fn initialize(&mut self, _machine: &mut Mac) -> Result<(), Error> {
        Ok(())
    }
    fn ecall(&mut self, machine: &mut Mac) -> Result<bool, Error> {
        if machine.registers()[A7].to_u64() != self.syscall_number {
            return Ok(false);
        }
        let mut segments: Vec<Segment> = vec![];
        let mut page = 0;
        while page < RISCV_PAGES as u64 {
            let mut start = page * RISCV_PAGESIZE as u64;
            let end = (page + 1) * RISCV_PAGESIZE as u64;
            while start < end {
                while start < end {
                    if machine.memory_mut().load64(&Mac::REG::from_u64(start))?.to_u64() != 0 {
                        break;
                    }
                    start += 8;
                }
                if start < end {
                    let executable = machine.memory_mut().fetch_flag(page)? & FLAG_WXORX_BIT == FLAG_EXECUTABLE;
                    let (bytes_start, mut bytes_mut) = if segments.is_empty() {
                        (start, BytesMut::new())
                    } else {
                        let last_segment = &segments[segments.len() - 1];
                        let same_page = page == last_segment.last_page();
                        let gap = start - (last_segment.start + last_segment.data.len() as u64);
                        if last_segment.executable == executable && ((gap <= self.maximum_zero_gap) || same_page) {
                            let Segment {
                                start: segment_start,
                                data: segment_data,
                                ..
                            } = segments.remove(segments.len() - 1);
                            let mut segment_data = BytesMut::from(segment_data.as_ref());
                            let mut zeros = vec![];
                            zeros.resize(gap as usize, 0);
                            segment_data.extend_from_slice(&zeros);
                            (segment_start, segment_data)
                        } else {
                            (start, BytesMut::new())
                        }
                    };
                    while start < end {
                        let value = machine.memory_mut().load64(&Mac::REG::from_u64(start))?.to_u64();
                        if value == 0 {
                            break;
                        }
                        bytes_mut.put_u64_le(value);
                        start += 8;
                    }
                    segments.push(Segment {
                        start: bytes_start,
                        data: bytes_mut.freeze(),
                        executable,
                    });
                }
            }
            page += 1;
        }
        if segments.is_empty() || segments[0].start <= RISCV_PAGESIZE as u64 {
            return Err(Error::Unexpected("Unexpected segments".into()));
        }
        let mut register_buffer = BytesMut::new();
        for register_value in &machine.registers()[1..] {
            register_buffer.put_u64_le(register_value.to_u64());
        }
        let register_entrypoint = register_buffer.len() as u64;
        register_buffer.put_u32_le(0x00000517); register_buffer.put_u32_le(0xf0050513); register_buffer.put_u32_le(0x00853083); register_buffer.put_u32_le(0x01053103); register_buffer.put_u32_le(0x01853183); register_buffer.put_u32_le(0x02053203); register_buffer.put_u32_le(0x02853283); register_buffer.put_u32_le(0x03053303); register_buffer.put_u32_le(0x03853383); register_buffer.put_u32_le(0x04053403); register_buffer.put_u32_le(0x04853483); register_buffer.put_u32_le(0x05853583); register_buffer.put_u32_le(0x06053603); register_buffer.put_u32_le(0x06853683); register_buffer.put_u32_le(0x07053703); register_buffer.put_u32_le(0x07853783); register_buffer.put_u32_le(0x08053803); register_buffer.put_u32_le(0x08853883); register_buffer.put_u32_le(0x09053903); register_buffer.put_u32_le(0x09853983); register_buffer.put_u32_le(0x0a053a03); register_buffer.put_u32_le(0x0a853a83); register_buffer.put_u32_le(0x0b053b03); register_buffer.put_u32_le(0x0b853b83); register_buffer.put_u32_le(0x0c053c03); register_buffer.put_u32_le(0x0c853c83); register_buffer.put_u32_le(0x0d053d03); register_buffer.put_u32_le(0x0d853d83); register_buffer.put_u32_le(0x0e053e03); register_buffer.put_u32_le(0x0e853e83); register_buffer.put_u32_le(0x0f053f03); register_buffer.put_u32_le(0x0f853f83); register_buffer.put_u32_le(0x05053503); let register_buffer_start = segments[0].first_page_address() - RISCV_PAGESIZE as u64;
        let jump_instruction_pc = register_buffer_start + register_buffer.len() as u64;
        let jump_offset = machine.pc().to_u64() - jump_instruction_pc;
        let masked = jump_offset & 0xFFFFFFFFFFE00001;
        if masked != 0 && masked != 0xFFFFFFFFFFE00000 {
            return Err(Error::Unexpected("Unexpected masked".into()));
        }
        let jump_instruction = 0b1101111
            | ((((jump_offset >> 12) & 0b_1111_1111) as u32) << 12)
            | ((((jump_offset >> 11) & 1) as u32) << 20)
            | ((((jump_offset >> 1) & 0b_1111_1111_11) as u32) << 21)
            | ((((jump_offset >> 20) & 1) as u32) << 31);
        register_buffer.put_u32_le(jump_instruction);
        assert!(register_buffer.len() < RISCV_PAGESIZE);
        segments.push(Segment {
            start: register_buffer_start,
            data: register_buffer.freeze(),
            executable: true,
        });
        let mut elf = BytesMut::new();
        elf.extend_from_slice(&[
            0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        ]);
        elf.put_u16_le(2);
        elf.put_u16_le(243);
        elf.put_u32_le(1);
        elf.put_u64_le(register_buffer_start + register_entrypoint);
        let program_header_offset = elf.len();
        elf.put_u64_le(0);
        let section_header_offset = elf.len();
        elf.put_u64_le(0);
        elf.put_u32_le(1);
        elf.put_u16_le(64);
        elf.put_u16_le(56);
        let program_header_number_offset = elf.len();
        elf.put_u16_le(0);
        elf.put_u16_le(64);
        let section_header_number_offset = elf.len();
        elf.put_u16_le(0);
        elf.put_u16_le(0);
        assert!(elf.len() == 64);
        let string_table_offset = elf.len() as u64;
        elf.put_u32_le(0);
        let mut section_headers = vec![];
        let mut string_table_section_header = BytesMut::new();
        string_table_section_header.put_u32_le(0);
        string_table_section_header.put_u32_le(3);
        string_table_section_header.put_u64_le(0);
        string_table_section_header.put_u64_le(0);
        string_table_section_header.put_u64_le(string_table_offset);
        string_table_section_header.put_u64_le(4);
        string_table_section_header.put_u32_le(0);
        string_table_section_header.put_u32_le(0);
        string_table_section_header.put_u64_le(1);
        string_table_section_header.put_u64_le(0);
        assert!(string_table_section_header.len() == 64);
        section_headers.push(string_table_section_header.freeze());
        let mut program_headers = vec![];
        for segment in segments {
            let current_offset = elf.len() as u64;
            elf.extend_from_slice(segment.data.as_ref());
            let mut program_header = BytesMut::new();
            program_header.put_u32_le(1);
            program_header.put_u32_le(if segment.executable { 5 } else { 6 });
            program_header.put_u64_le(current_offset);
            program_header.put_u64_le(segment.start);
            program_header.put_u64_le(segment.start);
            program_header.put_u64_le(segment.data.len() as u64);
            program_header.put_u64_le(segment.data.len() as u64);
            program_header.put_u64_le(0x1000);
            assert!(program_header.len() == 56);
            program_headers.push(program_header.freeze());
            if segment.executable {
                let mut section_header = BytesMut::new();
                section_header.put_u32_le(0);
                section_header.put_u32_le(1);
                section_header.put_u64_le(6);
                section_header.put_u64_le(segment.start);
                section_header.put_u64_le(current_offset);
                section_header.put_u64_le(segment.data.len() as u64);
                section_header.put_u32_le(0);
                section_header.put_u32_le(0);
                section_header.put_u64_le(2);
                section_header.put_u64_le(0);
                assert!(section_header.len() == 64);
                section_headers.push(section_header.freeze());
            }
        }
        while elf.len() % 4 != 0 {
            elf.put_u8(0);
        }
        let current_offset = elf.len() as u64;
        LittleEndian::write_u64(
            &mut elf[program_header_offset..program_header_offset + 8],
            current_offset,
        );
        LittleEndian::write_u16(
            &mut elf[program_header_number_offset..program_header_number_offset + 8],
            program_headers.len() as u16,
        );
        for program_header in program_headers {
            elf.extend_from_slice(program_header.as_ref());
        }
        while elf.len() % 4 != 0 {
            elf.put_u8(0);
        }
        let current_offset = elf.len() as u64;
        LittleEndian::write_u64(
            &mut elf[section_header_offset..section_header_offset + 8],
            current_offset,
        );
        LittleEndian::write_u16(
            &mut elf[section_header_number_offset..section_header_number_offset + 8],
            section_headers.len() as u16,
        );
        for section_header in section_headers {
            elf.extend_from_slice(section_header.as_ref());
        }
        let mut file = File::create(&self.dump_file_name)?;
        file.write_all(&elf)?;
        Ok(true)
    }
}