mod sprite_data;
mod draw_line;
mod draw_rect;
mod draw_sprite;

pub use sprite_data::*;
use geometry::HasDimensions;
use phosphor::*;
use std::cmp::{min, max, Ordering};
use std::iter::zip;

pub type ScreenDimensions = geometry::Dimensions<u16>;
pub type ScreenPosition = geometry::Point<u16>;
pub type Plane = [u8; 8];
pub type Sprite = [[u8; 8]; 8];

const TRANSPARENT:     u8 = 0x08;
const FLIP_DIAGONAL:   u8 = 0x04;
const FLIP_VERTICAL:   u8 = 0x02;
const FLIP_HORIZONTAL: u8 = 0x01;
const NEGATIVE:        u8 = 0x80;
const VERTICAL:        u8 = 0x40;


#[derive(Copy, Clone)]
pub enum ScreenLayer { Background, Foreground }

macro_rules! test { ($value:expr, $mask:expr) => { $value & $mask != 0 }; }


pub struct ScreenDevice {
    pub wake_flag: bool,

    /// Each byte represents a screen pixel, left-to-right and top-to-bottom.
    // Only the bottom four bits of each byte are used.
    pub foreground: Vec<u8>,
    pub background: Vec<u8>,
    pub dirty: bool,
    pub resizable: bool,

    pub cursor: ScreenPosition,
    pub vector: ScreenPosition,
    pub dimensions: ScreenDimensions,

    pub palette_high: u8,
    pub palette: [Colour; 16],
    pub sprite_buffer: SpriteBuffer,
}

impl ScreenDevice {
    pub fn new() -> Self {
        Self {
            wake_flag: false,

            foreground: Vec::new(),
            background: Vec::new(),
            dirty: false,
            resizable: true,

            cursor: ScreenPosition::ZERO,
            vector: ScreenPosition::ZERO,
            dimensions: ScreenDimensions::ZERO,

            palette_high: 0,
            palette: [Colour::BLACK; 16],
            sprite_buffer: SpriteBuffer::new(),
        }
    }

    pub fn set_size(&mut self) {
        self.resizable = false;
        self.resize(self.dimensions);
    }

    // Resize the screen buffers while preserving the current content.
    pub fn resize(&mut self, dimensions: ScreenDimensions) {
        let old_width  = self.dimensions.width  as usize;
        let old_height = self.dimensions.height as usize;
        let new_width  = dimensions.width  as usize;
        let new_height = dimensions.height as usize;
        let new_area = dimensions.area_usize();
        let y_range = 0..min(old_height, new_height);

        match new_width.cmp(&old_width) {
            Ordering::Less => {
                for y in y_range {
                    let from = y * old_width;
                    let to   = y * new_width;
                    let len  = new_width;
                    self.foreground.copy_within(from..from+len, to);
                    self.background.copy_within(from..from+len, to);
                }
                self.foreground.resize(new_area, 0);
                self.background.resize(new_area, 0);
            },
            Ordering::Greater => {
                self.foreground.resize(new_area, 0);
                self.background.resize(new_area, 0);
                for y in y_range.rev() {
                    let from = y * old_width;
                    let to   = y * new_width;
                    let len  = old_width;
                    self.foreground.copy_within(from..from+len, to);
                    self.background.copy_within(from..from+len, to);
                    self.foreground[to+len..to+new_width].fill(0);
                    self.background[to+len..to+new_width].fill(0);
                }
            },
            Ordering::Equal => {
                self.foreground.resize(new_area, 0);
                self.background.resize(new_area, 0);
            },
        };

        self.dimensions = dimensions;
        self.dirty = true;
        self.wake_flag = true;
    }

    pub fn render(&mut self, buffer: &mut Buffer) {
        // Pre-calculate a lookup table for the colour palette
        let mut lookup = [Colour::BLACK; 256];
        for (i, c) in lookup.iter_mut().enumerate() {
            match i > 0x0f {
                true  => *c = self.palette[i >> 4],
                false => *c = self.palette[i & 0x0f],
            }
        };
        // Prepare values
        let b_width  = buffer.width()  as usize;
        let b_height = buffer.height() as usize;
        let s_width  = self.dimensions.width()  as usize;
        let s_height = self.dimensions.height() as usize;

        // Write colours to the buffer
        if b_width == s_width && b_height == s_height {
            let screen_iter = zip(&self.background, &self.foreground);
            let buffer_iter = buffer.as_mut_slice();
            for (b, (bg, fg)) in zip(buffer_iter, screen_iter) {
                *b = lookup[(fg << 4 | bg) as usize];
            }
        // Write colours to the buffer when the size of the buffer is wrong
        } else {
            let width = min(b_width, s_width);
            let height = min(b_height, s_height);
            let width_excess = b_width.saturating_sub(width);
            let b_slice = &mut buffer.as_mut_slice();
            let mut bi = 0;
            let mut si = 0;
            for _ in 0..height {
                let b_iter = &mut b_slice[bi..bi+width];
                let s_iter = zip(
                    &self.background[si..si+width],
                    &self.foreground[si..si+width],
                );
                for (b, (bg, fg)) in zip(b_iter, s_iter) {
                    *b = lookup[(fg << 4 | bg) as usize];
                }
                b_slice[bi+width..bi+width+width_excess].fill(lookup[0]);
                bi += b_width;
                si += s_width;
            }
            b_slice[bi..].fill(lookup[0]);
        }
        self.dirty = false;
    }

    pub fn set_palette_high(&mut self, val: u8) {
        self.palette_high = val;
    }

    pub fn set_palette_low(&mut self, val: u8) {
        let index = (self.palette_high >> 4) as usize;
        let red   = (self.palette_high & 0x0f) * 17;
        let green =                 (val >> 4) * 17;
        let blue  =               (val & 0x0f) * 17;
        self.palette[index] = Colour::from_rgb(red, green, blue);
        self.dirty = true;
    }

    pub fn shunt(&mut self, val: u8) {
        let negative = test!(val, NEGATIVE);
        let vertical = test!(val, VERTICAL);
        let dist = (val & 0x3f) as u16;
        match (negative, vertical) {
            (false, false) => self.cursor.x = self.cursor.x.wrapping_add(dist),
            (false,  true) => self.cursor.y = self.cursor.y.wrapping_add(dist),
            ( true, false) => self.cursor.x = self.cursor.x.wrapping_sub(dist),
            ( true,  true) => self.cursor.y = self.cursor.y.wrapping_sub(dist),
        };
    }

    pub fn draw(&mut self, val: u8) {
        let operation  = val & 0x70;
        let parameters = val & 0x0f;
        let layer = match val & 0x80 != 0 {
            true => ScreenLayer::Foreground,
            false => ScreenLayer::Background
        };
        match operation {
            0x00 =>       self.draw_pixel(parameters, layer, self.cursor),
            0x10 => self.draw_sprite_1bit(parameters, layer),
            0x20 =>       self.fill_layer(parameters, layer),
            0x30 => self.draw_sprite_2bit(parameters, layer),
            0x40 =>        self.draw_line(parameters, layer),
            0x50 =>        self.draw_rect(parameters, layer),
            0x60 =>   self.draw_line_1bit(parameters, layer),
            0x70 =>   self.draw_rect_1bit(parameters, layer),
            _ => unreachable!(),
        };

        self.dirty = true;
        self.vector = self.cursor;
    }

    // Draw a single pixel of a single colour
    fn draw_pixel(&mut self, colour: u8, layer: ScreenLayer, point: ScreenPosition) {
        let dim = self.dimensions;
        if !dim.contains_point(point) || colour > 0xf { return }
        let index = point.x as usize + ((dim.width as usize) * (point.y as usize));
        match layer {
            ScreenLayer::Background => self.background[index] = colour,
            ScreenLayer::Foreground => self.foreground[index] = colour,
        };
    }

    // Fill an entire screen layer with a single colour
    fn fill_layer(&mut self, colour: u8, layer: ScreenLayer) {
        match layer {
            ScreenLayer::Background => self.background.fill(colour),
            ScreenLayer::Foreground => self.foreground.fill(colour),
        }
    }

    /// Returns [x0, y0, x1, y1], ensuring that x0 <= x1 and y0 <= y1
    fn find_vector_bounding_box(&self) -> Option<[usize; 4]> {
        macro_rules! raise {($v:expr) => {$v.wrapping_add(0x8000)};}
        macro_rules! lower {($v:expr) => {$v.wrapping_sub(0x8000)};}

        let [p0, p1] = [self.cursor, self.vector];
        let [p0x, p0y] = [ raise!(p0.x), raise!(p0.y) ];
        let [p1x, p1y] = [ raise!(p1.x), raise!(p1.y) ];
        let [x0, y0] = [ min(p0x, p1x), min(p0y, p1y) ];
        let [x1, y1] = [ max(p0x, p1x), max(p0y, p1y) ];
        let right  = self.dimensions.width.saturating_sub(1);
        let bottom = self.dimensions.height.saturating_sub(1);
        if x0 > raise!(right) || y0 > raise!(bottom) || x1 < 0x8000 || y1 < 0x8000 {
            None
        } else {
            Some([
                if x0 < 0x8000 { 0 } else { min(lower!(x0), right)  } as usize,
                if y0 < 0x8000 { 0 } else { min(lower!(y0), bottom) } as usize,
                if x1 < 0x8000 { 0 } else { min(lower!(x1), right)  } as usize,
                if y1 < 0x8000 { 0 } else { min(lower!(y1), bottom) } as usize,
            ])
        }
    }
}