use self::tile_window::TileWindow;
use crate::{
    processor::{as_signed, get_bit, set_bit, set_or_clear_bit, CPU},
    FACTOR, HEIGHT, WIDTH,
};
use minifb::{Window, WindowOptions};

mod tile_window;

#[derive(PartialEq)]
enum DrawMode {
    HBlank,
    VBlank,
    Mode2,
    Mode3,
}

#[derive(Debug)]
enum TilemapArea {
    T9800,
    T9C00,
}

impl TilemapArea {
    fn get_addr(&self, addr: u16) -> u16 {
        match self {
            TilemapArea::T9800 => 0x9800 + addr,
            TilemapArea::T9C00 => 0x9C00 + addr,
        }
    }
}

#[derive(Debug)]
enum TiledataArea {
    D8000,
    D9000,
}

impl TiledataArea {
    fn get_addr(&self, addr: u8) -> u16 {
        match self {
            TiledataArea::D8000 => 0x8000 + ((addr as u16) * 16),
            TiledataArea::D9000 => 0x9000_u16.wrapping_add_signed((as_signed(addr) as i16) * 16),
        }
    }
}

#[derive(Debug)]
enum ObjSize {
    S8x8,
    S8x16,
}

impl ObjSize {
    fn get_height(&self) -> u8 {
        match self {
            ObjSize::S8x8 => 8,
            ObjSize::S8x16 => 16,
        }
    }
}

#[derive(Debug)]
struct LCDC {
    enable: bool,
    window_tilemap: TilemapArea,
    window_enable: bool,
    tile_area: TiledataArea,
    bg_tilemap: TilemapArea,
    obj_size: ObjSize,
    obj_enable: bool,
    bg_window_enable: bool,
}

#[derive(Clone, Copy, Debug)]
enum Colour {
    White,
    LightGray,
    DarkGray,
    Black,
}

impl Colour {
    fn to_rgb(&self) -> u32 {
        match self {
            Colour::White => Self::from_u8_rgb(255, 255, 255),
            Colour::LightGray => Self::from_u8_rgb(190, 190, 190),
            Colour::DarkGray => Self::from_u8_rgb(110, 110, 110),
            Colour::Black => Self::from_u8_rgb(20, 20, 20),
        }
    }

    fn from_u8_rgb(r: u8, g: u8, b: u8) -> u32 {
        let (r, g, b) = (r as u32, g as u32, b as u32);
        (r << 16) | (g << 8) | b
    }
}

#[derive(Clone, Copy)]
struct Palette {
    zero: Colour,
    one: Colour,
    two: Colour,
    three: Colour,
}

struct ObjectFlags {
    behind_bg_and_window: bool,
    y_flip: bool,
    x_flip: bool,
    palette: Palette,
}

struct Object {
    x: u8,
    y: u8,
    tile_index: u8,
    flags: ObjectFlags,
}

const TILE_WINDOW_WIDTH: usize = 16 * 8;
const TILE_WINDOW_HEIGHT: usize = 24 * 8;
const TILE_WINDOW_WIDTH_SCALED: usize = TILE_WINDOW_WIDTH * FACTOR;
const TILE_WINDOW_HEIGHT_SCALED: usize = TILE_WINDOW_HEIGHT * FACTOR;

pub struct GPU {
    pub buffer: Vec<u32>,
    scaled_buffer: Vec<u32>,
    mode: DrawMode,
    mode_clock: usize,
    scanline: u8,
    tile_window: Option<TileWindow>,
}

impl GPU {
    pub(super) fn new(enable_tile_window: bool) -> Self {
        let tile_window = if enable_tile_window {
            let mut window = Window::new(
                "Tiles",
                TILE_WINDOW_WIDTH_SCALED,
                TILE_WINDOW_HEIGHT_SCALED,
                WindowOptions::default(),
            )
            .unwrap_or_else(|e| {
                panic!("{}", e);
            });

            window.set_position((550 + (WIDTH * FACTOR)) as isize, 50);
            window.topmost(true);
            Some(TileWindow::new(window))
        } else {
            None
        };

        Self {
            buffer: vec![0; WIDTH * HEIGHT],
            scaled_buffer: vec![0; WIDTH * HEIGHT * 4],
            mode: DrawMode::Mode2,
            mode_clock: 0,
            scanline: 0,
            tile_window,
        }
    }
}

impl CPU {
    pub fn advance_gpu_clock(&mut self, steps: u8) {
        let real_steps = (steps as usize) * 4;
        self.gpu.mode_clock += real_steps;

        let lcdc = self.get_lcdc();

        match self.gpu.mode {
            DrawMode::HBlank => {
                // mode 0: hblank
                if self.gpu.mode_clock >= 204 {
                    self.gpu.mode_clock = 0;
                    self.gpu.scanline += 1;
                    if self.gpu.scanline == 143 {
                        self.enter_vblank(&lcdc);
                    } else {
                        self.gpu.mode = DrawMode::Mode2;
                    }
                }
            }
            DrawMode::VBlank => {
                // mode 1: vblank
                if self.gpu.mode_clock >= 456 {
                    self.gpu.mode_clock = 0;
                    self.gpu.scanline += 1;
                    if self.gpu.scanline == 153 {
                        self.exit_vblank();
                    }
                }
            }
            DrawMode::Mode2 => {
                // search oam for sprites on this line
                // we dont really have to emulate this
                if self.gpu.mode_clock >= 80 {
                    self.gpu.mode_clock = 0;
                    self.gpu.mode = DrawMode::Mode3;
                }
            }
            DrawMode::Mode3 => {
                // generate scanline
                if self.gpu.mode_clock >= 172 {
                    self.gpu.mode_clock = 0;
                    self.enter_hblank(&lcdc);
                }
            }
        }
        self.set_lcd_status();
    }

    fn get_lcdc(&self) -> LCDC {
        let reg = self.memory.get(0xFF40);
        LCDC {
            enable: get_bit(reg, 7),
            window_tilemap: if get_bit(reg, 6) {
                TilemapArea::T9C00
            } else {
                TilemapArea::T9800
            },
            window_enable: get_bit(reg, 5),
            tile_area: if get_bit(reg, 4) {
                TiledataArea::D8000
            } else {
                TiledataArea::D9000
            },
            bg_tilemap: if get_bit(reg, 3) {
                TilemapArea::T9C00
            } else {
                TilemapArea::T9800
            },
            obj_size: if get_bit(reg, 2) {
                ObjSize::S8x16
            } else {
                ObjSize::S8x8
            },
            obj_enable: get_bit(reg, 1),
            bg_window_enable: get_bit(reg, 0),
        }
    }

    fn enter_hblank(&mut self, lcdc: &LCDC) {
        self.gpu.mode = DrawMode::HBlank;
        self.render_scanline(self.gpu.scanline, lcdc);
    }

    fn enter_vblank(&mut self, lcdc: &LCDC) {
        self.memory.update_pressed_keys(self.window.get_keys());
        self.gpu.mode = DrawMode::VBlank;
        if lcdc.enable {
            self.render_window();
            self.memory.set(0xFF0F, set_bit(self.memory.get(0xFF0F), 0));
        }
    }

    fn exit_vblank(&mut self) {
        self.gpu.mode = DrawMode::Mode2;
        self.gpu.scanline = 0;
        if let Some(tile_window) = &mut self.gpu.tile_window {
            tile_window.draw_sprite_window(byte_to_palette(self.memory.get(0xFF47)), &self.memory);
        }
    }

    fn set_lcd_status(&mut self) {
        let mut stat = self.memory.get(0xFF41);
        stat = set_or_clear_bit(stat, 2, self.memory.get(0xFF44) == self.memory.get(0xFF45));
        stat = set_or_clear_bit(
            stat,
            1,
            (self.gpu.mode == DrawMode::Mode2) || (self.gpu.mode == DrawMode::Mode3),
        );
        stat = set_or_clear_bit(
            stat,
            0,
            (self.gpu.mode == DrawMode::VBlank) || (self.gpu.mode == DrawMode::Mode3),
        );
        self.memory.set(0xFF41, stat);
        self.memory.set(0xFF44, self.gpu.scanline);
    }

    fn render_scanline(&mut self, scanline: u8, lcdc: &LCDC) {
        if lcdc.bg_window_enable {
            let palette = byte_to_palette(self.memory.get(0xFF47));
            self.render_scanline_bg(scanline, lcdc, palette);
            if lcdc.window_enable {
                self.render_scanline_window(scanline, lcdc, palette);
            }
        } else {
            for x in 0..WIDTH {
                self.gpu.buffer[(scanline as usize * WIDTH) + x] =
                    Colour::from_u8_rgb(255, 255, 255);
            }
        }
        if lcdc.obj_enable {
            self.render_scanline_obj(scanline, lcdc);
        }
    }

    fn render_scanline_bg(&mut self, scanline: u8, lcdc: &LCDC, palette: Palette) {
        let scroll_y = self.memory.get(0xFF42);
        let scroll_x = self.memory.get(0xFF43);
        self.render_tiles(
            scanline,
            &lcdc.bg_tilemap,
            &lcdc.tile_area,
            palette,
            scroll_x,
            scroll_y,
        );
    }

    fn render_scanline_window(&mut self, scanline: u8, lcdc: &LCDC, palette: Palette) {
        let pos_y = self.memory.get(0xFF4A);
        // subtracting 7 to get the Real Number...
        let pos_x = self.memory.get(0xFF4B).wrapping_sub(7);
        if pos_y < 143 && pos_x < 166 {
            self.render_tiles(
                scanline,
                &lcdc.window_tilemap,
                &lcdc.tile_area,
                palette,
                pos_x,
                pos_y,
            )
        }
    }

    fn render_scanline_obj(&mut self, scanline: u8, lcdc: &LCDC) {
        let objs = self.parse_oam(scanline, &lcdc.obj_size);
        for object in objs {
            self.render_object(scanline, object, &lcdc.obj_size);
        }
    }

    fn parse_oam(&mut self, scanline: u8, obj_size: &ObjSize) -> Vec<Object> {
        let mut objs = vec![];
        for i in (0xFE00..0xFE9F).step_by(4) {
            let y_pos = self.memory.get(i).wrapping_sub(16);
            if y_pos <= scanline && (y_pos + obj_size.get_height()) > scanline {
                // sprite is on line
                let x_pos = self.memory.get(i + 1);
                let tile_index = self.memory.get(i + 2);
                let flags = self.memory.get(i + 3);
                let palette_addr = if get_bit(flags, 4) { 0xFF49 } else { 0xFF48 };
                objs.push(Object {
                    x: x_pos,
                    y: y_pos,
                    tile_index,
                    flags: ObjectFlags {
                        behind_bg_and_window: get_bit(flags, 7),
                        y_flip: get_bit(flags, 6),
                        x_flip: get_bit(flags, 5),
                        palette: byte_to_palette(self.memory.get(palette_addr)),
                    },
                });
                if objs.len() >= 10 {
                    break;
                }
            }
        }
        objs
    }

    fn render_object(&mut self, scanline: u8, object: Object, obj_size: &ObjSize) {
        let mut object_row = scanline - object.y;
        if object.flags.y_flip {
            object_row = obj_size.get_height() - object_row;
        }
        let tile_row = object_row % 8;
        let tile_addr = TiledataArea::D8000
            .get_addr(object.tile_index + if object_row >= 8 { 1 } else { 0 })
            + (tile_row as u16 * 2);
        let lsbs = self.memory.get(tile_addr);
        let msbs = self.memory.get(tile_addr + 1);
        for px_x in 0..8 {
            let x_addr = if object.flags.x_flip { px_x } else { 7 - px_x };
            let lsb = get_bit(lsbs, x_addr);
            let msb = get_bit(msbs, x_addr);
            let colour = bits_to_mapped_colour(lsb, msb, object.flags.palette);
            let x_coord = (object.x as usize) + (px_x as usize);
            if x_coord < WIDTH {
                self.gpu.buffer[(scanline as usize * WIDTH) + x_coord] = colour.to_rgb();
            }
        }
    }

    fn render_tiles(
        &mut self,
        scanline: u8,
        tilemap: &TilemapArea,
        tiledata: &TiledataArea,
        palette: Palette,
        _offset_x: u8,
        offset_y: u8,
    ) {
        let tile_line = (scanline as usize) + (offset_y as usize);
        let tilemap_row = tile_line / 8;
        let tile_px = (tile_line) % 8;
        let tiledata_offset = tile_px * 2;
        let row_addr = ((tilemap_row * 32) % 0x400) as u16;
        for x in 0..32_u16 {
            let tile_addr = tiledata.get_addr(self.memory.get(tilemap.get_addr(row_addr + x)))
                + tiledata_offset as u16;
            let lsbs = self.memory.get(tile_addr);
            let msbs = self.memory.get(tile_addr + 1);
            for px_x in 0..8 {
                let lsb = get_bit(lsbs, 7 - px_x);
                let msb = get_bit(msbs, 7 - px_x);
                let colour = bits_to_mapped_colour(lsb, msb, palette);
                let x_coord = ((x * 8) + (px_x as u16)) as usize;
                if x_coord < WIDTH {
                    self.gpu.buffer[(scanline as usize * WIDTH) + x_coord] = colour.to_rgb();
                }
            }
        }
    }

    fn render_window(&mut self) {
        self.gpu.scaled_buffer = scale_buffer(&self.gpu.buffer, WIDTH, HEIGHT, FACTOR);
        self.window
            .update_with_buffer(&self.gpu.scaled_buffer, WIDTH * FACTOR, HEIGHT * FACTOR)
            .unwrap();
    }
}

fn scale_buffer(buffer: &Vec<u32>, width: usize, height: usize, factor: usize) -> Vec<u32> {
    let mut v = vec![];
    for y in 0..height {
        for _ in 0..factor {
            for x in 0..width {
                for _ in 0..factor {
                    v.push(buffer[(y * width) + x]);
                }
            }
        }
    }
    v
}

fn bits_to_mapped_colour(lsb: bool, msb: bool, palette: Palette) -> Colour {
    match (lsb, msb) {
        (true, true) => palette.three,
        (true, false) => palette.one,
        (false, true) => palette.two,
        (false, false) => palette.zero,
    }
}

fn byte_to_palette(byte: u8) -> Palette {
    Palette {
        zero: bits_to_colour(get_bit(byte, 0), get_bit(byte, 1)),
        one: bits_to_colour(get_bit(byte, 2), get_bit(byte, 3)),
        two: bits_to_colour(get_bit(byte, 4), get_bit(byte, 5)),
        three: bits_to_colour(get_bit(byte, 6), get_bit(byte, 7)),
    }
}

fn bits_to_colour(first: bool, second: bool) -> Colour {
    match (first, second) {
        (true, true) => Colour::Black,
        (true, false) => Colour::DarkGray,
        (false, true) => Colour::LightGray,
        (false, false) => Colour::White,
    }
}