use glutin::{
    dpi::{PhysicalPosition, PhysicalSize},
    event_loop::EventLoop,
    monitor::VideoMode,
    window::{Fullscreen, Window as GlutinWindow, WindowBuilder},
    Api, ContextBuilder, ContextWrapper, GlProfile, GlRequest, PossiblyCurrent,
};
use log::{error, info};

use crate::{resources::Config, Error};

pub struct Window {
    context: ContextWrapper<PossiblyCurrent, GlutinWindow>,
}

impl Window {
    pub fn new<T>(event_loop: &EventLoop<T>, config: &Config) -> Result<Self, Error> {
        let monitor = event_loop
            .primary_monitor()
            .or_else(|| event_loop.available_monitors().next());

        let (fullscreen, border) = match (config.video.windowed, config.video.fullscreen) {
            (false, true) => {
                let mut video_mode: Option<VideoMode> = None;

                if let Some(monitor) = monitor {
                    for mode in monitor.video_modes() {
                        video_mode = Some(match video_mode {
                            Some(current) => {
                                if mode.size().width <= config.video.resolution.0
                                    && mode.size().height <= config.video.resolution.1
                                    && mode.size().width >= current.size().width
                                    && mode.size().height >= current.size().height
                                    && mode.bit_depth() >= current.bit_depth()
                                    && mode.refresh_rate() >= current.refresh_rate()
                                {
                                    mode
                                } else {
                                    current
                                }
                            }
                            None => mode,
                        });
                    }
                }

                match video_mode {
                    Some(video_mode) => (Some(Fullscreen::Exclusive(video_mode)), false),
                    None => {
                        error!("Unable to find video mode for exclusive fullscreen!");

                        (Some(Fullscreen::Borderless(None)), false)
                    }
                }
            }
            (true, true) => (Some(Fullscreen::Borderless(None)), false),
            (false, false) => (None, false),
            (true, false) => (None, true),
        };

        let mut multisampling = [16u16, 8, 4, 2, 1, 0].iter();
        let context = loop {
            let multisampling = match multisampling.next() {
                Some(multisampling) => *multisampling,
                None => return Err(Error::CreateContext),
            };

            if config.video.multisampling < multisampling {
                continue;
            }

            info!("Create OpenGL context (multisampling={})", multisampling);

            let window_builder = WindowBuilder::new()
                .with_title("space-crush")
                .with_visible(true)
                .with_decorations(border)
                .with_fullscreen(fullscreen.clone())
                .with_inner_size::<PhysicalSize<u32>>(config.video.resolution.into());
            let ret = ContextBuilder::new()
                .with_double_buffer(Some(true))
                .with_hardware_acceleration(Some(true))
                .with_pixel_format(24, 8)
                .with_multisampling(multisampling)
                .with_vsync(false)
                .with_gl(GlRequest::Specific(Api::OpenGl, (4, 5)))
                .with_gl_profile(GlProfile::Core)
                .build_windowed(window_builder, event_loop);

            if let Ok(context) = ret {
                break context;
            }
        };

        let window = context.window();
        let window_size = window.outer_size();
        let monitor_size = window.current_monitor().unwrap().size();
        window.set_outer_position(PhysicalPosition::new(
            (monitor_size.width - window_size.width) / 2,
            (monitor_size.height - window_size.height) / 2,
        ));

        let context = unsafe { context.make_current().unwrap() };
        gl::load_with(|s| context.get_proc_address(s));

        gl::disable(gl::CULL_FACE);
        gl::disable(gl::DEPTH_TEST);
        gl::clear_color(0.1, 0.1, 0.1, 1.0);
        gl::pixel_store_i(gl::UNPACK_ALIGNMENT, 1);

        Ok(Self { context })
    }

    pub fn swap_buffers(&self) -> Result<(), Error> {
        self.context.swap_buffers()?;

        Ok(())
    }

    pub fn set_mouse_locked(&self, value: bool) -> Result<(), Error> {
        let window = self.context.window();

        window.set_cursor_grab(value)?;

        if !value {
            window.set_cursor_visible(true);
        }

        Ok(())
    }

    pub fn set_mouse_pos(&self, x: f64, y: f64) -> Result<(), Error> {
        let window = self.context.window();

        window.set_cursor_visible(false);
        window.set_cursor_position(PhysicalPosition { x, y })?;

        Ok(())
    }
}
