use std::cell::RefCell;

use macroquad::prelude::*;

pub const WIDTH: usize = 320;
pub const HEIGHT: usize = 200;
pub const PIXELS: usize = WIDTH * HEIGHT;

/// DOS/VGA mode 13h palette, converted to 8-bit RGB.
pub const PALETTE: [[u8; 3]; 256] = build_mode13h_palette();

thread_local! {
    static STATE: RefCell<State> = RefCell::new(State::new());
}

struct State {
    pixels: Vec<u8>,
    image: Image,
    texture: Option<Texture2D>,
    dirty: bool,
}

impl State {
    fn new() -> Self {
        let mut bytes = vec![0; PIXELS * 4];
        for rgba in bytes.chunks_exact_mut(4) {
            rgba[3] = 255;
        }

        Self {
            pixels: vec![0; PIXELS],
            image: Image {
                bytes,
                width: WIDTH as u16,
                height: HEIGHT as u16,
            },
            texture: None,
            dirty: true,
        }
    }

    fn init_texture(&mut self) {
        if self.texture.is_some() {
            return;
        }

        let texture = Texture2D::from_image(&self.image);
        texture.set_filter(FilterMode::Nearest);
        self.texture = Some(texture);
        self.dirty = true;
    }

    fn write_image_pixel(&mut self, x: usize, y: usize, color: u8) {
        let [r, g, b] = PALETTE[color as usize];
        let offset = (y * WIDTH + x) * 4;
        self.image.bytes[offset] = r;
        self.image.bytes[offset + 1] = g;
        self.image.bytes[offset + 2] = b;
        self.image.bytes[offset + 3] = 255;
        self.dirty = true;
    }
}

/// Recommended macroquad window configuration for this framebuffer.
///
/// Use it by wrapping it in a local function for macroquad's attribute macro.
pub fn window_conf() -> Conf {
    Conf {
        window_title: "picotic".to_owned(),
        window_width: (WIDTH * 3) as i32,
        window_height: (HEIGHT * 3) as i32,
        window_resizable: true,
        ..Default::default()
    }
}

/// Initialize the framebuffer texture.
///
/// Call once after macroquad has started, before the main loop. Calling it more
/// than once is harmless.
pub fn init() {
    STATE.with(|state| state.borrow_mut().init_texture());
}

/// Put a pixel to the screen.
///
/// Out-of-bounds coordinates are ignored.
pub fn pset(x: i32, y: i32, color: u8) {
    if !(0..WIDTH as i32).contains(&x) || !(0..HEIGHT as i32).contains(&y) {
        return;
    }

    STATE.with(|state| {
        let mut state = state.borrow_mut();
        let x = x as usize;
        let y = y as usize;
        state.pixels[y * WIDTH + x] = color;
        state.write_image_pixel(x, y, color);
    });
}

/// Read a pixel's screen value.
///
/// Out-of-bounds coordinates return `0`.
pub fn pget(x: i32, y: i32) -> u8 {
    if !(0..WIDTH as i32).contains(&x) || !(0..HEIGHT as i32).contains(&y) {
        return 0;
    }

    STATE.with(|state| {
        let state = state.borrow();
        state.pixels[y as usize * WIDTH + x as usize]
    })
}

/// Clear the whole screen with a given color.
pub fn cls(color: u8) {
    STATE.with(|state| {
        let mut state = state.borrow_mut();
        state.pixels.fill(color);

        let [r, g, b] = PALETTE[color as usize];
        for rgba in state.image.bytes.chunks_exact_mut(4) {
            rgba[0] = r;
            rgba[1] = g;
            rgba[2] = b;
            rgba[3] = 255;
        }
        state.dirty = true;
    });
}

/// Return whether a button is held.
///
/// Mapping:
/// - `1`: left arrow
/// - `2`: right arrow
/// - `3`: up arrow
/// - `4`: down arrow
/// - `5`: space, left ctrl, or right ctrl
/// - `6`: any other key
pub fn btn(i: u8) -> bool {
    use KeyCode::*;

    match i {
        1 => is_key_down(Left),
        2 => is_key_down(Right),
        3 => is_key_down(Up),
        4 => is_key_down(Down),
        5 => is_key_down(Space) || is_key_down(LeftControl) || is_key_down(RightControl),
        6 => get_keys_down().into_iter().any(|key| {
            !matches!(
                key,
                Left | Right | Up | Down | Space | LeftControl | RightControl
            )
        }),
        _ => false,
    }
}

pub async fn next_frame() {
    render();
    macroquad::prelude::next_frame().await;
}

pub fn render() {
    STATE.with(|state| {
        let mut state = state.borrow_mut();
        state.init_texture();

        let texture = state
            .texture
            .as_ref()
            .expect("texture was initialized")
            .clone();
        if state.dirty {
            texture.update(&state.image);
            state.dirty = false;
        }

        let scale = (screen_width() / WIDTH as f32).min(screen_height() / HEIGHT as f32);
        let dest_width = WIDTH as f32 * scale;
        let dest_height = HEIGHT as f32 * scale;
        let x = (screen_width() - dest_width) * 0.5;
        let y = (screen_height() - dest_height) * 0.5;

        clear_background(BLACK);
        draw_texture_ex(
            &texture,
            x,
            y,
            WHITE,
            DrawTextureParams {
                dest_size: Some(vec2(dest_width, dest_height)),
                ..Default::default()
            },
        );
    });
}

/// Convert a palette index to 8-bit RGB.
pub fn palette_rgb(color: u8) -> [u8; 3] {
    PALETTE[color as usize]
}

const fn build_mode13h_palette() -> [[u8; 3]; 256] {
    let mut out = [[0; 3]; 256];

    let ega = [
        [0, 0, 0],
        [0, 0, 42],
        [0, 42, 0],
        [0, 42, 42],
        [42, 0, 0],
        [42, 0, 42],
        [42, 21, 0],
        [42, 42, 42],
        [21, 21, 21],
        [21, 21, 63],
        [21, 63, 21],
        [21, 63, 63],
        [63, 21, 21],
        [63, 21, 63],
        [63, 63, 21],
        [63, 63, 63],
    ];

    let mut i = 0;
    while i < 16 {
        out[i] = dac_to_rgb(ega[i]);
        i += 1;
    }

    let grays = [0, 5, 8, 11, 14, 17, 20, 24, 28, 32, 36, 40, 45, 50, 56, 63];
    i = 0;
    while i < 16 {
        out[16 + i] = dac_to_rgb([grays[i], grays[i], grays[i]]);
        i += 1;
    }

    let rings = [
        [0, 63],
        [31, 63],
        [45, 63],
        [0, 28],
        [14, 28],
        [20, 28],
        [0, 16],
        [8, 16],
        [11, 16],
    ];

    let mut ring = 0;
    let mut dst = 32;
    while ring < rings.len() {
        let colors = hue_ring(rings[ring][0], rings[ring][1]);
        let mut c = 0;
        while c < 24 {
            out[dst] = dac_to_rgb(colors[c]);
            dst += 1;
            c += 1;
        }
        ring += 1;
    }

    out
}

const fn hue_ring(low: u8, high: u8) -> [[u8; 3]; 24] {
    let s = steps(low, high);
    [
        [s[0], low, high],
        [s[1], low, high],
        [s[2], low, high],
        [s[3], low, high],
        [s[4], low, high],
        [high, low, s[3]],
        [high, low, s[2]],
        [high, low, s[1]],
        [high, low, low],
        [high, s[1], low],
        [high, s[2], low],
        [high, s[3], low],
        [high, high, low],
        [s[3], high, low],
        [s[2], high, low],
        [s[1], high, low],
        [low, high, low],
        [low, high, s[1]],
        [low, high, s[2]],
        [low, high, s[3]],
        [low, high, high],
        [low, s[3], high],
        [low, s[2], high],
        [low, s[1], high],
    ]
}

const fn steps(low: u8, high: u8) -> [u8; 5] {
    let span = high - low;
    [
        low,
        low + ((span as u16 + 1) / 4) as u8,
        low + ((span as u16 * 2 + 1) / 4) as u8,
        low + ((span as u16 * 3 + 1) / 4) as u8,
        high,
    ]
}

const fn dac_to_rgb(rgb: [u8; 3]) -> [u8; 3] {
    [dac_to_u8(rgb[0]), dac_to_u8(rgb[1]), dac_to_u8(rgb[2])]
}

const fn dac_to_u8(v: u8) -> u8 {
    ((v as u16 * 255 + 31) / 63) as u8
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn first_sixteen_colors_are_ega_colors() {
        assert_eq!(palette_rgb(0), [0, 0, 0]);
        assert_eq!(palette_rgb(1), [0, 0, 170]);
        assert_eq!(palette_rgb(6), [170, 85, 0]);
        assert_eq!(palette_rgb(15), [255, 255, 255]);
    }

    #[test]
    fn pixels_can_be_set_read_and_cleared_without_a_window() {
        cls(3);
        assert_eq!(pget(10, 20), 3);
        pset(10, 20, 12);
        assert_eq!(pget(10, 20), 12);
        assert_eq!(pget(-1, 20), 0);
        assert_eq!(pget(WIDTH as i32, 20), 0);
    }
}
