use std::{cell::RefCell, num::NonZeroU32, ops::Deref, rc::Rc};

use anyhow::{Context as _, Result};
use glutin::{
    context::{NotCurrentContext, PossiblyCurrentContext},
    prelude::{GlDisplay as _, NotCurrentGlContext},
    surface::{GlSurface as _, Surface, WindowSurface},
};
use vao::VaoBindGuard;
use vek::{Extent2, Rect, Vec2};

use self::{shader::ProgramGuard, wrapper::GlowContext};

pub mod buffer_object;
pub mod framebuffer;
pub mod shader;
#[cfg_attr(test, allow(dead_code))]
pub mod texture;
pub mod vao;
#[cfg_attr(test, allow(elided_named_lifetimes))]
pub mod wrapper;

#[derive(Debug)]
pub struct GlContext {
    gl: GlowContext,
    capacities: Capabilities,
    info: RefCell<GlContextInfo>,
    surface: Option<Surface<WindowSurface>>,
    #[cfg(not(test))]
    context: PossiblyCurrentContext,
}

pub struct FutureGlThreadContext {
    display: glutin::display::Display,
    surface: Option<Surface<WindowSurface>>,
    context: NotCurrentContext,
}

impl FutureGlThreadContext {
    pub fn new(
        surface: Option<Surface<WindowSurface>>,
        context: NotCurrentContext,
        display: glutin::display::Display,
    ) -> Self {
        Self {
            display,
            surface,
            context,
        }
    }

    pub fn activate(self) -> Result<Rc<GlContext>> {
        let context = match &self.surface {
            Some(surface) => {
                let current = self
                    .context
                    .make_current(surface)
                    .context("Cannot make context current")?;
                surface
                    .set_swap_interval(
                        &current,
                        glutin::surface::SwapInterval::Wait(
                            NonZeroU32::new(1).expect("should never happen"),
                        ),
                    )
                    .context("Cannot configure swap for GL buffers")?;
                current
            }
            None => match self.context {
                NotCurrentContext::Egl(not_current_context) => PossiblyCurrentContext::Egl(
                    not_current_context
                        .make_current_surfaceless()
                        .context("Cannot make context current")?,
                ),
            },
        };

        let gl = unsafe {
            glow::Context::from_loader_function_cstr(|s| self.display.get_proc_address(s))
        };

        GlContext::new(self.surface, context, gl.into())
    }

    pub fn get_context(&self) -> &NotCurrentContext {
        &self.context
    }
}

impl Deref for GlContext {
    type Target = GlowContext;

    fn deref(&self) -> &Self::Target {
        &self.gl
    }
}

#[derive(Debug)]
pub struct GlContextInfo {
    viewport: Rect<i32, i32>,
    bound_shader: Option<NonZeroU32>,
    blend_mode: Option<BlendMode>,
}

#[derive(Debug)]
pub struct Capabilities {
    pub max_texture_size: u32,
}

#[derive(Default)]
pub struct DrawParameters {
    pub blend: Option<BlendMode>,
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct BlendMode {
    pub alpha: BlendEquation,
    pub color: BlendEquation,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[allow(dead_code)]
pub enum BlendEquation {
    Add(BlendFunction),
    Subtract(BlendFunction),
    ReverseSubtract(BlendFunction),
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[allow(dead_code)]
pub struct BlendFunction {
    pub src: BlendFactor,
    pub dst: BlendFactor,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[allow(dead_code)]
pub enum BlendFactor {
    Zero,
    One,
    SrcColor,
    OneMinusSrcColor,
    DstColor,
    OneMinusDstColor,
    SrcAlpha,
    OneMinusSrcAlpha,
    DstAlpha,
    OneMinusDstAlpha,
    ConstantColor,
    OneMinusConstantColor,
    ConstantAlpha,
    OneMinusConstantAlpha,
    SrcAlphaSaturate,
}
impl BlendMode {
    pub fn alpha() -> Self {
        Self {
            alpha: BlendEquation::Add(BlendFunction {
                src: BlendFactor::SrcAlpha,
                dst: BlendFactor::OneMinusSrcAlpha,
            }),
            color: BlendEquation::Add(BlendFunction {
                src: BlendFactor::SrcAlpha,
                dst: BlendFactor::OneMinusSrcAlpha,
            }),
        }
    }
}
impl BlendEquation {
    pub fn to_gl(self) -> u32 {
        match self {
            BlendEquation::Add(_) => glow::FUNC_ADD,
            BlendEquation::Subtract(_) => glow::FUNC_SUBTRACT,
            BlendEquation::ReverseSubtract(_) => glow::FUNC_REVERSE_SUBTRACT,
        }
    }

    pub fn get_function(&self) -> &BlendFunction {
        match self {
            BlendEquation::Add(f) => f,
            BlendEquation::Subtract(f) => f,
            BlendEquation::ReverseSubtract(f) => f,
        }
    }
}
impl BlendFactor {
    pub fn to_gl(self) -> u32 {
        match self {
            BlendFactor::Zero => glow::ZERO,
            BlendFactor::One => glow::ONE,
            BlendFactor::SrcColor => glow::SRC_COLOR,
            BlendFactor::OneMinusSrcColor => glow::ONE_MINUS_SRC_COLOR,
            BlendFactor::DstColor => glow::DST_COLOR,
            BlendFactor::ConstantAlpha => glow::CONSTANT_ALPHA,
            BlendFactor::ConstantColor => glow::CONSTANT_COLOR,
            BlendFactor::OneMinusConstantAlpha => glow::ONE_MINUS_CONSTANT_ALPHA,
            BlendFactor::OneMinusConstantColor => glow::ONE_MINUS_CONSTANT_COLOR,
            BlendFactor::OneMinusDstAlpha => glow::ONE_MINUS_DST_ALPHA,
            BlendFactor::OneMinusDstColor => glow::ONE_MINUS_DST_COLOR,
            BlendFactor::OneMinusSrcAlpha => glow::ONE_MINUS_SRC_ALPHA,
            BlendFactor::SrcAlpha => glow::SRC_ALPHA,
            BlendFactor::DstAlpha => glow::DST_ALPHA,
            BlendFactor::SrcAlphaSaturate => glow::SRC_ALPHA_SATURATE,
        }
    }
}

impl GlContext {
    #[cfg(test)]
    pub fn mocked(gl: GlowContext) -> Self {
        Self {
            capacities: Capabilities {
                max_texture_size: 2048,
            },
            info: RefCell::new(GlContextInfo {
                viewport: Rect::new(0, 0, 800, 600),
                bound_shader: None,
                blend_mode: None,
            }),
            gl,
            surface: None,
        }
    }

    #[cfg_attr(test, allow(unused_variables))]
    fn new(
        surface: Option<Surface<WindowSurface>>,
        context: PossiblyCurrentContext,
        gl: GlowContext,
    ) -> Result<Rc<Self>> {
        let dimensions = if let Some(surface) = &surface {
            let width = surface.width().context("cannot get surface width")?;
            let height = surface.height().context("cannot get surface height")?;
            Extent2::new(width as i32, height as i32)
        } else {
            Extent2::zero()
        };
        let viewport = Rect::from((Vec2::zero(), dimensions));
        Ok(Rc::new(Self {
            capacities: Capabilities {
                max_texture_size: unsafe { gl.get_parameter_i32(glow::MAX_TEXTURE_SIZE) } as u32,
            },
            info: RefCell::new(GlContextInfo {
                viewport,
                bound_shader: None,
                blend_mode: None,
            }),
            gl,
            surface,
            #[cfg(not(test))]
            context,
        }))
    }

    pub fn draw<T>(
        &self,
        _vao: &VaoBindGuard<T>,
        _program: &ProgramGuard,
        count: i32,
        offset: i32,
        draw_parameters: &DrawParameters,
    ) {
        unsafe {
            if self.info.borrow().blend_mode != draw_parameters.blend {
                if let Some(blend) = &draw_parameters.blend {
                    self.gl.enable(glow::BLEND);
                    self.gl
                        .blend_equation_separate(blend.color.to_gl(), blend.alpha.to_gl());
                    self.gl.blend_func_separate(
                        blend.color.get_function().src.to_gl(),
                        blend.color.get_function().dst.to_gl(),
                        blend.alpha.get_function().src.to_gl(),
                        blend.alpha.get_function().dst.to_gl(),
                    );
                } else {
                    self.gl.disable(glow::BLEND);
                }
            }
            self.gl
                .draw_elements(glow::TRIANGLES, count, glow::UNSIGNED_INT, offset);
        }
    }

    pub fn clear(&self) {
        unsafe {
            self.gl.clear(glow::COLOR_BUFFER_BIT);
        }
    }

    pub fn current_viewport(&self) -> Rect<i32, i32> {
        self.info.borrow().viewport
    }

    pub fn set_viewport(&self, viewport: Rect<i32, i32>) {
        unsafe {
            self.gl
                .viewport(viewport.x, viewport.y, viewport.w, viewport.h)
        };
        self.info.borrow_mut().viewport = viewport;
    }

    pub fn capabilities(&self) -> &Capabilities {
        &self.capacities
    }

    pub fn swap_buffers(&self) -> Result<()> {
        #[cfg(not(test))]
        if let Some(surface) = &self.surface {
            surface
                .swap_buffers(&self.context)
                .context("Cannot swap buffers")
        } else {
            anyhow::bail!("Cannot swap buffers on offscreen surface")
        }
        #[cfg(test)]
        Ok(())
    }

    pub fn is_background(&self) -> bool {
        self.surface.is_none()
    }

    pub fn wait(&self) {
        unsafe {
            self.gl.finish();
        }
    }

    /// Update the bound shader and return the previous bound shader.
    fn set_bound_shader(&self, shader: NonZeroU32) -> Option<NonZeroU32> {
        self.info.borrow_mut().bound_shader.replace(shader)
    }
}
