mod storage;

use storage::Storage;

pub use iced_graphics::triangle::{Mesh2D, Vertex2D};

use crate::program::{self, Shader};
use crate::Transformation;

#[cfg(feature = "image")]
use iced_graphics::image::raster;

#[cfg(feature = "svg")]
use iced_graphics::image::vector;

use iced_graphics::layer;
use iced_graphics::Rectangle;
use iced_graphics::Size;

use glow::HasContext;

use std::cell::RefCell;

#[cfg(feature = "tracing")]
use tracing::info_span;

#[derive(Debug)]
pub(crate) struct Pipeline {
    program: <glow::Context as HasContext>::Program,
    vertex_array: <glow::Context as HasContext>::VertexArray,
    vertex_buffer: <glow::Context as HasContext>::Buffer,
    transform_location: <glow::Context as HasContext>::UniformLocation,
    storage: Storage,
    #[cfg(feature = "image")]
    raster_cache: RefCell<raster::Cache<Storage>>,
    #[cfg(feature = "svg")]
    vector_cache: RefCell<vector::Cache<Storage>>,
}

impl Pipeline {
    pub fn new(
        gl: &glow::Context,
        shader_version: &program::Version,
    ) -> Pipeline {
        let program = unsafe {
            let vertex_shader = Shader::vertex(
                gl,
                shader_version,
                include_str!("shader/common/image.vert"),
            );
            let fragment_shader = Shader::fragment(
                gl,
                shader_version,
                include_str!("shader/common/image.frag"),
            );

            program::create(
                gl,
                &[vertex_shader, fragment_shader],
                &[(0, "i_Position")],
            )
        };

        let transform_location =
            unsafe { gl.get_uniform_location(program, "u_Transform") }
                .expect("Get transform location");

        unsafe {
            gl.use_program(Some(program));

            let transform: [f32; 16] = Transformation::identity().into();
            gl.uniform_matrix_4_f32_slice(
                Some(&transform_location),
                false,
                &transform,
            );

            gl.use_program(None);
        }

        let vertex_buffer =
            unsafe { gl.create_buffer().expect("Create vertex buffer") };
        let vertex_array =
            unsafe { gl.create_vertex_array().expect("Create vertex array") };

        unsafe {
            gl.bind_vertex_array(Some(vertex_array));
            gl.bind_buffer(glow::ARRAY_BUFFER, Some(vertex_buffer));

            let vertices = &[0u8, 0, 1, 0, 0, 1, 1, 1];
            gl.buffer_data_size(
                glow::ARRAY_BUFFER,
                vertices.len() as i32,
                glow::STATIC_DRAW,
            );
            gl.buffer_sub_data_u8_slice(
                glow::ARRAY_BUFFER,
                0,
                bytemuck::cast_slice(vertices),
            );

            gl.enable_vertex_attrib_array(0);
            gl.vertex_attrib_pointer_f32(
                0,
                2,
                glow::UNSIGNED_BYTE,
                false,
                0,
                0,
            );

            gl.bind_buffer(glow::ARRAY_BUFFER, None);
            gl.bind_vertex_array(None);
        }

        Pipeline {
            program,
            vertex_array,
            vertex_buffer,
            transform_location,
            storage: Storage::default(),
            #[cfg(feature = "image")]
            raster_cache: RefCell::new(raster::Cache::default()),
            #[cfg(feature = "svg")]
            vector_cache: RefCell::new(vector::Cache::default()),
        }
    }

    #[cfg(feature = "image")]
    pub fn dimensions(&self, handle: &iced_native::image::Handle) -> Size<u32> {
        self.raster_cache.borrow_mut().load(handle).dimensions()
    }

    #[cfg(feature = "svg")]
    pub fn viewport_dimensions(
        &self,
        handle: &iced_native::svg::Handle,
    ) -> Size<u32> {
        let mut cache = self.vector_cache.borrow_mut();
        let svg = cache.load(handle);

        svg.viewport_dimensions()
    }

    pub fn draw(
        &mut self,
        mut gl: &glow::Context,
        target_height: u32,
        transformation: Transformation,
        _scale_factor: f32,
        images: &[layer::Image],
        layer_bounds: Rectangle<u32>,
    ) {
        #[cfg(feature = "tracing")]
        let _ = info_span!("Glow::Image", "DRAW").entered();

        unsafe {
            gl.use_program(Some(self.program));
            gl.bind_vertex_array(Some(self.vertex_array));
            gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.vertex_buffer));
            gl.enable(glow::SCISSOR_TEST);
        }

        #[cfg(feature = "image")]
        let mut raster_cache = self.raster_cache.borrow_mut();

        #[cfg(feature = "svg")]
        let mut vector_cache = self.vector_cache.borrow_mut();

        for image in images {
            let (entry, bounds) = match &image {
                #[cfg(feature = "image")]
                layer::Image::Raster { handle, bounds } => (
                    raster_cache.upload(handle, &mut gl, &mut self.storage),
                    bounds,
                ),
                #[cfg(not(feature = "image"))]
                layer::Image::Raster { handle: _, bounds } => (None, bounds),

                #[cfg(feature = "svg")]
                layer::Image::Vector {
                    handle,
                    color,
                    bounds,
                } => {
                    let size = [bounds.width, bounds.height];
                    (
                        vector_cache.upload(
                            handle,
                            *color,
                            size,
                            _scale_factor,
                            &mut gl,
                            &mut self.storage,
                        ),
                        bounds,
                    )
                }

                #[cfg(not(feature = "svg"))]
                layer::Image::Vector { bounds, .. } => (None, bounds),
            };

            unsafe {
                gl.scissor(
                    layer_bounds.x as i32,
                    (target_height - (layer_bounds.y + layer_bounds.height))
                        as i32,
                    layer_bounds.width as i32,
                    layer_bounds.height as i32,
                );

                if let Some(storage::Entry { texture, .. }) = entry {
                    gl.bind_texture(glow::TEXTURE_2D, Some(*texture))
                } else {
                    continue;
                }

                let translate = Transformation::translate(bounds.x, bounds.y);
                let scale = Transformation::scale(bounds.width, bounds.height);
                let transformation = transformation * translate * scale;
                let matrix: [f32; 16] = transformation.into();
                gl.uniform_matrix_4_f32_slice(
                    Some(&self.transform_location),
                    false,
                    &matrix,
                );

                gl.draw_arrays(glow::TRIANGLE_STRIP, 0, 4);

                gl.bind_texture(glow::TEXTURE_2D, None);
            }
        }

        unsafe {
            gl.bind_buffer(glow::ARRAY_BUFFER, None);
            gl.bind_vertex_array(None);
            gl.use_program(None);
            gl.disable(glow::SCISSOR_TEST);
        }
    }

    pub fn trim_cache(&mut self, mut gl: &glow::Context) {
        #[cfg(feature = "image")]
        self.raster_cache
            .borrow_mut()
            .trim(&mut self.storage, &mut gl);

        #[cfg(feature = "svg")]
        self.vector_cache
            .borrow_mut()
            .trim(&mut self.storage, &mut gl);
    }
}
