use crate::*;
use bedrock_core::*;

use phosphor::*;

use std::time::Instant;


pub struct GraphicalDeviceBus {
    pub sys: SystemDevice,
    pub mem: MemoryDevice,
    pub mat: MathDevice,
    pub clk: ClockDevice,
    pub inp: InputDevice,
    pub scr: ScreenDevice,
    pub loc: LocalDevice,
    pub rem: RemoteDevice,
    pub fs1: FileDevice,
    pub fs2: FileDevice,
}

impl GraphicalDeviceBus {
    pub fn new(config: &EmulatorConfig) -> Self {
        Self {
            sys: SystemDevice::new(),
            mem: MemoryDevice::new(),
            mat: MathDevice::new(),
            clk: ClockDevice::new(),
            inp: InputDevice::new(),
            scr: ScreenDevice::new(config),
            loc: LocalDevice::new(config),
            rem: RemoteDevice::new(),
            fs1: FileDevice::new(),
            fs2: FileDevice::new(),
        }
    }

    pub fn graphical(&self) -> bool {
        self.inp.accessed || self.scr.accessed
    }
}

impl DeviceBus for GraphicalDeviceBus {
    fn read(&mut self, port: u8) -> u8 {
        match port & 0xf0 {
            0x00 => self.sys.read(port & 0x0f),
            0x10 => self.mem.read(port & 0x0f),
            0x20 => self.mat.read(port & 0x0f),
            0x30 => self.clk.read(port & 0x0f),
            0x40 => self.inp.read(port & 0x0f),
            0x50 => self.scr.read(port & 0x0f),
            0x80 => self.loc.read(port & 0x0f),
            0x90 => self.rem.read(port & 0x0f),
            0xa0 => self.fs1.read(port & 0x0f),
            0xb0 => self.fs2.read(port & 0x0f),
            _ => 0
        }
    }

    fn write(&mut self, port: u8, value: u8) -> Option<Signal> {
        match port & 0xf0 {
            0x00 => self.sys.write(port & 0x0f, value),
            0x10 => self.mem.write(port & 0x0f, value),
            0x20 => self.mat.write(port & 0x0f, value),
            0x30 => self.clk.write(port & 0x0f, value),
            0x40 => self.inp.write(port & 0x0f, value),
            0x50 => self.scr.write(port & 0x0f, value),
            0x80 => self.loc.write(port & 0x0f, value),
            0x90 => self.rem.write(port & 0x0f, value),
            0xa0 => self.fs1.write(port & 0x0f, value),
            0xb0 => self.fs2.write(port & 0x0f, value),
            _ => None
        }
    }

    fn wake(&mut self) -> bool {
        macro_rules! rouse {
            ($id:expr, $dev:ident) => {
                if self.sys.can_wake($id) && self.$dev.wake() {
                    self.sys.wake_id = $id;
                    self.sys.asleep = false;
                    return true;
                }
            };
        }
        rouse!(0xb, fs2);
        rouse!(0xa, fs1);
        rouse!(0x9, rem);
        rouse!(0x8, loc);
        rouse!(0x5, scr);
        rouse!(0x4, inp);
        rouse!(0x3, clk);
        rouse!(0x2, mat);
        rouse!(0x1, mem);
        rouse!(0x0, sys);
        return false;
    }
}


pub struct GraphicalEmulator {
    pub br: BedrockEmulator<GraphicalDeviceBus>,
    pub debug: DebugState,
    pub dimensions: ScreenDimensions,
    pub fullscreen: bool,
    pub scale: u32,
    pub render_mark: Instant,
    pub debug_palette: Option<[Colour; 16]>,
    pub show_debug_palette: bool,
    pub show_cursor: bool,
}

impl GraphicalEmulator {
    pub fn new(config: &EmulatorConfig, debug: bool) -> Self {
        let devices = GraphicalDeviceBus::new(config);
        Self {
            br: BedrockEmulator::new(devices),
            debug: DebugState::new(debug, config.symbols_path.as_ref()),
            dimensions: config.dimensions,
            fullscreen: config.fullscreen,
            scale: config.scale,
            render_mark: Instant::now(),
            debug_palette: config.debug_palette,
            show_debug_palette: config.debug_palette.is_some(),
            show_cursor: config.show_cursor,
        }
    }

    pub fn load_program(&mut self, bytecode: &[u8]) {
        self.br.core.mem.load_program(bytecode);
    }

    pub fn run(&mut self) -> EmulatorSignal {
        loop {
            match self.br.evaluate(BATCH_SIZE, self.debug.enabled) {
                Some(Signal::Fork) => {
                    self.br.core.mem.pc = 0;
                    self.br.core.wst.sp = 0;
                    self.br.core.rst.sp = 0;
                }
                Some(Signal::Sleep) => loop {
                    if self.br.dev.graphical() {
                        return EmulatorSignal::Promote;
                    }
                    if self.br.dev.wake() { break; }
                    std::thread::sleep(MIN_TICK_DURATION);
                }
                Some(Signal::Halt) => {
                    self.debug.print("Program halted, exiting.");
                    self.debug.debug_summary(&self.br.core);
                    return EmulatorSignal::Halt;
                }
                Some(Signal::Debug1) => {
                    self.debug.debug_summary(&self.br.core);
                }
                _ => (),
            }

            if self.br.dev.graphical() {
                return EmulatorSignal::Promote;
            }
        }
    }

    pub fn size_bounds(&self) -> SizeBounds {
        macro_rules! to_u32 {
            ($opt:expr) => {
                match $opt {
                    Some(a) => Some(u32::from(a)),
                    None => None,
                }
            };
        }
        match self.fullscreen {
            true => SizeBounds {
                min_width: None,
                max_width: None,
                min_height: None,
                max_height: None,
            },
            false => SizeBounds {
                min_width: to_u32!(self.br.dev.scr.fixed_width),
                max_width: to_u32!(self.br.dev.scr.fixed_width),
                min_height: to_u32!(self.br.dev.scr.fixed_height),
                max_height: to_u32!(self.br.dev.scr.fixed_height),
            },

        }
    }

    pub fn dimensions(&self) -> Dimensions {
        Dimensions {
            width: u32::from(self.br.dev.scr.dimensions.width),
            height: u32::from(self.br.dev.scr.dimensions.height),
        }
    }
}


impl WindowProgram for GraphicalEmulator {
    fn handle_event(&mut self, event: Event, r: &mut EventWriter<Request>) {
        match event {
            Event::CloseRequest => r.write(Request::CloseWindow),
            Event::CursorEnter => self.br.dev.inp.on_cursor_enter(),
            Event::CursorExit => self.br.dev.inp.on_cursor_exit(),
            Event::CursorMove(p) => self.br.dev.inp.on_cursor_move(p),
            Event::Resize(d) => self.br.dev.scr.resize(d),
            Event::CharacterInput(c) => self.br.dev.inp.on_character(c),
            Event::ModifierChange(m) => self.br.dev.inp.on_modifier(m),
            Event::MouseButton { button, action } =>
                self.br.dev.inp.on_mouse_button(button, action),
            Event::FocusChange(_) => (),
            Event::Initialise => (),
            Event::ScrollLines { axis, distance } => match axis {
                Axis::Horizontal => self.br.dev.inp.on_horizontal_scroll(distance),
                Axis::Vertical   => self.br.dev.inp.on_horizontal_scroll(distance),
            }
            Event::ScrollPixels { axis, distance } => match axis {
                Axis::Horizontal => self.br.dev.inp.on_horizontal_scroll(distance / 20.0),
                Axis::Vertical   => self.br.dev.inp.on_horizontal_scroll(distance / 20.0),
            }
            Event::FileDrop(_path) => todo!("FileDrop"),

            Event::Close => (),
            Event::KeyboardInput { key, action } => {
                self.br.dev.inp.on_keypress(key, action);
                if action == Action::Pressed {
                    match key {
                        KeyCode::F2 => {
                            self.show_debug_palette = !self.show_debug_palette;
                            r.write(Request::Redraw);
                        },
                        KeyCode::F5 => {
                            self.scale = std::cmp::max(1, self.scale.saturating_sub(1));
                            r.write(Request::SetPixelScale(self.scale));
                        },
                        KeyCode::F6 => {
                            self.scale = self.scale.saturating_add(1);
                            r.write(Request::SetPixelScale(self.scale));
                        },
                        KeyCode::F11 => {
                            self.fullscreen = !self.fullscreen;
                            r.write(Request::SetFullscreen(self.fullscreen));
                        },
                        _ => (),
                    }
                }
            }
        }
    }

    fn process(&mut self, requests: &mut EventWriter<Request>) {
        self.br.dev.loc.flush();

        if self.br.dev.sys.asleep {
            // Stay asleep if there are no pending wake events.
            if !self.br.dev.wake() {
                if self.br.dev.scr.dirty {
                    requests.write(Request::Redraw);
                }
                std::thread::sleep(MIN_TICK_DURATION);
                return;
            }

            // Wait for the current frame to be rendered.
            if self.br.dev.scr.dirty {
                if self.render_mark.elapsed() > MIN_FRAME_DURATION {
                    requests.write(Request::Redraw);
                }
                std::thread::sleep(MIN_TICK_DURATION);
                return;
            }
        }

        // Run the processor for the remainder of the frame.
        let frame_end = Instant::now() + MIN_TICK_DURATION;
        while Instant::now() < frame_end {
            match self.br.evaluate(BATCH_SIZE, self.debug.enabled) {
                Some(Signal::Fork) => {
                    todo!("Fork")
                }
                Some(Signal::Sleep) => {
                    self.br.dev.sys.asleep = true;
                    break;
                }
                Some(Signal::Halt) => {
                    self.debug.print("Program halted, exiting.");
                    self.debug.debug_summary(&self.br.core);
                    requests.write(Request::CloseWindow);
                    break;
                }
                Some(Signal::Debug1) => {
                    self.debug.debug_summary(&self.br.core);
                }
                _ => (),
            }
        }

        if std::mem::take(&mut self.br.dev.scr.dirty_dimensions) {
            requests.write(Request::SetSizeBounds(self.size_bounds()));
        }

        if self.br.dev.scr.dirty {
            let elapsed = self.render_mark.elapsed();
            if self.br.dev.sys.asleep && elapsed > MIN_FRAME_DURATION {
                requests.write(Request::Redraw);
            } else if elapsed > MAX_FRAME_DURATION {
                requests.write(Request::Redraw);
            }
        } else {
            self.render_mark = Instant::now();
        }
    }

    fn render(&mut self, buffer: &mut Buffer, _full: bool) {
        let screen = &mut self.br.dev.scr;

        // Generate table for calculating pixel colours from layer values.
        // A given screen pixel will be rendered as the colour given by
        // table[fg][bg], where fg and bg are the corresponding layer values.
        let mut table = [Colour::BLACK; 256];
        let palette = match self.debug_palette {
            Some(debug_palette) => match self.show_debug_palette {
                true => debug_palette,
                false => screen.palette,
            }
            None => screen.palette,
        };
        table[0..16].clone_from_slice(&palette);
        for i in 1..16 { table[i*16..(i+1)*16].fill(palette[i]); }

        // Copy pixels to buffer when it is the same size as the screen.
        if buffer.area_usize() == screen.area_usize() {
            for (i, colour) in buffer.iter_mut().enumerate() {
                let fg = screen.fg[i];
                let bg = screen.bg[i];
                let index = unsafe { fg.unchecked_shl(4) | bg };
                *colour = table[index as usize];

                // TODO: merge fg and bg: *colour = table[screen.bg[i] as usize];
            }
        // Copy pixels to buffer when it is a different size to the screen.
        } else {
            let buffer_width  = buffer.width()  as usize;
            let buffer_height = buffer.height() as usize;
            let screen_width  = screen.width()  as usize;
            let screen_height = screen.height() as usize;
            let width  = std::cmp::min(buffer_width,  screen_width );
            let height = std::cmp::min(buffer_height, screen_height);

            let mut bi = 0;
            let mut si = 0;
            for _ in 0..height {
                let bi_next = bi + buffer_width;
                let si_next = si + screen_width;
                for _ in 0..width {
                    let fg = screen.fg[si];
                    let bg = screen.bg[si];
                    let index = unsafe { fg.unchecked_shl(4) | bg };
                    buffer[bi] = table[index as usize];
                    bi += 1;
                    si += 1;
                }
                // Fill remaining right edge with background colour.
                buffer[bi..bi_next].fill(table[0]);
                bi = bi_next;
                si = si_next;
            }
            // Fill remaining bottom edge with background colour.
            buffer[bi..].fill(table[0]);
        }

        screen.dirty = false;
        self.render_mark = Instant::now();
    }
}