/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

use canvas_traits::webgl::{GLLimits, WebGLVersion};
use sparkle::gl;
use sparkle::gl::GLenum;
use sparkle::gl::Gl;
use sparkle::gl::GlType;

pub trait GLLimitsDetect {
    fn detect(gl: &Gl, webgl_version: WebGLVersion) -> Self;
}

impl GLLimitsDetect for GLLimits {
    fn detect(gl: &Gl, webgl_version: WebGLVersion) -> GLLimits {
        let max_vertex_attribs = gl.get_integer(gl::MAX_VERTEX_ATTRIBS);
        let max_tex_size = gl.get_integer(gl::MAX_TEXTURE_SIZE);
        let max_cube_map_tex_size = gl.get_integer(gl::MAX_CUBE_MAP_TEXTURE_SIZE);
        let max_combined_texture_image_units = gl.get_integer(gl::MAX_COMBINED_TEXTURE_IMAGE_UNITS);
        let max_renderbuffer_size = gl.get_integer(gl::MAX_RENDERBUFFER_SIZE);
        let max_texture_image_units = gl.get_integer(gl::MAX_TEXTURE_IMAGE_UNITS);
        let max_vertex_texture_image_units = gl.get_integer(gl::MAX_VERTEX_TEXTURE_IMAGE_UNITS);

        // TODO: better value for this?
        let max_client_wait_timeout_webgl = std::time::Duration::new(1, 0);

        // Based on:
        // https://searchfox.org/mozilla-central/rev/5a744713370ec47969595e369fd5125f123e6d24/dom/canvas/WebGLContextValidate.cpp#523-558
        let (
            max_fragment_uniform_vectors,
            max_varying_vectors,
            max_vertex_uniform_vectors,
            max_vertex_output_vectors,
            max_fragment_input_vectors,
        );
        if gl.get_type() == GlType::Gles {
            max_fragment_uniform_vectors = gl.get_integer(gl::MAX_FRAGMENT_UNIFORM_VECTORS);
            max_varying_vectors = gl.get_integer(gl::MAX_VARYING_VECTORS);
            max_vertex_uniform_vectors = gl.get_integer(gl::MAX_VERTEX_UNIFORM_VECTORS);
            max_vertex_output_vectors = gl
                .try_get_integer(gl::MAX_VERTEX_OUTPUT_COMPONENTS)
                .map(|c| c / 4)
                .unwrap_or(max_varying_vectors);
            max_fragment_input_vectors = gl
                .try_get_integer(gl::MAX_FRAGMENT_INPUT_COMPONENTS)
                .map(|c| c / 4)
                .unwrap_or(max_vertex_output_vectors);
        } else {
            max_fragment_uniform_vectors = gl.get_integer(gl::MAX_FRAGMENT_UNIFORM_COMPONENTS) / 4;
            max_vertex_uniform_vectors = gl.get_integer(gl::MAX_VERTEX_UNIFORM_COMPONENTS) / 4;

            max_fragment_input_vectors = gl
                .try_get_integer(gl::MAX_FRAGMENT_INPUT_COMPONENTS)
                .or_else(|| gl.try_get_integer(gl::MAX_VARYING_COMPONENTS))
                .map(|c| c / 4)
                .unwrap_or_else(|| gl.get_integer(gl::MAX_VARYING_VECTORS));
            max_vertex_output_vectors = gl
                .try_get_integer(gl::MAX_VERTEX_OUTPUT_COMPONENTS)
                .map(|c| c / 4)
                .unwrap_or(max_fragment_input_vectors);
            max_varying_vectors = max_vertex_output_vectors
                .min(max_fragment_input_vectors)
                .max(4);
        };

        let (
            max_uniform_block_size,
            max_uniform_buffer_bindings,
            min_program_texel_offset,
            max_program_texel_offset,
            max_transform_feedback_separate_attribs,
            max_draw_buffers,
            max_color_attachments,
            max_combined_uniform_blocks,
            max_combined_vertex_uniform_components,
            max_combined_fragment_uniform_components,
            max_vertex_uniform_blocks,
            max_vertex_uniform_components,
            max_fragment_uniform_blocks,
            max_fragment_uniform_components,
            max_3d_texture_size,
            max_array_texture_layers,
            uniform_buffer_offset_alignment,
            max_element_index,
            max_elements_indices,
            max_elements_vertices,
            max_fragment_input_components,
            max_samples,
            max_server_wait_timeout,
            max_texture_lod_bias,
            max_varying_components,
            max_vertex_output_components,
        );
        if webgl_version == WebGLVersion::WebGL2 {
            max_uniform_block_size = gl.get_integer64(gl::MAX_UNIFORM_BLOCK_SIZE);
            max_uniform_buffer_bindings = gl.get_integer(gl::MAX_UNIFORM_BUFFER_BINDINGS);
            min_program_texel_offset = gl.get_signed_integer(gl::MIN_PROGRAM_TEXEL_OFFSET);
            max_program_texel_offset = gl.get_integer(gl::MAX_PROGRAM_TEXEL_OFFSET);
            max_transform_feedback_separate_attribs =
                gl.get_integer(gl::MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS);
            max_color_attachments = gl.get_integer(gl::MAX_COLOR_ATTACHMENTS);
            max_draw_buffers = gl
                .get_integer(gl::MAX_DRAW_BUFFERS)
                .min(max_color_attachments);
            max_combined_uniform_blocks = gl.get_integer(gl::MAX_COMBINED_UNIFORM_BLOCKS);
            max_combined_vertex_uniform_components =
                gl.get_integer64(gl::MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS);
            max_combined_fragment_uniform_components =
                gl.get_integer64(gl::MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS);
            max_vertex_uniform_blocks = gl.get_integer(gl::MAX_VERTEX_UNIFORM_BLOCKS);
            max_vertex_uniform_components = gl.get_integer(gl::MAX_VERTEX_UNIFORM_COMPONENTS);
            max_fragment_uniform_blocks = gl.get_integer(gl::MAX_FRAGMENT_UNIFORM_BLOCKS);
            max_fragment_uniform_components = gl.get_integer(gl::MAX_FRAGMENT_UNIFORM_COMPONENTS);
            uniform_buffer_offset_alignment = gl.get_integer(gl::UNIFORM_BUFFER_OFFSET_ALIGNMENT);
            max_3d_texture_size = gl.get_integer(gl::MAX_3D_TEXTURE_SIZE);
            max_array_texture_layers = gl.get_integer(gl::MAX_ARRAY_TEXTURE_LAYERS);
            max_element_index = gl
                .try_get_integer64(gl::MAX_ELEMENT_INDEX)
                .unwrap_or(u32::MAX as u64); // requires GL 4.3
            max_elements_indices = gl.get_integer(gl::MAX_ELEMENTS_INDICES);
            max_elements_vertices = gl.get_integer(gl::MAX_ELEMENTS_VERTICES);
            max_fragment_input_components = gl.get_integer(gl::MAX_FRAGMENT_INPUT_COMPONENTS);
            max_samples = gl.get_integer(gl::MAX_SAMPLES);
            max_server_wait_timeout =
                std::time::Duration::from_nanos(gl.get_integer64(gl::MAX_SERVER_WAIT_TIMEOUT));
            max_texture_lod_bias = gl.get_float(gl::MAX_TEXTURE_LOD_BIAS);
            max_varying_components = gl.try_get_integer(gl::MAX_VARYING_COMPONENTS).unwrap_or(
                // macOS Core Profile is buggy. The spec says this value is 4 * MAX_VARYING_VECTORS.
                max_varying_vectors * 4,
            );
            max_vertex_output_components = gl.get_integer(gl::MAX_VERTEX_OUTPUT_COMPONENTS);
        } else {
            max_uniform_block_size = 0;
            max_uniform_buffer_bindings = 0;
            min_program_texel_offset = 0;
            max_program_texel_offset = 0;
            max_transform_feedback_separate_attribs = 0;
            max_color_attachments = 1;
            max_draw_buffers = 1;
            max_combined_uniform_blocks = 0;
            max_combined_vertex_uniform_components = 0;
            max_combined_fragment_uniform_components = 0;
            max_vertex_uniform_blocks = 0;
            max_vertex_uniform_components = 0;
            max_fragment_uniform_blocks = 0;
            max_fragment_uniform_components = 0;
            uniform_buffer_offset_alignment = 0;
            max_3d_texture_size = 0;
            max_array_texture_layers = 0;
            max_element_index = 0;
            max_elements_indices = 0;
            max_elements_vertices = 0;
            max_fragment_input_components = 0;
            max_samples = 0;
            max_server_wait_timeout = std::time::Duration::default();
            max_texture_lod_bias = 0.0;
            max_varying_components = 0;
            max_vertex_output_components = 0;
        }

        GLLimits {
            max_vertex_attribs,
            max_tex_size,
            max_cube_map_tex_size,
            max_combined_texture_image_units,
            max_fragment_uniform_vectors,
            max_renderbuffer_size,
            max_texture_image_units,
            max_varying_vectors,
            max_vertex_texture_image_units,
            max_vertex_uniform_vectors,
            max_client_wait_timeout_webgl,
            max_transform_feedback_separate_attribs,
            max_vertex_output_vectors,
            max_fragment_input_vectors,
            max_uniform_buffer_bindings,
            min_program_texel_offset,
            max_program_texel_offset,
            max_color_attachments,
            max_draw_buffers,
            max_uniform_block_size,
            max_combined_uniform_blocks,
            max_combined_vertex_uniform_components,
            max_combined_fragment_uniform_components,
            max_vertex_uniform_blocks,
            max_vertex_uniform_components,
            max_fragment_uniform_blocks,
            max_fragment_uniform_components,
            max_3d_texture_size,
            max_array_texture_layers,
            uniform_buffer_offset_alignment,
            max_element_index,
            max_elements_indices,
            max_elements_vertices,
            max_fragment_input_components,
            max_samples,
            max_server_wait_timeout,
            max_texture_lod_bias,
            max_varying_components,
            max_vertex_output_components,
        }
    }
}

trait GLExt {
    fn try_get_integer(self, parameter: GLenum) -> Option<u32>;
    fn try_get_integer64(self, parameter: GLenum) -> Option<u64>;
    fn try_get_signed_integer(self, parameter: GLenum) -> Option<i32>;
    fn try_get_float(self, parameter: GLenum) -> Option<f32>;
    fn get_integer(self, parameter: GLenum) -> u32;
    fn get_integer64(self, parameter: GLenum) -> u64;
    fn get_signed_integer(self, parameter: GLenum) -> i32;
    fn get_float(self, parameter: GLenum) -> f32;
}

macro_rules! create_fun {
    ($tryer:ident, $getter:ident, $gltype:ty, $glcall:ident, $rstype:ty) => {
        #[allow(unsafe_code)]
        fn $tryer(self, parameter: GLenum) -> Option<$rstype> {
            let mut value = [<$gltype>::default()];
            unsafe {
                self.$glcall(parameter, &mut value);
            }
            if self.get_error() != gl::NO_ERROR {
                None
            } else {
                Some(value[0] as $rstype)
            }
        }

        fn $getter(self, parameter: GLenum) -> $rstype {
            self.$tryer(parameter).unwrap()
        }
    };
}

impl<'a> GLExt for &'a Gl {
    create_fun!(try_get_integer, get_integer, i32, get_integer_v, u32);
    create_fun!(try_get_integer64, get_integer64, i64, get_integer64_v, u64);
    create_fun!(
        try_get_signed_integer,
        get_signed_integer,
        i32,
        get_integer_v,
        i32
    );
    create_fun!(try_get_float, get_float, f32, get_float_v, f32);
}