use crate::*;


pub struct GraphicalEmulator {
    pub br: BedrockEmulator<GraphicalDeviceBus>,
    pub debug: DebugState,

    pub fullscreen: bool,
    pub scale: u32,
    pub render_mark: Instant,
    pub show_override_palette: bool,

    pub config: EmulatorConfig,
}

impl GraphicalEmulator {
    pub fn new(config: EmulatorConfig, debug: bool) -> Self {
        Self {
            br: BedrockEmulator::new(GraphicalDeviceBus::new(&config)),
            debug: DebugState::new(debug, config.symbols_path.as_ref()),

            fullscreen: config.fullscreen,
            scale: config.zoom.into(),
            show_override_palette: config.override_palette.is_some(),
            render_mark: Instant::now(),

            config,
        }
    }

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

    pub fn run(self, mut phosphor: Phosphor) {
        let cursor = match self.config.show_cursor {
            true => Some(CursorIcon::Default),
            false => None,
        };

        let program_name = match &self.config.metadata {
            Some(metadata) => match &metadata.name {
                Some(name) => name.to_string(),
                None => String::from("Bedrock"),
            }
            None => String::from("Bedrock"),
        };
        let window = WindowBuilder {
            dimensions: Some(self.dimensions()),
            size_bounds: Some(self.size_bounds()),
            fullscreen: self.fullscreen,
            scale: self.scale,
            title: Some(program_name),
            cursor,
            icon: parse_icon_from_config(&self.config),
            program: Box::new(self),
        };

        phosphor.create_window(window);
        phosphor.run().unwrap();
    }

    pub fn size_bounds(&self) -> SizeBounds {
        match self.fullscreen {
            true => SizeBounds {
                min_width:  None,
                max_width:  None,
                min_height: None,
                max_height: None,
            },
            false => SizeBounds {
                min_width:  self.br.dev.screen.fixed_width.map(u32::from),
                max_width:  self.br.dev.screen.fixed_width.map(u32::from),
                min_height: self.br.dev.screen.fixed_height.map(u32::from),
                max_height: self.br.dev.screen.fixed_height.map(u32::from),
            },

        }
    }

    pub fn dimensions(&self) -> Dimensions {
        Dimensions {
            width: self.br.dev.screen.dimensions.width.into(),
            height: self.br.dev.screen.dimensions.height.into(),
        }
    }
}


pub struct GraphicalDeviceBus {
    pub system: SystemDevice,
    pub memory: MemoryDevice,
    pub math:   MathDevice,
    pub clock:  ClockDevice,
    pub input:  InputDevice,
    pub screen: ScreenDevice,
    pub stream: StreamDevice,
    pub file:   FileDevice,
}

impl GraphicalDeviceBus {
    pub fn new(config: &EmulatorConfig) -> Self {
        Self {
            system: SystemDevice::new(0b1111_1100_1010_0000),
            memory: MemoryDevice::new(),
            math:   MathDevice::new(),
            clock:  ClockDevice::new(),
            input:  InputDevice::new(),
            screen: ScreenDevice::new(&config),
            stream: StreamDevice::new(&config),
            file:   FileDevice::new(),
        }
    }
}


impl DeviceBus for GraphicalDeviceBus {
    fn read(&mut self, port: u8) -> u8 {
        match port & 0xf0 {
            0x00 => self.system.read(port & 0x0f),
            0x10 => self.memory.read(port & 0x0f),
            0x20 => self.math  .read(port & 0x0f),
            0x30 => self.clock .read(port & 0x0f),
            0x40 => self.input .read(port & 0x0f),
            0x50 => self.screen.read(port & 0x0f),
            0x80 => self.stream.read(port & 0x0f),
            0xa0 => self.file  .read(port & 0x0f),
            _ => 0
        }
    }

    fn write(&mut self, port: u8, value: u8) -> Option<Signal> {
        match port & 0xf0 {
            0x00 => self.system.write(port & 0x0f, value),
            0x10 => self.memory.write(port & 0x0f, value),
            0x20 => self.math  .write(port & 0x0f, value),
            0x30 => self.clock .write(port & 0x0f, value),
            0x40 => self.input .write(port & 0x0f, value),
            0x50 => self.screen.write(port & 0x0f, value),
            0x80 => self.stream.write(port & 0x0f, value),
            0xa0 => self.file  .write(port & 0x0f, value),
            _ => None
        }
    }

    fn wake(&mut self) -> bool {
        macro_rules! rouse {
            ($id:expr, $dev:ident) => {
                let is_eligible = test_bit!(self.system.wakers, 0x8000 >> $id);
                if is_eligible && self.$dev.wake() {
                    self.system.waker = $id;
                    self.system.asleep = false;
                    return true;
                }
            };
        }
        rouse!(0xa, file  );
        rouse!(0x8, stream);
        rouse!(0x5, screen);
        rouse!(0x4, input );
        rouse!(0x3, clock );
        return false;
    }
}


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.input.on_cursor_enter(),
            Event::CursorExit => self.br.dev.input.on_cursor_exit(),
            Event::CursorMove(p) => self.br.dev.input.on_cursor_move(p),
            Event::Resize(d) => self.br.dev.screen.resize(d),
            Event::CharacterInput(c) => self.br.dev.input.on_character(c),
            Event::ModifierChange(m) => self.br.dev.input.on_modifier(m),
            Event::MouseButton { button, action } =>
                self.br.dev.input.on_mouse_button(button, action),
            Event::FocusChange(_) => (),
            Event::Initialise => (),
            Event::ScrollLines { axis, distance } => match axis {
                Axis::Horizontal => self.br.dev.input.on_horizontal_scroll(distance),
                Axis::Vertical   => self.br.dev.input.on_vertical_scroll(distance),
            }
            Event::ScrollPixels { axis, distance } => match axis {
                Axis::Horizontal => self.br.dev.input.on_horizontal_scroll(distance / 20.0),
                Axis::Vertical   => self.br.dev.input.on_vertical_scroll(distance / 20.0),
            }
            Event::FileDrop(_path) => todo!("FileDrop"),

            Event::Close => (),
            Event::KeyboardInput { key, action } => {
                self.br.dev.input.on_keypress(key, action);
                if action == Action::Pressed {
                    match key {
                        KeyCode::F2 => {
                            self.show_override_palette = !self.show_override_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.stream.flush();

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

            // Wait for the current frame to be rendered.
            if self.br.dev.screen.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.system.asleep = true;
                    break;
                }
                Some(Signal::Halt) => {
                    self.br.dev.stream.flush();
                    log::info!("Program halted, exiting.");
                    self.debug.debug_summary(&self.br.core);
                    requests.write(Request::CloseWindow);
                    break;
                }
                Some(Signal::Debug(Debug::Debug1)) => {
                    self.debug.debug_summary(&self.br.core);
                }
                _ => (),
            }
        }

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

        if self.br.dev.screen.dirty {
            let elapsed = self.render_mark.elapsed();
            if self.br.dev.system.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.screen;

        // 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.config.override_palette {
            Some(override_palette) => match self.show_override_palette {
                true => override_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();
    }
}


fn parse_icon_from_config(config: &EmulatorConfig) -> Option<Icon> {
    let metadata = config.metadata.as_ref()?;
    let bytes = metadata.small_icon.as_ref()?;
    let bg = metadata.bg_colour.unwrap_or(Colour::BLACK);
    let fg = metadata.bg_colour.unwrap_or(Colour::WHITE);
    let rgba = sprite_icon_to_rgb(bytes, 3, bg, fg);
    match Icon::from_rgba(rgba, 24, 24) {
        Ok(icon) => Some(icon),
        Err(err) => unreachable!("Error while parsing small icon data: {err}"),
    }
}

fn sprite_icon_to_rgb(bytes: &[u8], size: usize, bg: Colour, fg: Colour) -> Vec<u8> {
    let sprites: Vec<&[u8]> = bytes.chunks_exact(8).collect();
    let mut rgba = Vec::new();
    for sprite_row in 0..size {
        for pixel_row in 0..8 {
            for sprite_column in 0..size {
                let sprite = &sprites[sprite_column + (sprite_row * size)];
                let row = &sprite[pixel_row];
                for bit in 0..8 {
                    let state = row & (0x80 >> bit);
                    let colour = match state != 0 {
                        true => fg,
                        false => bg,
                    };
                    rgba.extend_from_slice(&colour.as_rgba_array());
                }
            }
        }
    }
    return rgba;
}