use crate::{
    memory_mapped::{MemoryMapped, MemoryMapped1DArray, MemoryMapped2DArray},
    single::{Single, SingleToken},
};
use bitflags::bitflags;
use core::convert::TryInto;

const DISPLAY_CONTROL: MemoryMapped<u16> = unsafe { MemoryMapped::new(0x0400_0000) };
const DISPLAY_STATUS: MemoryMapped<u16> = unsafe { MemoryMapped::new(0x0400_0004) };
const VCOUNT: MemoryMapped<u16> = unsafe { MemoryMapped::new(0x0400_0006) };

const PALETTE_BACKGROUND: MemoryMapped1DArray<u16, 256> =
    unsafe { MemoryMapped1DArray::new(0x0500_0000) };
const PALETTE_SPRITE: MemoryMapped1DArray<u16, 256> =
    unsafe { MemoryMapped1DArray::new(0x0500_0200) };

const BITMAP_MODE_3: MemoryMapped2DArray<u16, { WIDTH as usize }, { HEIGHT as usize }> =
    unsafe { MemoryMapped2DArray::new(0x600_0000) };

const BITMAP_PAGE_FRONT_MODE_4: MemoryMapped2DArray<
    u16,
    { (WIDTH / 2) as usize },
    { HEIGHT as usize },
> = unsafe { MemoryMapped2DArray::new(0x600_0000) };
const BITMAP_PAGE_BACK_MODE_4: MemoryMapped2DArray<
    u16,
    { (WIDTH / 2) as usize },
    { HEIGHT as usize },
> = unsafe { MemoryMapped2DArray::new(0x600_A000) };

pub const WIDTH: i32 = 240;
pub const HEIGHT: i32 = 160;

pub enum DisplayMode {
    Tiled0 = 0,
    Tiled1 = 1,
    Tiled2 = 2,
    Bitmap3 = 3,
    Bitmap4 = 4,
    Bitmap5 = 5,
}

pub enum Page {
    Front = 0,
    Back = 1,
}

bitflags! {
    pub struct GraphicsSettings: u16 {
        const PAGE_SELECT = 1 << 0x4;
        const OAM_HBLANK = 1 << 0x5;
        const SPRITE1_D = 1 << 0x6;
        const SCREEN_BLANK = 1 << 0x7;
        const LAYER_BG0 = 1 << 0x8;
        const LAYER_BG1 = 1 << 0x9;
        const LAYER_BG2 = 1 << 0xA;
        const LAYER_BG3 = 1  << 0xB;
        const LAYER_OBJ = 1 << 0xC;
        const WINDOW0 = 1 << 0xD;
        const WINDOW1 = 1 << 0xE;
        const WINDOW_OBJECT = 1 << 0xF;
    }
}

pub struct Display {
    in_mode: Single,
}

impl Display {
    pub(crate) const unsafe fn new() -> Self {
        Display {
            in_mode: Single::new(),
        }
    }

    pub fn bitmap3(&self) -> Bitmap3 {
        Bitmap3::new(
            self.in_mode
                .take()
                .expect("Cannot create new mode as mode already taken"),
        )
    }
    pub fn bitmap4(&self) -> Bitmap4 {
        Bitmap4::new(
            self.in_mode
                .take()
                .expect("Cannot create new mode as mode already taken"),
        )
    }
}

pub struct Bitmap3<'a> {
    _in_mode: SingleToken<'a>,
}

impl<'a> Bitmap3<'a> {
    fn new(in_mode: SingleToken<'a>) -> Self {
        set_graphics_mode(DisplayMode::Bitmap3);
        set_graphics_settings(GraphicsSettings::LAYER_BG2);
        Bitmap3 { _in_mode: in_mode }
    }
    pub fn draw_point(&self, x: i32, y: i32, colour: u16) {
        let x = x.try_into().unwrap();
        let y = y.try_into().unwrap();
        BITMAP_MODE_3.set(x, y, colour)
    }
}

pub struct Bitmap4<'a> {
    _in_mode: SingleToken<'a>,
}

impl<'a> Bitmap4<'a> {
    fn new(in_mode: SingleToken<'a>) -> Self {
        set_graphics_mode(DisplayMode::Bitmap4);
        set_graphics_settings(GraphicsSettings::LAYER_BG2);
        Bitmap4 { _in_mode: in_mode }
    }

    pub fn draw_point_page(&self, x: i32, y: i32, colour: u8, page: Page) {
        let addr = match page {
            Page::Front => BITMAP_PAGE_FRONT_MODE_4,
            Page::Back => BITMAP_PAGE_BACK_MODE_4,
        };

        let x_in_screen = (x / 2) as usize;
        let y_in_screen = y as usize;

        let c = addr.get(x_in_screen, y_in_screen);
        if x & 0b1 != 0 {
            addr.set(x_in_screen, y_in_screen, c | (colour as u16) << 8);
        } else {
            addr.set(x_in_screen, y_in_screen, c | colour as u16);
        }
    }

    pub fn draw_point(&self, x: i32, y: i32, colour: u8) {
        let disp = DISPLAY_CONTROL.get();

        let page = if disp & GraphicsSettings::PAGE_SELECT.bits() != 0 {
            Page::Back
        } else {
            Page::Front
        };

        self.draw_point_page(x, y, colour, page)
    }

    pub fn set_palette_entry(&self, entry: u32, colour: u16) {
        PALETTE_BACKGROUND.set(entry as usize, colour);
    }

    pub fn flip_page(&self) {
        let disp = DISPLAY_CONTROL.get();
        let swapped = disp ^ GraphicsSettings::PAGE_SELECT.bits();
        DISPLAY_CONTROL.set(swapped);
    }
}

fn set_graphics_mode(mode: DisplayMode) {
    let current = DISPLAY_CONTROL.get();
    let current = current & (!0b111);
    let s = current | (mode as u16 & 0b111);

    DISPLAY_CONTROL.set(s);
}

pub fn set_graphics_settings(settings: GraphicsSettings) {
    let current = DISPLAY_CONTROL.get();
    // preserve display mode
    let current = current & 0b111;
    let s = settings.bits() | current;

    DISPLAY_CONTROL.set(s);
}

#[allow(non_snake_case)]
pub fn busy_wait_for_VBlank() {
    while VCOUNT.get() >= 160 {}
    while VCOUNT.get() < 160 {}
}

#[allow(non_snake_case)]
pub fn enable_VBlank_interrupt() {
    let status = DISPLAY_STATUS.get() | (1 << 3);
    DISPLAY_STATUS.set(status);
}

#[allow(non_snake_case)]
pub fn wait_for_VBlank() {
    crate::syscall::wait_for_VBlank();
}