use {
    crate::{
        allocator::Allocator,
        cpu_worker::CpuWorker,
        format::{Format, XRGB8888},
        gfx_api::{
            AsyncShmGfxTexture, BufferResvUser, GfxApi, GfxBlendBuffer, GfxContext, GfxError,
            GfxFormat, GfxFramebuffer, GfxImage, GfxInternalFramebuffer, ResetStatus,
            ShmGfxTexture,
        },
        gfx_apis::gl::{
            GfxGlState, RenderError, Texture,
            egl::{context::EglContext, display::EglDisplay, image::EglImage},
            ext::GL_OES_EGL_IMAGE_EXTERNAL,
            gl::{
                program::GlProgram, render_buffer::GlRenderBuffer, sys::GLint, texture::GlTexture,
            },
            renderer::{framebuffer::Framebuffer, image::Image},
        },
        rect::Rect,
        video::{
            dmabuf::DmaBuf,
            drm::{Drm, syncobj::SyncobjCtx},
            gbm::GbmDevice,
        },
    },
    ahash::AHashMap,
    linearize::{Linearize, StaticMap, static_map},
    std::{
        cell::{Cell, RefCell},
        ffi::CString,
        fmt::{Debug, Formatter},
        rc::Rc,
    },
};

pub(crate) struct TexProg {
    pub(crate) prog: GlProgram,
    pub(crate) pos: GLint,
    pub(crate) texcoord: GLint,
    pub(crate) tex: GLint,
    pub(crate) alpha: GLint,
}

impl TexProg {
    unsafe fn from(prog: GlProgram, alpha_multiplier: bool) -> Self {
        unsafe {
            let alpha = match alpha_multiplier {
                true => prog.get_uniform_location(c"alpha"),
                false => 0,
            };
            Self {
                pos: prog.get_attrib_location(c"pos"),
                texcoord: prog.get_attrib_location(c"texcoord"),
                tex: prog.get_uniform_location(c"tex"),
                alpha,
                prog,
            }
        }
    }
}

pub(crate) struct RoundedFillProg {
    pub(crate) prog: GlProgram,
    pub(crate) pos: GLint,
    pub(crate) geo: GLint,
    pub(crate) color: GLint,
    pub(crate) size: GLint,
    pub(crate) corner_radius: GLint,
    pub(crate) border_width: GLint,
    pub(crate) scale: GLint,
}

pub(crate) struct RoundedTexProg {
    pub(crate) prog: GlProgram,
    pub(crate) pos: GLint,
    pub(crate) texcoord: GLint,
    pub(crate) geo: GLint,
    pub(crate) tex: GLint,
    pub(crate) alpha: GLint,
    pub(crate) size: GLint,
    pub(crate) corner_radius: GLint,
    pub(crate) scale: GLint,
}

#[derive(Copy, Clone, PartialEq, Linearize)]
pub(in crate::gfx_apis::gl) enum TexCopyType {
    Identity,
    Multiply,
}

#[derive(Copy, Clone, PartialEq, Linearize)]
pub(in crate::gfx_apis::gl) enum TexSourceType {
    Opaque,
    HasAlpha,
}

pub(in crate::gfx_apis::gl) struct GlRenderContext {
    pub(crate) ctx: Rc<EglContext>,
    pub gbm: Rc<GbmDevice>,
    pub sync_ctx: Rc<SyncobjCtx>,

    pub(crate) render_node: Rc<CString>,

    pub(crate) tex_internal: StaticMap<TexCopyType, StaticMap<TexSourceType, TexProg>>,
    pub(crate) tex_external: Option<StaticMap<TexCopyType, StaticMap<TexSourceType, TexProg>>>,

    pub(crate) fill_prog: GlProgram,
    pub(crate) fill_prog_pos: GLint,
    pub(crate) fill_prog_color: GLint,

    pub(crate) rounded_fill_prog: RoundedFillProg,
    pub(crate) rounded_tex_internal:
        StaticMap<TexCopyType, StaticMap<TexSourceType, RoundedTexProg>>,
    pub(crate) rounded_tex_external:
        Option<StaticMap<TexCopyType, StaticMap<TexSourceType, RoundedTexProg>>>,

    pub(in crate::gfx_apis::gl) gl_state: RefCell<GfxGlState>,

    pub(in crate::gfx_apis::gl) buffer_resv_user: BufferResvUser,
}

impl Debug for GlRenderContext {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("RenderContext").finish_non_exhaustive()
    }
}

impl GlRenderContext {
    pub fn reset_status(&self) -> Option<ResetStatus> {
        self.ctx.reset_status()
    }

    pub(in crate::gfx_apis::gl) fn from_drm_device(
        drm: &Drm,
        software: bool,
    ) -> Result<Self, RenderError> {
        let node = drm
            .get_render_node()?
            .ok_or(RenderError::NoRenderNode)
            .map(Rc::new)?;
        let dpy = EglDisplay::create(drm, software)?;
        if !dpy.formats.contains_key(&XRGB8888.drm) {
            return Err(RenderError::XRGB888);
        }
        let ctx = dpy.create_context()?;
        ctx.with_current(|| unsafe { Self::new(&ctx, &node) })
    }

    unsafe fn new(ctx: &Rc<EglContext>, node: &Rc<CString>) -> Result<Self, RenderError> {
        let tex_vert = include_str!("../shaders/tex.vert.glsl");
        let tex_frag = include_str!("../shaders/tex.frag.glsl");
        let create_programs = |external: bool| {
            let create_program = |alpha_multiplier: bool, alpha: bool| {
                let mut tex_frac_src = String::new();
                if external {
                    tex_frac_src.push_str("#define EXTERNAL\n");
                }
                if alpha_multiplier {
                    tex_frac_src.push_str("#define ALPHA_MULTIPLIER\n");
                }
                if alpha {
                    tex_frac_src.push_str("#define ALPHA\n");
                }
                tex_frac_src.push_str(tex_frag);
                unsafe {
                    let prog = GlProgram::from_shaders(ctx, tex_vert, &tex_frac_src)?;
                    Ok::<_, RenderError>(TexProg::from(prog, alpha_multiplier))
                }
            };
            Ok::<_, RenderError>(static_map! {
                TexCopyType::Identity => static_map! {
                    TexSourceType::Opaque => create_program(false, false)?,
                    TexSourceType::HasAlpha => create_program(false, true)?,
                },
                TexCopyType::Multiply => static_map! {
                    TexSourceType::Opaque => create_program(true, false)?,
                    TexSourceType::HasAlpha => create_program(true, true)?,
                },
            })
        };
        let tex_internal = create_programs(false)?;
        let tex_external = if ctx.ext.contains(GL_OES_EGL_IMAGE_EXTERNAL) {
            Some(create_programs(true)?)
        } else {
            None
        };
        let fill_prog = unsafe {
            GlProgram::from_shaders(
                ctx,
                include_str!("../shaders/fill.vert.glsl"),
                include_str!("../shaders/fill.frag.glsl"),
            )?
        };
        let rounded_fill_prog = unsafe {
            let prog = GlProgram::from_shaders(
                ctx,
                include_str!("../shaders/rounded_fill.vert.glsl"),
                include_str!("../shaders/rounded_fill.frag.glsl"),
            )?;
            RoundedFillProg {
                pos: prog.get_attrib_location(c"pos"),
                geo: prog.get_attrib_location(c"geo"),
                color: prog.get_uniform_location(c"color"),
                size: prog.get_uniform_location(c"size"),
                corner_radius: prog.get_uniform_location(c"corner_radius"),
                border_width: prog.get_uniform_location(c"border_width"),
                scale: prog.get_uniform_location(c"scale"),
                prog,
            }
        };
        let rounded_tex_vert = include_str!("../shaders/rounded_tex.vert.glsl");
        let rounded_tex_frag = include_str!("../shaders/rounded_tex.frag.glsl");
        let create_rounded_tex_programs = |external: bool| {
            let create_program = |alpha_multiplier: bool, alpha: bool| {
                let mut src = String::new();
                if external {
                    src.push_str("#define EXTERNAL\n");
                }
                if alpha_multiplier {
                    src.push_str("#define ALPHA_MULTIPLIER\n");
                }
                if alpha {
                    src.push_str("#define ALPHA\n");
                }
                src.push_str(rounded_tex_frag);
                unsafe {
                    let prog = GlProgram::from_shaders(ctx, rounded_tex_vert, &src)?;
                    let alpha_loc = match alpha_multiplier {
                        true => prog.get_uniform_location(c"alpha"),
                        false => 0,
                    };
                    Ok::<_, RenderError>(RoundedTexProg {
                        pos: prog.get_attrib_location(c"pos"),
                        texcoord: prog.get_attrib_location(c"texcoord"),
                        geo: prog.get_attrib_location(c"geo"),
                        tex: prog.get_uniform_location(c"tex"),
                        alpha: alpha_loc,
                        size: prog.get_uniform_location(c"size"),
                        corner_radius: prog.get_uniform_location(c"corner_radius"),
                        scale: prog.get_uniform_location(c"scale"),
                        prog,
                    })
                }
            };
            Ok::<_, RenderError>(static_map! {
                TexCopyType::Identity => static_map! {
                    TexSourceType::Opaque => create_program(false, false)?,
                    TexSourceType::HasAlpha => create_program(false, true)?,
                },
                TexCopyType::Multiply => static_map! {
                    TexSourceType::Opaque => create_program(true, false)?,
                    TexSourceType::HasAlpha => create_program(true, true)?,
                },
            })
        };
        let rounded_tex_internal = create_rounded_tex_programs(false)?;
        let rounded_tex_external = if ctx.ext.contains(GL_OES_EGL_IMAGE_EXTERNAL) {
            Some(create_rounded_tex_programs(true)?)
        } else {
            None
        };
        Ok(Self {
            ctx: ctx.clone(),
            gbm: ctx.dpy.gbm.clone(),
            sync_ctx: Rc::new(SyncobjCtx::new(ctx.dpy.gbm.drm.fd())),

            render_node: node.clone(),

            tex_internal,
            tex_external,

            fill_prog_pos: unsafe { fill_prog.get_attrib_location(c"pos") },
            fill_prog_color: unsafe { fill_prog.get_uniform_location(c"color") },
            fill_prog,

            rounded_fill_prog,
            rounded_tex_internal,
            rounded_tex_external,

            gl_state: Default::default(),

            buffer_resv_user: Default::default(),
        })
    }

    pub fn render_node(&self) -> Rc<CString> {
        self.render_node.clone()
    }

    pub fn formats(&self) -> &Rc<AHashMap<u32, GfxFormat>> {
        &self.ctx.formats
    }

    fn dmabuf_fb(self: &Rc<Self>, buf: &DmaBuf) -> Result<Rc<Framebuffer>, RenderError> {
        self.ctx.with_current(|| unsafe {
            let img = self.ctx.dpy.import_dmabuf(buf)?;
            let rb = GlRenderBuffer::from_image(&img, &self.ctx)?;
            let fb = rb.create_framebuffer()?;
            Ok(Rc::new(Framebuffer {
                ctx: self.clone(),
                gl: fb,
            }))
        })
    }

    fn dmabuf_img(self: &Rc<Self>, buf: &DmaBuf) -> Result<Rc<Image>, RenderError> {
        self.ctx.with_current(|| {
            let img = self.ctx.dpy.import_dmabuf(buf)?;
            Ok(Rc::new(Image {
                ctx: self.clone(),
                gl: img,
            }))
        })
    }

    fn shmem_texture(
        self: &Rc<Self>,
        data: &[Cell<u8>],
        format: &'static Format,
        width: i32,
        height: i32,
        stride: i32,
    ) -> Result<Rc<Texture>, RenderError> {
        let gl = GlTexture::import_shm(&self.ctx, data, format, width, height, stride)?;
        Ok(Rc::new(Texture {
            ctx: self.clone(),
            gl,
            format,
        }))
    }

    pub fn image_to_fb(
        self: &Rc<Self>,
        img: &Rc<EglImage>,
    ) -> Result<Rc<Framebuffer>, RenderError> {
        self.ctx.with_current(|| unsafe {
            let rb = GlRenderBuffer::from_image(img, &self.ctx)?;
            let fb = rb.create_framebuffer()?;
            Ok(Rc::new(Framebuffer {
                ctx: self.clone(),
                gl: fb,
            }))
        })
    }
}

impl GfxContext for GlRenderContext {
    fn reset_status(&self) -> Option<ResetStatus> {
        self.reset_status()
    }

    fn render_node(&self) -> Option<Rc<CString>> {
        Some(self.render_node())
    }

    fn formats(&self) -> &Rc<AHashMap<u32, GfxFormat>> {
        self.formats()
    }

    fn fast_ram_access(&self) -> bool {
        self.ctx.dpy.fast_ram_access
    }

    fn dmabuf_fb(self: Rc<Self>, buf: &DmaBuf) -> Result<Rc<dyn GfxFramebuffer>, GfxError> {
        (&self)
            .dmabuf_fb(buf)
            .map(|w| w as Rc<dyn GfxFramebuffer>)
            .map_err(|e| e.into())
    }

    fn dmabuf_img(self: Rc<Self>, buf: &DmaBuf) -> Result<Rc<dyn GfxImage>, GfxError> {
        (&self)
            .dmabuf_img(buf)
            .map(|w| w as Rc<dyn GfxImage>)
            .map_err(|e| e.into())
    }

    fn shmem_texture(
        self: Rc<Self>,
        _old: Option<Rc<dyn ShmGfxTexture>>,
        data: &[Cell<u8>],
        format: &'static Format,
        width: i32,
        height: i32,
        stride: i32,
        _damage: Option<&[Rect]>,
    ) -> Result<Rc<dyn ShmGfxTexture>, GfxError> {
        (&self)
            .shmem_texture(data, format, width, height, stride)
            .map(|w| w as Rc<dyn ShmGfxTexture>)
            .map_err(|e| e.into())
    }

    fn async_shmem_texture(
        self: Rc<Self>,
        format: &'static Format,
        width: i32,
        height: i32,
        stride: i32,
        _cpu_worker: &Rc<CpuWorker>,
    ) -> Result<Rc<dyn AsyncShmGfxTexture>, GfxError> {
        let tex = self.ctx.with_current(|| unsafe {
            let mut tex = 0;
            (self.ctx.dpy.gles.glGenTextures)(1, &mut tex);
            Ok(tex)
        })?;
        Ok(Rc::new(Texture {
            gl: GlTexture {
                ctx: self.ctx.clone(),
                img: None,
                tex,
                width,
                height,
                stride,
                external_only: false,
                format,
                contents_valid: Cell::new(false),
            },
            ctx: self,
            format,
        }))
    }

    fn allocator(&self) -> Rc<dyn Allocator> {
        self.gbm.clone()
    }

    fn gfx_api(&self) -> GfxApi {
        GfxApi::OpenGl
    }

    fn create_internal_fb(
        self: Rc<Self>,
        _cpu_worker: &Rc<CpuWorker>,
        width: i32,
        height: i32,
        stride: i32,
        format: &'static Format,
    ) -> Result<Rc<dyn GfxInternalFramebuffer>, GfxError> {
        let fb = self.ctx.with_current(|| unsafe {
            GlRenderBuffer::new(&self.ctx, width, height, stride, format)?.create_framebuffer()
        })?;
        Ok(Rc::new(Framebuffer { ctx: self, gl: fb }))
    }

    fn syncobj_ctx(&self) -> Option<&Rc<SyncobjCtx>> {
        Some(&self.sync_ctx)
    }

    fn acquire_blend_buffer(
        &self,
        _width: i32,
        _height: i32,
    ) -> Result<Rc<dyn GfxBlendBuffer>, GfxError> {
        Err(GfxError(Box::new(RenderError::NoBlendBuffer)))
    }

    fn supports_invalid_modifier(&self) -> bool {
        true
    }
}
