use crate::renderer::framework::gpu_program::GpuProgramBinding;
use crate::{
    core::{color::Color, math::Rect, scope_profile},
    renderer::framework::{
        error::FrameworkError,
        geometry_buffer::{DrawCallStatistics, GeometryBuffer},
        gpu_program::GpuProgram,
        gpu_texture::{CubeMapFace, GpuTexture, GpuTextureKind},
        state::{ColorMask, PipelineState},
    },
};
use glow::HasContext;
use std::{cell::RefCell, rc::Rc};

#[derive(Copy, Clone, PartialOrd, PartialEq, Hash, Debug)]
pub enum AttachmentKind {
    Color,
    DepthStencil,
    Depth,
}

pub struct Attachment {
    pub kind: AttachmentKind,
    pub texture: Rc<RefCell<GpuTexture>>,
}

pub struct FrameBuffer {
    state: *mut PipelineState,
    fbo: glow::Framebuffer,
    depth_attachment: Option<Attachment>,
    color_attachments: Vec<Attachment>,
}

#[derive(Copy, Clone, PartialOrd, PartialEq, Hash, Debug)]
pub enum CullFace {
    Back,
    Front,
}

impl CullFace {
    pub fn into_gl_value(self) -> u32 {
        match self {
            Self::Front => glow::FRONT,
            Self::Back => glow::BACK,
        }
    }
}

pub struct DrawParameters {
    pub cull_face: CullFace,
    pub culling: bool,
    pub color_write: ColorMask,
    pub depth_write: bool,
    pub stencil_test: bool,
    pub depth_test: bool,
    pub blend: bool,
}

impl Default for DrawParameters {
    fn default() -> Self {
        Self {
            cull_face: CullFace::Back,
            culling: true,
            color_write: Default::default(),
            depth_write: true,
            stencil_test: false,
            depth_test: true,
            blend: false,
        }
    }
}

unsafe fn set_attachment(state: &mut PipelineState, gl_attachment_kind: u32, texture: &GpuTexture) {
    match texture.kind() {
        GpuTextureKind::Line { .. } => {
            state.gl.framebuffer_texture(
                glow::FRAMEBUFFER,
                gl_attachment_kind,
                Some(texture.id()),
                0,
            );
        }
        GpuTextureKind::Rectangle { .. } => {
            state.gl.framebuffer_texture_2d(
                glow::FRAMEBUFFER,
                gl_attachment_kind,
                glow::TEXTURE_2D,
                Some(texture.id()),
                0,
            );
        }
        GpuTextureKind::Cube { .. } => {
            state.gl.framebuffer_texture_2d(
                glow::FRAMEBUFFER,
                gl_attachment_kind,
                glow::TEXTURE_CUBE_MAP_POSITIVE_X,
                Some(texture.id()),
                0,
            );
        }
        GpuTextureKind::Volume { .. } => {
            state.gl.framebuffer_texture_3d(
                glow::FRAMEBUFFER,
                gl_attachment_kind,
                glow::TEXTURE_3D,
                Some(texture.id()),
                0,
                0,
            );
        }
    }
}

impl FrameBuffer {
    pub fn new(
        state: &mut PipelineState,
        depth_attachment: Option<Attachment>,
        color_attachments: Vec<Attachment>,
    ) -> Result<Self, FrameworkError> {
        unsafe {
            let fbo = state.gl.create_framebuffer()?;

            state.set_framebuffer(fbo);

            if let Some(depth_attachment) = depth_attachment.as_ref() {
                let depth_attachment_kind = match depth_attachment.kind {
                    AttachmentKind::Color => {
                        panic!("Attempt to use color attachment as depth/stencil!")
                    }
                    AttachmentKind::DepthStencil => glow::DEPTH_STENCIL_ATTACHMENT,
                    AttachmentKind::Depth => glow::DEPTH_ATTACHMENT,
                };
                set_attachment(
                    state,
                    depth_attachment_kind,
                    &depth_attachment.texture.borrow(),
                );
            }

            let mut color_buffers = Vec::new();
            for (i, color_attachment) in color_attachments.iter().enumerate() {
                assert_eq!(color_attachment.kind, AttachmentKind::Color);
                let color_attachment_kind = glow::COLOR_ATTACHMENT0 + i as u32;
                set_attachment(
                    state,
                    color_attachment_kind,
                    &color_attachment.texture.borrow(),
                );
                color_buffers.push(color_attachment_kind);
            }

            if color_buffers.is_empty() {
                state.gl.draw_buffer(glow::NONE)
            } else {
                state.gl.draw_buffers(&color_buffers);
            }

            if state.gl.check_framebuffer_status(glow::FRAMEBUFFER) != glow::FRAMEBUFFER_COMPLETE {
                return Err(FrameworkError::FailedToConstructFBO);
            }

            state.set_framebuffer(glow::Framebuffer::default());

            Ok(Self {
                state,
                fbo,
                depth_attachment,
                color_attachments,
            })
        }
    }

    pub fn backbuffer(state: &mut PipelineState) -> Self {
        Self {
            state,
            fbo: Default::default(),
            depth_attachment: None,
            color_attachments: Default::default(),
        }
    }

    pub fn color_attachments(&self) -> &[Attachment] {
        &self.color_attachments
    }

    pub fn depth_attachment(&self) -> Option<&Attachment> {
        self.depth_attachment.as_ref()
    }

    pub fn set_cubemap_face(
        &mut self,
        state: &mut PipelineState,
        attachment_index: usize,
        face: CubeMapFace,
    ) -> &mut Self {
        unsafe {
            state.set_framebuffer(self.fbo);

            let attachment = self.color_attachments.get(attachment_index).unwrap();
            state.gl.framebuffer_texture_2d(
                glow::FRAMEBUFFER,
                glow::COLOR_ATTACHMENT0 + attachment_index as u32,
                face.into_gl_value(),
                Some(attachment.texture.borrow().id()),
                0,
            );
        }

        self
    }

    pub fn id(&self) -> glow::Framebuffer {
        self.fbo
    }

    pub fn clear(
        &mut self,
        state: &mut PipelineState,
        viewport: Rect<i32>,
        color: Option<Color>,
        depth: Option<f32>,
        stencil: Option<i32>,
    ) {
        scope_profile!();

        let mut mask = 0;

        state.set_viewport(viewport);
        state.set_framebuffer(self.id());

        if let Some(color) = color {
            state.set_color_write(ColorMask::default());
            state.set_clear_color(color);
            mask |= glow::COLOR_BUFFER_BIT;
        }
        if let Some(depth) = depth {
            state.set_depth_write(true);
            state.set_clear_depth(depth);
            mask |= glow::DEPTH_BUFFER_BIT;
        }
        if let Some(stencil) = stencil {
            state.set_stencil_mask(0xFFFF_FFFF);
            state.set_clear_stencil(stencil);
            mask |= glow::STENCIL_BUFFER_BIT;
        }

        unsafe {
            state.gl.clear(mask);
        }
    }

    pub fn draw<F: FnOnce(GpuProgramBinding<'_>)>(
        &mut self,
        geometry: &GeometryBuffer,
        state: &mut PipelineState,
        viewport: Rect<i32>,
        program: &GpuProgram,
        params: &DrawParameters,
        apply_uniforms: F,
    ) -> DrawCallStatistics {
        scope_profile!();

        pre_draw(self.id(), state, viewport, program, params, apply_uniforms);
        geometry.bind(state).draw()
    }

    pub fn draw_instances<F: FnOnce(GpuProgramBinding<'_>)>(
        &mut self,
        count: usize,
        geometry: &GeometryBuffer,
        state: &mut PipelineState,
        viewport: Rect<i32>,
        program: &GpuProgram,
        params: &DrawParameters,
        apply_uniforms: F,
    ) -> DrawCallStatistics {
        scope_profile!();

        pre_draw(self.id(), state, viewport, program, params, apply_uniforms);
        geometry.bind(state).draw_instances(count)
    }

    pub fn draw_part<F: FnOnce(GpuProgramBinding<'_>)>(
        &mut self,
        geometry: &mut GeometryBuffer,
        state: &mut PipelineState,
        viewport: Rect<i32>,
        program: &GpuProgram,
        params: DrawParameters,
        offset: usize,
        count: usize,
        apply_uniforms: F,
    ) -> Result<DrawCallStatistics, FrameworkError> {
        scope_profile!();

        pre_draw(self.id(), state, viewport, program, &params, apply_uniforms);
        geometry.bind(state).draw_part(offset, count)
    }
}

fn pre_draw<F: FnOnce(GpuProgramBinding<'_>)>(
    fbo: glow::Framebuffer,
    state: &mut PipelineState,
    viewport: Rect<i32>,
    program: &GpuProgram,
    params: &DrawParameters,
    apply_uniforms: F,
) {
    scope_profile!();

    state.set_framebuffer(fbo);
    state.set_viewport(viewport);
    state.apply_draw_parameters(params);

    let program_binding = program.bind(state);
    apply_uniforms(program_binding);
}

impl Drop for FrameBuffer {
    fn drop(&mut self) {
        unsafe {
            if self.fbo != Default::default() {
                (*self.state).gl.delete_framebuffer(self.fbo);
            }
        }
    }
}