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

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

impl Chip8Video {
    pub fn reset(&mut self) {
        self.cls();
        self.start_frame();
    }

    pub fn cls(&mut self) {
        self.memory = [false; CHIP8_VIDEO_MEMORY];
    }

    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 {
        let effective_address = if address >= 2048 {
            address % 2048
        } else { address };
        self.memory[effective_address as usize]
    }

    pub fn poke(&mut self, address: u16, new_value: bool) {
        // println!("OFFSET: {address} - POKING {new_value}");

        // Loop the address
        let effective_address = address % 2048;

        let old_value = self.memory[effective_address as usize];
        let xored_value = new_value ^ old_value;       // XOR of the video
        // if the frame has already changed we dont care if it changed again.
        if !self.has_frame_changed {
            if old_value != xored_value {
                self.has_frame_changed = true
            };
        }

        self.memory[effective_address as usize] = xored_value;
    }


    pub fn poke_byte(&mut self, first_address: u16, to_write: u8) {
        for i in (0..8).rev() {
            self.poke(first_address + (7 - i), (to_write & (1 << i)) != 0);
        }
    }

    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
    }

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

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

#[cfg(test)]
mod test {
    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 super::*;

    const TEST_OUTPUT_SAMPLE_DIR: &str = "../resources/test/";

    fn build_checkerboard() -> Chip8Video {
        let mut r = Chip8Video::default();

        for i in 0..CHIP8_VIDEO_MEMORY {
            r.poke(i as u16, i % 2 == 0);
        }

        r
    }

    fn read_test_result(suffix: &str) -> String {
        std::fs::read_to_string(TEST_OUTPUT_SAMPLE_DIR.to_owned() + suffix)
            .unwrap()
    }

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

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

        for i in 0..CHIP8_VIDEO_MEMORY {
            assert!(!x.clone().peek(i as u16));
            // then flip the value and test again.
            &x.poke(i as u16, true);
            assert!(x.clone().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();

        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(0x01));
        assert!(v.peek(0x02));
        assert!(!v.peek(0x03));
        assert!(v.peek(0x04));
        assert!(!v.peek(0x05));
        assert!(v.peek(0x06));
        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.clone().peek(0x40));
            assert!(v.clone().peek(0x41));
            assert!(v.clone().peek(0x42));
            assert!(v.clone().peek(0x43));
            assert!(v.clone().peek(0x44));
            assert!(v.clone().peek(0x45));
            assert!(v.clone().peek(0x46));
            assert!(v.clone().peek(0x47));

            // row 3 column 1
            assert!(!v.peek(0xC0));
            assert!(v.clone().peek(0xC1));
            assert!(!v.clone().peek(0xC2));
            assert!(v.clone().peek(0xC3));
            assert!(!v.clone().peek(0xC4));
            assert!(v.clone().peek(0xC5));
            assert!(!v.clone().peek(0xC6));
            assert!(v.clone().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.clone().peek(test_offset + 1));
        assert!(!v.clone().peek(test_offset + 2));
        assert!(!v.clone().peek(test_offset + 3));
        assert!(!v.clone().peek(test_offset + 4));
        assert!(!v.clone().peek(test_offset + 5));
        assert!(!v.clone().peek(test_offset + 6));
        assert!(!v.clone().peek(test_offset + 7));

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

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

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

    #[test]
    fn write_checkboard() {
        let mut v = build_checkerboard();

        assert_eq!(v.format_as_string(), read_test_result("test_video_write_checkerboard.asc"));
    }

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

        for (byte_index, data_offset) in (0..=0x100).step_by(0x40).enumerate() {
            x.poke_byte(data_offset as u16, CHIP8FONT_0[byte_index]);
        }

        assert_eq!(read_test_result("test_video_zero.asc"), x.format_as_string());
    }

    #[test]
    fn multi_sprite_test() {
        let mut x = Chip8Video::default();
        // draw a row of digits 01234567
        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;
            for (index, offset) in (0..=0x100).step_by(0x40).enumerate() {
                x.poke_byte((data_base_offset + offset) as u16, sprite[index]);
            }
        }

        assert_eq!(read_test_result("test_multi_sprite.asc"), x.format_as_string());
    }

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

        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;
            for (index, offset) in (0..=0x100).step_by(0x40).enumerate() {
                x.poke_byte((data_base_offset + offset) as u16, sprite[index]);
            }
        }

        x.reset();
        assert_eq!(x.format_as_string(), read_test_result("test_reset_clears_video.asc"));
    }

    #[test]
    fn collision_test() {
        // Setup: Set 0xFF to 0x00 with a new frame ready
        // Action: Run Poke to the same area
        // Test: Verify the 'changed' flag is tripped
        let mut x = Chip8Video::default();
        x.poke_byte(0x00, 0xff);
        x.tick();
        // set the cell thats already set...
        x.poke(0x00, true);
        // it becomes unset and theres a frame changed
        assert_eq!(false, x.peek(0x00));

        assert_eq!(true, x.has_frame_changed);
    }

    #[test]
    fn collision_test2() {
        let mut x = Chip8Video::default();
        x.poke_byte(0x00, 0b11110000);
        assert_eq!(true, x.has_frame_changed);
        x.tick();
        assert_eq!(false, x.has_frame_changed);
        // clear the 'has changed' flag

        // now set a no-collision value
        x.poke_byte(0x00, 0b00001111);
        assert_eq!(true, x.has_frame_changed);
    }

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

        let y = x.clone().peek(2049);
        let y = x.clone().peek(0);

        // if we got here we didn't panic
        assert!(true);
    }
}
