use crate::console::Console;
use crate::image::Image;
use crate::Tileset;
use glam::IVec2;
use rand::Rng;
use std::cmp::{max, min};
use std::num::NonZeroU32;
use std::time::{Duration, Instant};
use winit::dpi::LogicalSize;
use winit::event::ElementState::Pressed;
use winit::event::VirtualKeyCode::{Escape, F12};
use winit::event::{
    ElementState, Event as WinitEvent, MouseButton as WinitMouseButton, WindowEvent,
};
use winit::event_loop::{ControlFlow, EventLoop};
use winit::platform::run_return::EventLoopExtRunReturn;
use winit::window::WindowBuilder;

pub enum MouseButton {
    Left,
    Right,
    Middle,
    WheelUp,
    WheelDown,
    Forward,
    Back,
}

pub enum Event {
    MouseClick(MouseButton, IVec2),
}

pub trait Game {
    fn new(window_state: &mut WindowState) -> Self;
    fn update(&mut self, window_state: &mut WindowState);
    fn on_event(&mut self, window_state: &mut WindowState, event: Event);
    fn draw(&self, target: &mut Image);
}

pub struct WindowState {
    palette: [u32; 256],
    mouse_pos: IVec2,
    console: Console,
}

impl WindowState {
    pub fn log(&mut self, msg: &str) {
        self.console.add(msg);
    }
}

impl WindowState {
    fn new(console_size: IVec2) -> Self {
        WindowState {
            palette: [0u32; 256],
            mouse_pos: IVec2::ZERO,
            console: Console::new(console_size.y, console_size.x),
        }
    }

    pub fn set_palette(&mut self, index: u8, r: u8, g: u8, b: u8) {
        self.palette[index as usize] =
            (r as u32) | ((g as u32) << 8) | ((b as u32) << 16) | 0xff000000;
    }

    pub fn scramble_palette(&mut self) {
        let mut rng = rand::thread_rng();
        for i in 0..=255 {
            self.set_palette(i, rng.gen(), rng.gen(), rng.gen());
        }
    }

    pub fn mouse_pos(&self) -> IVec2 {
        self.mouse_pos
    }
}

pub fn run<T: Game + 'static>(width: i32, height: i32, target_fps: u32) {
    let mut event_loop = EventLoop::new();
    let window = {
        let size = LogicalSize::new(width as f64, height as f64);
        WindowBuilder::new()
            .with_title("Skunk 2D")
            .with_inner_size(size)
            .with_min_inner_size(size)
            .with_decorations(false)
            .with_maximized(true)
            .build(&event_loop)
            .unwrap()
    };

    //todo: replace Pixels with custom thingie (startup time slow because wgpu?)
    let context = unsafe { softbuffer::Context::new(&window) }.unwrap();
    let mut surface = unsafe { softbuffer::Surface::new(&context, &window) }.unwrap();
    let mut screen = Image::new(IVec2 {
        x: width,
        y: height,
    });
    let internal_font = Tileset::load_data(include_bytes!("ega-8x14.gif"), IVec2 { x: 16, y: 16 });
    let mut window_state = WindowState::new(IVec2 {
        x: width / internal_font.tile_size().x,
        y: height / internal_font.tile_size().y / 3,
    });
    let mut game = T::new(&mut window_state);
    window_state.console.add("Initialising Skunk2d");

    let mut display_console = true;

    let mut frames_since_last_second = 0;
    let mut last_second = Instant::now();

    let mut last_frame = last_second;
    let target_frame_duration = Duration::from_secs(1) / target_fps;

    let mut scale = 1usize;
    let mut off_x = 0usize;
    let mut off_y = 0usize;

    event_loop.run_return(move |event, _, control_flow| {
        let scale = &mut scale;
        let off_x = &mut off_x;
        let off_y = &mut off_y;
        if Instant::now() - last_second > Duration::from_secs(1) {
            println!("fps: {}", frames_since_last_second);
            frames_since_last_second = 0;
            last_second += Duration::from_secs(1);
        }
        match event {
            WinitEvent::NewEvents(_) => {}
            WinitEvent::WindowEvent {
                event: window_event,
                ..
            } => {
                match window_event {
                    WindowEvent::Resized(_) => {}
                    WindowEvent::Moved(_) => {}
                    WindowEvent::CloseRequested => {}
                    WindowEvent::Destroyed => {}
                    WindowEvent::DroppedFile(_) => {}
                    WindowEvent::HoveredFile(_) => {}
                    WindowEvent::HoveredFileCancelled => {}
                    WindowEvent::ReceivedCharacter(_) => {}
                    WindowEvent::Focused(_) => {}
                    WindowEvent::KeyboardInput { input, .. } => {
                        //todo: actually do input handling
                        if let Some(k) = input.virtual_keycode {
                            if k == Escape {
                                *control_flow = ControlFlow::Exit;
                            }
                            if k == F12 && input.state == Pressed {
                                display_console = !display_console;
                            }
                        };
                    }
                    WindowEvent::ModifiersChanged(_) => {}
                    WindowEvent::Ime(_) => {}
                    WindowEvent::CursorMoved { position, .. } => {
                        let x = (position.x as i32 - *off_x as i32) / *scale as i32;
                        let y = (position.y as i32 - *off_y as i32) / *scale as i32;
                        window_state.mouse_pos = IVec2 {
                            x: min(width - 1, max(0, x)),
                            y: min(height - 1, max(0, y)),
                        }
                    }
                    WindowEvent::CursorEntered { .. } => {}
                    WindowEvent::CursorLeft { .. } => {}
                    WindowEvent::MouseWheel { .. } => {}
                    WindowEvent::MouseInput { button, state, .. } => {
                        let b = match button {
                            WinitMouseButton::Left => MouseButton::Left,
                            WinitMouseButton::Right => MouseButton::Right,
                            WinitMouseButton::Middle => MouseButton::Middle,
                            //todo: handle other mousebuttons
                            WinitMouseButton::Other(_) => MouseButton::Back,
                        };
                        match state {
                            ElementState::Pressed => {
                                let e = Event::MouseClick(b, window_state.mouse_pos);
                                game.on_event(&mut window_state, e);
                            }
                            ElementState::Released => {}
                        }
                    }
                    WindowEvent::TouchpadMagnify { .. } => {}
                    WindowEvent::SmartMagnify { .. } => {}
                    WindowEvent::TouchpadRotate { .. } => {}
                    WindowEvent::TouchpadPressure { .. } => {}
                    WindowEvent::AxisMotion { .. } => {}
                    WindowEvent::Touch(_) => {}
                    WindowEvent::ScaleFactorChanged { .. } => {}
                    WindowEvent::ThemeChanged(_) => {}
                    WindowEvent::Occluded(_) => {}
                }
            }
            WinitEvent::DeviceEvent { .. } => {}
            WinitEvent::UserEvent(_) => {}
            WinitEvent::Suspended => {}
            WinitEvent::Resumed => {}
            WinitEvent::MainEventsCleared => {
                if Instant::now() - last_frame >= target_frame_duration {
                    game.update(&mut window_state);
                    game.draw(&mut screen);
                    if display_console {
                        window_state.console.draw_to_image(
                            &mut screen,
                            IVec2::ZERO,
                            &internal_font,
                        );
                    }

                    let size = window.inner_size();
                    surface
                        .resize(
                            NonZeroU32::new(size.width).unwrap(),
                            NonZeroU32::new(size.height).unwrap(),
                        )
                        .unwrap();

                    *scale = min(size.width / width as u32, size.height / height as u32) as usize;
                    *off_x = (size.width as usize - width as usize * *scale) / 2;
                    *off_y = (size.height as usize - height as usize * *scale) / 2;

                    let mut buf = surface.buffer_mut().unwrap();
                    //for y in 0..height as usize {

                    {
                        let width = width as usize;
                        let window_width = size.width as usize;
                        let scale = *scale;
                        let chunk_size = scale * window_width;
                        let screen_data = screen.data();
                        buf.chunks_exact_mut(chunk_size)
                            .enumerate()
                            .for_each(|(y, chunk)| {
                                for x in 0..width {
                                    let p =
                                        window_state.palette[screen_data[x + y * width] as usize];
                                    //let p = 0;
                                    for scaley in 0..scale {
                                        for scalex in 0..scale {
                                            let sx = x * scale + scalex;
                                            chunk[sx + scaley * window_width] = p;
                                        }
                                    }
                                }
                            });
                    }

                    buf.present().unwrap();
                    frames_since_last_second += 1;
                    last_frame += target_frame_duration;
                } else {
                    std::thread::sleep(Duration::from_millis(1));
                }
            }
            WinitEvent::RedrawRequested(_) => {}
            WinitEvent::RedrawEventsCleared => {}
            WinitEvent::LoopDestroyed => {}
        }
    });
}