use crate::registers::LCDControlRegister;
use std::cmp::min;

// LCD Mode represents the current mode of the LCD controller
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum LCDMode {
    HBlank = 0,  // Horizontal Blank
    VBlank = 1,  // Vertical Blank
    OAMSearch = 2, // Searching OAM (Object Attribute Memory)
    PixelTransfer = 3, // Transferring data to LCD driver
}

// LCD-related constants
pub const SCREEN_WIDTH: usize = 160;
pub const SCREEN_HEIGHT: usize = 144;

// LCD memory addresses
pub const LCDC_ADDR: usize = 0xFF40;
pub const STAT_ADDR: usize = 0xFF41;
pub const SCY_ADDR: usize = 0xFF42;
pub const SCX_ADDR: usize = 0xFF43;
pub const LY_ADDR: usize = 0xFF44;
pub const LYC_ADDR: usize = 0xFF45;
pub const DMA_ADDR: usize = 0xFF46;
pub const BGP_ADDR: usize = 0xFF47;
pub const OBP0_ADDR: usize = 0xFF48;
pub const OBP1_ADDR: usize = 0xFF49;
pub const WY_ADDR: usize = 0xFF4A;
pub const WX_ADDR: usize = 0xFF4B;

// LCD timing constants
pub const MODE_0_CYCLES: u32 = 204; // H-Blank
pub const MODE_1_CYCLES: u32 = 456 * 10; // V-Blank (10 scanlines)
pub const MODE_2_CYCLES: u32 = 80; // OAM Search
pub const MODE_3_CYCLES: u32 = 172; // Pixel Transfer

// OAM (Object Attribute Memory) constants
pub const OAM_SIZE: usize = 160; // 40 sprites * 4 bytes each

// GPU with LCD functionality
#[derive(Debug)]
pub struct GPU {
    pub vram: Vec<u8>,
    pub tile_set: [[[TilePixelValue; 8]; 8]; 384],

    // LCD registers
    pub lcd_control: LCDControlRegister,
    pub lcd_status: u8,
    pub scroll_y: u8,
    pub scroll_x: u8,
    pub ly: u8,  // Current scanline
    pub lyc: u8, // Scanline compare
    pub window_y: u8,
    pub window_x: u8,
    pub bg_palette: u8,
    pub obj_palette0: u8,
    pub obj_palette1: u8,

    // OAM (Object Attribute Memory) for sprites
    pub oam: [u8; OAM_SIZE],

    // LCD screen buffer (160x144 pixels)
    pub screen_buffer: [[u8; SCREEN_WIDTH]; SCREEN_HEIGHT],

    // LCD timing
    pub mode_clock: u32,
    pub mode: LCDMode,
}

#[derive(Copy, Clone, Debug)]
pub enum TilePixelValue { Zero, One, Two, Three }

impl GPU {
    // Create a new GPU with default values
    pub fn new(vram: Vec<u8>) -> Self {
        GPU {
            vram,
            tile_set: [[[TilePixelValue::Zero; 8]; 8]; 384],
            lcd_control: LCDControlRegister::from(0),
            lcd_status: 0,
            scroll_y: 0,
            scroll_x: 0,
            ly: 0,
            lyc: 0,
            window_y: 0,
            window_x: 0,
            bg_palette: 0,
            obj_palette0: 0,
            obj_palette1: 0,
            oam: [0; OAM_SIZE],
            screen_buffer: [[0; SCREEN_WIDTH]; SCREEN_HEIGHT],
            mode_clock: 0,
            mode: LCDMode::HBlank,
        }
    }

    // Read from VRAM
    pub fn read_vram(&self, address: usize) -> u8 {
        self.vram[address]
    }

    // Write to VRAM and update tile data if necessary
    pub fn write_vram(&mut self, index: usize, value: u8) {
        self.vram[index] = value;

        // If our index is greater than 0x1800, we're not writing to the tile set storage
        // so we can just return.
        if index >= 0x1800 { return }

        // Tiles rows are encoded in two bytes with the first byte always
        // on an even address. Bitwise ANDing the address with 0xffe
        // gives us the address of the first byte.
        // For example, `12 & 0xFFFE == 12` and `13 & 0xFFFE == 12`
        let normalized_index = index & 0xFFFE;

        // First, we need to get the two bytes that encode the tile row.
        let byte1 = self.vram[normalized_index];
        let byte2 = self.vram[normalized_index + 1];

        // Tiles are 8 rows tall. Since each row is encoded with two bytes, a tile
        // is therefore 16 bytes in total.
        let tile_index = index / 16;
        // Every two bytes is a new row
        let row_index = (index % 16) / 2;

        // Now we're going to loop 8 times to get the 8 pixels that make up a given row.
        for pixel_index in 0..8 {
            // To determine a pixel's value, we must first find the corresponding bit that encodes
            // that pixel value.
            let mask = 1 << (7 - pixel_index);
            let lsb = byte1 & mask;
            let msb = byte2 & mask;

            // If the masked values are not 0, the masked bit must be 1. If they are 0, the masked
            // bit must be 0.
            let value = match (lsb != 0, msb != 0) {
                (true, true) => TilePixelValue::Three,
                (false, true) => TilePixelValue::Two,
                (true, false) => TilePixelValue::One,
                (false, false) => TilePixelValue::Zero,
            };

            self.tile_set[tile_index][row_index][pixel_index] = value;
        }
    }

    // Read from OAM
    pub fn read_oam(&self, address: usize) -> u8 {
        self.oam[address]
    }

    // Write to OAM
    pub fn write_oam(&mut self, address: usize, value: u8) {
        self.oam[address] = value;
    }

    // Update the LCD state
    pub fn update(&mut self, cycles: u32) {
        // Debug: Track update calls
        static mut UPDATE_COUNTER: u32 = 0;
        static mut FORCE_REFRESH_COUNTER: u32 = 0;
        unsafe {
            UPDATE_COUNTER += 1;
            FORCE_REFRESH_COUNTER += 1;

            if UPDATE_COUNTER % 10000 == 0 {
                println!("GPU update called {} times, cycles={}", UPDATE_COUNTER, cycles);
            }

            // Force a complete refresh of the screen buffer periodically
            if FORCE_REFRESH_COUNTER >= 1_000_000 {
                println!("Forcing complete screen refresh");

                // Force render all scanlines
                for scanline in 0..SCREEN_HEIGHT {
                    self.ly = scanline as u8;
                    self.render_scanline();
                }

                // Reset counter
                FORCE_REFRESH_COUNTER = 0;
                return;
            }
        }

        // If LCD is disabled, reset and return
        if !self.lcd_control.lcd_enabled {
            self.mode_clock = 0;
            self.ly = 0;
            self.mode = LCDMode::HBlank;
            return;
        }

        // Store the old LY value for change detection
        let old_ly = self.ly;

        // Check if we're stuck at LY < 144
        static mut STUCK_COUNTER: u32 = 0;
        unsafe {
            if self.ly < 144 {
                STUCK_COUNTER += 1;
                // If we've been stuck for a while, force LY to increment
                if STUCK_COUNTER > 10000 {
                    println!("Forcing LY increment from {} to {}", self.ly, self.ly + 1);
                    self.ly += 1;
                    if self.ly == 144 {
                        self.mode = LCDMode::VBlank;
                        println!("Forced transition to VBlank at LY = 144");
                    }
                    STUCK_COUNTER = 0;
                    return;
                }
            } else {
                STUCK_COUNTER = 0;
            }
        }

        self.mode_clock += cycles;

        // Update LCD based on current mode
        match self.mode {
            LCDMode::HBlank => {
                if self.mode_clock >= MODE_0_CYCLES {
                    self.mode_clock = 0;
                    self.ly += 1;

                    // Debug print for HBlank mode when approaching VBlank
                    if self.ly >= 140 {
                        println!("HBlank: LY = {} (approaching VBlank)", self.ly);
                    }

                    if self.ly == SCREEN_HEIGHT as u8 {
                        // Enter V-Blank
                        self.mode = LCDMode::VBlank;
                        println!("Entering VBlank mode at LY = {}", self.ly);
                        // TODO: Request V-Blank interrupt
                    } else {
                        // Start next scanline
                        self.mode = LCDMode::OAMSearch;
                    }
                }

                // Check if we're stuck in HBlank mode
                static mut HBLANK_STUCK_COUNTER: u32 = 0;
                unsafe {
                    HBLANK_STUCK_COUNTER += 1;
                    // If we've been stuck in HBlank for a while, force transition to next mode
                    if HBLANK_STUCK_COUNTER > 50000 {
                        println!("Forcing transition from HBlank at LY = {}", self.ly);
                        self.ly += 1;

                        if self.ly == SCREEN_HEIGHT as u8 {
                            // Enter V-Blank
                            self.mode = LCDMode::VBlank;
                            println!("Forced transition to VBlank at LY = {}", self.ly);
                        } else {
                            // Start next scanline
                            self.mode = LCDMode::OAMSearch;
                            println!("Forced transition to OAMSearch at LY = {}", self.ly);
                        }

                        HBLANK_STUCK_COUNTER = 0;
                    }
                }
            },
            LCDMode::VBlank => {
                if self.mode_clock >= MODE_1_CYCLES / 10 {
                    self.mode_clock = 0;
                    self.ly += 1;

                    // Debug print for VBlank mode
                    println!("VBlank: LY = {}", self.ly);

                    if self.ly > 153 {
                        // End of V-Blank, start new frame
                        self.ly = 0;
                        self.mode = LCDMode::OAMSearch;
                        println!("End of VBlank, starting new frame");
                    }
                }

                // Check if we're stuck in VBlank mode
                static mut VBLANK_STUCK_COUNTER: u32 = 0;
                unsafe {
                    VBLANK_STUCK_COUNTER += 1;
                    // If we've been stuck in VBlank for a while, force transition to next frame
                    if VBLANK_STUCK_COUNTER > 100000 {
                        println!("Forcing end of VBlank, LY was {}", self.ly);
                        self.ly = 0;
                        self.mode = LCDMode::OAMSearch;
                        VBLANK_STUCK_COUNTER = 0;
                    }
                }
            },
            LCDMode::OAMSearch => {
                if self.mode_clock >= MODE_2_CYCLES {
                    self.mode_clock = 0;
                    self.mode = LCDMode::PixelTransfer;

                    // Debug print for OAMSearch mode when approaching VBlank
                    if self.ly >= 140 {
                        println!("OAMSearch -> PixelTransfer at LY = {}", self.ly);
                    }
                }

                // Check if we're stuck in OAMSearch mode
                static mut OAMSEARCH_STUCK_COUNTER: u32 = 0;
                unsafe {
                    OAMSEARCH_STUCK_COUNTER += 1;
                    // If we've been stuck in OAMSearch for a while, force transition to PixelTransfer
                    if OAMSEARCH_STUCK_COUNTER > 50000 {
                        println!("Forcing transition from OAMSearch to PixelTransfer at LY = {}", self.ly);
                        self.mode = LCDMode::PixelTransfer;
                        OAMSEARCH_STUCK_COUNTER = 0;
                    }
                }
            },
            LCDMode::PixelTransfer => {
                if self.mode_clock >= MODE_3_CYCLES {
                    self.mode_clock = 0;
                    self.mode = LCDMode::HBlank;

                    // Debug print for PixelTransfer mode when approaching VBlank
                    if self.ly >= 140 {
                        println!("PixelTransfer -> HBlank at LY = {}", self.ly);
                    }

                    // Render scanline
                    self.render_scanline();
                }

                // Check if we're stuck in PixelTransfer mode
                static mut PIXELTRANSFER_STUCK_COUNTER: u32 = 0;
                unsafe {
                    PIXELTRANSFER_STUCK_COUNTER += 1;
                    // If we've been stuck in PixelTransfer for a while, force transition to HBlank
                    if PIXELTRANSFER_STUCK_COUNTER > 50000 {
                        println!("Forcing transition from PixelTransfer to HBlank at LY = {}", self.ly);
                        self.mode = LCDMode::HBlank;
                        PIXELTRANSFER_STUCK_COUNTER = 0;
                        // Render scanline
                        self.render_scanline();
                    }
                }
            }
        }

        // Update LCD status register
        self.update_status();

        // Print debug info when LY changes
        // if self.ly != old_ly {
        //     println!("LY changed: {} -> {} (Mode: {:?})", old_ly, self.ly, self.mode);
        // }
    }

    // Update the LCD status register
    fn update_status(&mut self) {
        // Update mode bits
        self.lcd_status = (self.lcd_status & 0xFC) | (self.mode as u8);

        // Check LYC=LY coincidence flag
        if self.ly == self.lyc {
            self.lcd_status |= 0x04;
            // TODO: Request LYC=LY interrupt if enabled
        } else {
            self.lcd_status &= !0x04;
        }

        // TODO: Check for mode interrupts
    }

    // Render a single scanline
    fn render_scanline(&mut self) {
        // Debug print for rendering
        if self.ly % 20 == 0 {  // Print every 20th scanline to avoid flooding the console
            println!("Rendering scanline {} (Mode: {:?})", self.ly, self.mode);
            println!("  LCD Control: {:?}", self.lcd_control);
            println!("  Background Palette: 0x{:02X}", self.bg_palette);
        }

        if self.lcd_control.bg_and_window_enable {
            self.render_background();

            if self.lcd_control.window_enable {
                self.render_window();
            }
        } else {
            if self.ly % 20 == 0 {
                println!("  Background and window display disabled!");
            }
        }

        if self.lcd_control.object_enable {
            self.render_sprites();
        }

        // Debug: Check if any pixels are set in this scanline
        if self.ly % 20 == 0 {
            let mut non_zero_pixels = 0;
            for x in 0..SCREEN_WIDTH {
                if self.screen_buffer[self.ly as usize][x] > 0 {
                    non_zero_pixels += 1;
                }
            }
            println!("  Non-zero pixels in scanline {}: {}/{}", self.ly, non_zero_pixels, SCREEN_WIDTH);

            // Print a sample of the screen buffer for this scanline
            if non_zero_pixels > 0 {
                println!("  Sample of screen buffer for scanline {}:", self.ly);
                for x in 0..min(20, SCREEN_WIDTH) {  // Print first 20 pixels
                    print!("{} ", self.screen_buffer[self.ly as usize][x]);
                    if (x + 1) % 10 == 0 {
                        println!();
                    }
                }
                println!();
            }
        }
    }

    // Render the background for the current scanline
    fn render_background(&mut self) {
        let tile_map_area = if self.lcd_control.bg_tile_map_area { 0x9C00 } else { 0x9800 };
        let tile_data_area = if self.lcd_control.bg_and_window_tile_area { 0x8000 } else { 0x8800 };
        let signed_addressing = tile_data_area == 0x8800;

        // Debug print for rendering
        if self.ly == 80 {  // Only print for a specific scanline to avoid flooding the console
            println!("Rendering background for scanline {}", self.ly);
            println!("  Tile map area: 0x{:04X}", tile_map_area);
            println!("  Tile data area: 0x{:04X}", tile_data_area);
            println!("  Signed addressing: {}", signed_addressing);
            println!("  Background palette: 0x{:02X}", self.bg_palette);
            println!("  LCD Control: {:?}", self.lcd_control);
        }

        let y_pos = self.ly.wrapping_add(self.scroll_y);
        let tile_row = (y_pos / 8) as usize;

        // Track if any non-zero pixels are set
        let mut non_zero_pixels = 0;

        for x in 0..SCREEN_WIDTH {
            let x_pos = (x as u8).wrapping_add(self.scroll_x);
            let tile_col = (x_pos / 8) as usize;

            // Get the tile index from the tile map
            let tile_map_addr = tile_map_area - 0x8000 + tile_row * 32 + tile_col;
            let tile_index = self.vram[tile_map_addr];

            // Get the tile data
            let tile_data_addr = if signed_addressing {
                // 8800 method uses signed addressing
                let signed_index = tile_index as i8;
                // Calculate the offset in i16 to handle negative indices correctly
                let offset = 0x1000i16 + (signed_index as i16 * 16);
                // Ensure the result is non-negative before converting to usize
                if offset < 0 {
                    // Handle the error case - use a default address or log an error
                    println!("Warning: Negative tile data address calculated in render_background: {}", offset);
                    0 // Use tile 0 as a fallback
                } else {
                    offset as usize
                }
            } else {
                // 8000 method uses unsigned addressing
                (tile_data_area - 0x8000) + (tile_index as usize * 16)
            };

            // Get the specific row of the tile
            let row = (y_pos % 8) as usize;
            let row_addr = tile_data_addr + row * 2;

            // Get the pixel data for the row
            let byte1 = self.vram[row_addr];
            let byte2 = self.vram[row_addr + 1];

            // Get the specific pixel in the row
            let bit = 7 - (x_pos % 8);
            let pixel = ((byte1 >> bit) & 1) | (((byte2 >> bit) & 1) << 1);

            // Map the pixel value through the palette
            let color = (self.bg_palette >> (pixel * 2)) & 0x03;

            // Debug: Print detailed palette mapping for a few pixels
            if self.ly == 80 && x < 5 {
                println!("  Pixel mapping: value={}, palette=0x{:02X}, shift={}, color={}",
                         pixel, self.bg_palette, pixel * 2, color);
            }

            // Set the pixel in the screen buffer
            self.screen_buffer[self.ly as usize][x] = color;

            // Count non-zero pixels
            if color > 0 {
                non_zero_pixels += 1;
            }

            // Debug print for the first few pixels of a specific scanline
            if self.ly == 80 && x < 5 {
                println!("  Pixel at ({}, {}): value={}, color={}", x, self.ly, pixel, color);
                println!("    Tile index: 0x{:02X}, Tile data addr: 0x{:04X}", tile_index, tile_data_addr);
                println!("    Byte1: 0x{:02X}, Byte2: 0x{:02X}, Bit: {}", byte1, byte2, bit);
            }
        }

        // Debug print for non-zero pixels
        if self.ly == 80 {
            println!("  Non-zero pixels in scanline {}: {}/{}", self.ly, non_zero_pixels, SCREEN_WIDTH);
        }
    }

    // Render the window for the current scanline
    fn render_window(&mut self) {
        // Only render if the window is visible on this scanline
        if self.window_y > self.ly {
            return;
        }

        let tile_map_area = if self.lcd_control.window_tile_map_area { 0x9C00 } else { 0x9800 };
        let tile_data_area = if self.lcd_control.bg_and_window_tile_area { 0x8000 } else { 0x8800 };
        let signed_addressing = tile_data_area == 0x8800;

        let y_pos = self.ly - self.window_y;
        let tile_row = (y_pos / 8) as usize;

        for x in 0..SCREEN_WIDTH {
            // Window X position is offset by 7
            let window_x = self.window_x.wrapping_sub(7);

            // Only render if this pixel is within the window
            if (x as u8) < window_x {
                continue;
            }

            let x_pos = (x as u8) - window_x;
            let tile_col = (x_pos / 8) as usize;

            // Get the tile index from the tile map
            let tile_map_addr = tile_map_area - 0x8000 + tile_row * 32 + tile_col;
            let tile_index = self.vram[tile_map_addr];

            // Get the tile data
            let tile_data_addr = if signed_addressing {
                // 8800 method uses signed addressing
                let signed_index = tile_index as i8;
                // Calculate the offset in i16 to handle negative indices correctly
                let offset = 0x1000i16 + (signed_index as i16 * 16);
                // Ensure the result is non-negative before converting to usize
                if offset < 0 {
                    // Handle the error case - use a default address or log an error
                    println!("Warning: Negative tile data address calculated in render_window: {}", offset);
                    0 // Use tile 0 as a fallback
                } else {
                    offset as usize
                }
            } else {
                // 8000 method uses unsigned addressing
                (tile_data_area - 0x8000) + (tile_index as usize * 16)
            };

            // Get the specific row of the tile
            let row = (y_pos % 8) as usize;
            let row_addr = tile_data_addr + row * 2;

            // Get the pixel data for the row
            let byte1 = self.vram[row_addr];
            let byte2 = self.vram[row_addr + 1];

            // Get the specific pixel in the row
            let bit = 7 - (x_pos % 8);
            let pixel = ((byte1 >> bit) & 1) | (((byte2 >> bit) & 1) << 1);

            // Map the pixel value through the palette
            let color = (self.bg_palette >> (pixel * 2)) & 0x03;

            // Set the pixel in the screen buffer
            self.screen_buffer[self.ly as usize][x] = color;
        }
    }

    // Render sprites for the current scanline
    fn render_sprites(&mut self) {
        // Sprite height depends on the object size flag
        let sprite_height = if self.lcd_control.object_size { 16 } else { 8 };

        // We can have up to 10 sprites per scanline
        let mut sprites_on_line = 0;

        // Check all 40 sprites
        for sprite_idx in 0..40 {
            if sprites_on_line >= 10 {
                break;
            }

            let sprite_addr = sprite_idx * 4;
            let sprite_y = self.oam[sprite_addr].wrapping_sub(16);
            let sprite_x = self.oam[sprite_addr + 1].wrapping_sub(8);
            let tile_idx = self.oam[sprite_addr + 2];
            let attributes = self.oam[sprite_addr + 3];

            // Check if sprite is on this scanline
            if self.ly < sprite_y || self.ly >= sprite_y.wrapping_add(sprite_height) {
                continue;
            }

            sprites_on_line += 1;

            // Get sprite flags
            let palette = if attributes & 0x10 != 0 { self.obj_palette1 } else { self.obj_palette0 };
            let x_flip = attributes & 0x20 != 0;
            let y_flip = attributes & 0x40 != 0;
            let priority = attributes & 0x80 != 0;

            // Calculate the row of the sprite to use
            let mut row = (self.ly - sprite_y) as usize;
            if y_flip {
                row = sprite_height as usize - 1 - row;
            }

            // For 8x16 sprites, the bottom half uses the next tile
            let tile = if sprite_height == 16 && row >= 8 {
                (tile_idx & 0xFE) + 1
            } else {
                tile_idx
            };

            // Get the tile data address
            let tile_addr = (0x8000 - 0x8000) + (tile as usize * 16) + (row % 8) * 2;

            // Get the pixel data for the row
            let byte1 = self.vram[tile_addr];
            let byte2 = self.vram[tile_addr + 1];

            // Draw the sprite pixels
            for x in 0..8 {
                // Skip if sprite pixel is off-screen
                if sprite_x.wrapping_add(x) >= SCREEN_WIDTH as u8 {
                    continue;
                }

                // Get the pixel bit (flipped if needed)
                let bit = if x_flip { x } else { 7 - x };
                let pixel = ((byte1 >> bit) & 1) | (((byte2 >> bit) & 1) << 1);

                // Skip transparent pixels
                if pixel == 0 {
                    continue;
                }

                // Check background priority
                let screen_x = sprite_x.wrapping_add(x) as usize;
                if priority && self.screen_buffer[self.ly as usize][screen_x] != 0 {
                    continue;
                }

                // Map the pixel value through the palette
                let color = (palette >> (pixel * 2)) & 0x03;

                // Set the pixel in the screen buffer
                self.screen_buffer[self.ly as usize][screen_x] = color;
            }
        }
    }
}
