use log::{debug, trace};
use crate::constants::CHIP8_VIDEO_MEMORY;

#[derive(Clone, Copy)]
pub struct Chip8Video {
    memory: [bool; CHIP8_VIDEO_MEMORY],
    pub has_frame_changed: bool
}

impl Chip8Video {
    pub fn cls(&mut self)  {
        for i in 0..CHIP8_VIDEO_MEMORY {
            self.memory[i] = false;
        }
    }

    pub fn start_frame(&mut self) {
        self.has_frame_changed = false;
    }

    pub fn new(initial_configuration: [bool; CHIP8_VIDEO_MEMORY]) -> Self {
        Self {
            memory: initial_configuration,
            has_frame_changed: false
        }
    }

    pub fn peek(self, address: u16) -> bool {
        self.memory[address as usize]
    }

    pub fn poke(&mut self, address: u16, new_value: bool) -> Self {
        trace!("OFFSET: {address} - POKING {new_value}");
        let old_value = self.memory[address as usize];
        if old_value != new_value {
            trace!("**VIDEO** TOGGLING");
            self.has_frame_changed = true;
        } else {
            trace!("NOT TOGGLING");
        }
        self.memory[address as usize] = new_value;
        self.to_owned()
    }

    pub fn poke_byte(&mut self, first_address: u16, to_write: u8) -> Self  {
        for i in (0..8).rev() {
           let shifted = ((1 << i) & to_write) >> i;
            //
            let target_address = first_address + (7 - i);
            let is_set = shifted == 1;
           self.poke(target_address, is_set);
        }
        self.to_owned()
    }

    pub fn poke_sprite(&mut self, first_address: u16, to_write: Vec<u8>) -> Self {
        let sprite_length = to_write.len();

        for (index, byte) in to_write.iter().enumerate() {
            let real_address = index * 64;
            self.poke_byte(real_address as u16, *byte);
        }
        self.to_owned()
    }

    pub fn format_as_string(self) -> String {
        let mut output = String::new();
        for row in 0..32 {
            for column in 0..64 {
                let data_offset = row * 64 + column;
                debug!("Rendering {data_offset} with value {}", self.memory[data_offset]);
                if self.memory[data_offset] {
                    output += "*"
                } else {
                    output += " "
                }
            }
            output += "\n";
        }

        output
    }
}

impl Default for Chip8Video {
    fn default() -> Self {
        Self { memory: [false; CHIP8_VIDEO_MEMORY as usize], has_frame_changed: false }
    }
}

#[cfg(test)]
mod test {
    use std::fs::File;
    use std::io::Read;
    use crate::chip8::system_memory::{CHIP8FONT_0, CHIP8FONT_1, CHIP8FONT_2, CHIP8FONT_3, CHIP8FONT_4, CHIP8FONT_5, CHIP8FONT_6, CHIP8FONT_7};
    use crate::constants::{CHIP8_VIDEO_HEIGHT, CHIP8_VIDEO_WIDTH};
    use super::*;

    #[test]
    fn smoke() { assert!(true) }

    #[test]
    fn default_test() {
        let mut x = Chip8Video::default();

        for i in 0..CHIP8_VIDEO_MEMORY {
            assert!(!x.peek(i as u16));
            // then flip the value and test again.
            &x.poke(i as u16, true);
            assert!(x.peek(i as u16));
        }
    }

    #[test]
    fn string_test_1() {
        let mut x = Chip8Video::default();
        let mut working_string = String::new();
        for i in 0..32 {
            working_string += &*(" ".repeat(64) + "\n");
        }

        assert_eq!(working_string, x.format_as_string());

        let mut working_string = String::new();
        // set a checkerboard...
        for cb_row in 0..32 {
            for cb_col in 0..64 {
                let data_offset = cb_row * 64 + cb_col;
                if data_offset % 2 == 0 {
                    x.poke(data_offset, true);
                    working_string += "*";
                } else {
                    x.poke(data_offset, false);
                    working_string += " ";
                }
            }
            working_string += "\n";
        }

        assert_eq!(working_string, x.format_as_string());
    }

    #[test]
    fn set_initial_memory() {
        let mut x = Chip8Video::default();
        // let mut initial_memory = [false; CHIP8_VIDEO_MEMORY];
        let mut ws = String::new();
        // set our checkerboard
        for cbr in 0..32 {
            for cbc in 0..64 {
                let dof = cbr * 64 + cbc;
                if (dof as i32 % 2) == 0 {
                    x.poke(dof, true);
                    ws += "*";
                } else {
                    ws += " ";
                }
            }
            ws += "\n";
        }
        assert_eq!(x.format_as_string(), ws);
    }

    #[test]
    fn poke_byte() {
        let to_poke = 0b11001111;
        let mut x = Chip8Video::default();
        x.poke_byte(0x05, to_poke);
        let mut expected = String::new();
        expected = "     **  ****                                                   \n".to_string();
        for i in 0..31 {
            expected += &*(" ".repeat(64) + "\n");
        }
        assert_eq!(x.format_as_string(), expected);
    }

    #[test]
    fn cls() {
        let mut initial_memory = [false; CHIP8_VIDEO_MEMORY];
        let mut ws = String::new();
        // set our checkerboard
        for cbr in 0..32 {
            for cbc in 0..64 {
                let dof = cbr * 64 + cbc;
                if (dof as i32 % 2) == 0 {
                    initial_memory[dof] = true;
                }
                ws += " ";
            }
            ws += "\n";
        }
        let mut set_x = Chip8Video::new(initial_memory);
        set_x.cls();
        assert_eq!(set_x.format_as_string(), ws);
    }

    #[test]
    fn poke_byte_test() {
        let to_poke = 0b10101010;
        let mut v = Chip8Video::default();
        v.poke_byte(0x00, to_poke);
        assert!(v.peek(0x00));
        assert!(v.peek(0x02));
        assert!(v.peek(0x04));
        assert!(v.peek(0x06));
        assert!(!v.peek(0x01));
        assert!(!v.peek(0x03));
        assert!(!v.peek(0x05));
        assert!(!v.peek(0x07));
        for i in 0x8..CHIP8_VIDEO_MEMORY {
            assert!(!v.peek(i as u16));
        }
    }

    #[test]
    fn poke_multi_line_test() {
        let mut v = Chip8Video::default();
        let to_poke = [
            0b00000000,
            0b11111111,
            0b10101010,
            0b01010101
        ];

        for (byte_in_set, byte_to_poke) in to_poke.iter().enumerate() {
            let base_offset = byte_in_set * 64;
           v.poke_byte(base_offset as u16, *byte_to_poke);
        }

        // row 2 column 1
        {
            assert!(v.peek(0x40));
            assert!(v.peek(0x41));
            assert!(v.peek(0x42));
            assert!(v.peek(0x43));
            assert!(v.peek(0x44));
            assert!(v.peek(0x45));
            assert!(v.peek(0x46));
            assert!(v.peek(0x47));

            // row 3 column 1
            assert!(!v.peek(0xC0));
            assert!(v.peek(0xC1));
            assert!(!v.peek(0xC2));
            assert!(v.peek(0xC3));
            assert!(!v.peek(0xC4));
            assert!(v.peek(0xC5));
            assert!(!v.peek(0xC6));
            assert!(v.peek(0xC7));
        }
    }

    #[test]
    fn moved_poke_test() {
        let mut v = Chip8Video::default();
        let to_poke = [
            0b00000000,
            0b11111111,
            0b10101010,
            0b01010101
        ];

        let x_offset = 20;
        let y_offset = 5;


        for (byte_in_set, byte_to_poke) in to_poke.iter().enumerate() {
            let base_offset = (x_offset + byte_in_set) * 64 + y_offset;
            v.poke_byte(base_offset as u16, *byte_to_poke);
        }

        let test_offset = (x_offset * 64 + y_offset) as u16;
        assert!(!v.peek(test_offset));
        assert!(!v.peek(test_offset + 1));
        assert!(!v.peek(test_offset + 2));
        assert!(!v.peek(test_offset + 3));
        assert!(!v.peek(test_offset + 4));
        assert!(!v.peek(test_offset + 5));
        assert!(!v.peek(test_offset + 6));
        assert!(!v.peek(test_offset + 7));

        let test_offset= test_offset + 0x40;
        assert!(v.peek(test_offset));
        assert!(v.peek(test_offset + 1));
        assert!(v.peek(test_offset + 2));
        assert!(v.peek(test_offset + 3));
        assert!(v.peek(test_offset + 4));
        assert!(v.peek(test_offset + 5));
        assert!(v.peek(test_offset + 6));
        assert!(v.peek(test_offset + 7));
    }

    #[test]
    fn poke_sprite_test() {
        let mut v = Chip8Video::default();
        let to_poke = [
            0b00000000,
            0b11111111,
            0b10101010,
            0b01010101
        ];

        v.poke_sprite(0x00, to_poke.into());

        assert!(v.peek(0x40));
        assert!(v.peek(0x41));
        assert!(v.peek(0x42));
        assert!(v.peek(0x43));
        assert!(v.peek(0x44));
        assert!(v.peek(0x45));
        assert!(v.peek(0x46));
        assert!(v.peek(0x47));

        // row 3 column 1
        assert!(!v.peek(0xC0));
        assert!(v.peek(0xC1));
        assert!(!v.peek(0xC2));
        assert!(v.peek(0xC3));
        assert!(!v.peek(0xC4));
        assert!(v.peek(0xC5));
        assert!(!v.peek(0xC6));
        assert!(v.peek(0xC7));
    }

    #[test]
    fn verify_change_registered() {
        let mut v = Chip8Video::default();
        v.poke(0x01, true);
        assert!(v.has_frame_changed);

        v.start_frame();
        assert!(!v.has_frame_changed);
    }

    #[test]
    fn write_checkboard() {
        let mut v = Chip8Video::default();
        for current_row in 0..CHIP8_VIDEO_WIDTH {
            for current_col in 0..(CHIP8_VIDEO_HEIGHT / 8){
                let offset = current_row * CHIP8_VIDEO_HEIGHT + current_col;
                println!("CHECKBOARD OFFSET = {offset}");
                v.poke(offset as u16, offset % 2 == 0);
            }
        }
        println!("{}", v.format_as_string());
        println!("fsck is a cool tool");
    }

    #[test]
    fn zero_test() {
        let mut x = Chip8Video::default();

        x.poke_byte(0x00, CHIP8FONT_0[0]);
        x.poke_byte(0x40, CHIP8FONT_0[1]);
        x.poke_byte(0x80, CHIP8FONT_0[2]);
        x.poke_byte(0xC0, CHIP8FONT_0[3]);
        x.poke_byte(0x100, CHIP8FONT_0[4]);

        assert_eq!(std::fs::read_to_string("../resources/test/test_video_zero.asc")
                       .unwrap(),
                   x.format_as_string());
    }

    #[test]
    fn multi_sprite_test() {
        let mut x = Chip8Video::default();
        // draw a row of digits 01234567

        let start_offset = 0x00;
        let per_row_skip = 0x40;
        let to_draw = [CHIP8FONT_0, CHIP8FONT_1, CHIP8FONT_2, CHIP8FONT_3, CHIP8FONT_4, CHIP8FONT_5, CHIP8FONT_6, CHIP8FONT_7];
        for (index, sprite) in to_draw.iter().enumerate() {
            let data_base_offset = index * 0x8;
            println!("STARTING {index} at 0x{data_base_offset:04x} ({data_base_offset})");
            x.poke_byte(data_base_offset as u16, sprite[0]);
            x.poke_byte((data_base_offset + 0x40) as u16, sprite[1]);
            x.poke_byte((data_base_offset + 0x80) as u16, sprite[2]);
            x.poke_byte((data_base_offset + 0xC0) as u16, sprite[3]);
            x.poke_byte((data_base_offset + 0x100) as u16, sprite[4]);
        }


        assert_eq!(std::fs::read_to_string("../resources/test/test_multi_sprite.asc")
                       .unwrap(),
                   x.format_as_string());    }
}
