// surfman/examples/threads.rs
//
// This example demonstrates how to create a multithreaded OpenGL application using `surfman`.

#![cfg(feature = "sm-raw-window-handle-06")]

use self::common::{ck, Buffer, Program, ResourceLoader, Shader, ShaderKind};

use euclid::default::{Point2D, Rect, Size2D, Vector2D};
use gl::types::{GLenum, GLint, GLuint};
use std::sync::mpsc::{self, Receiver, Sender};
use std::thread;
use surfman::{declare_surfman, SurfaceAccess, SurfaceTexture, SurfaceType};
use surfman::{Adapter, Connection, Context, ContextDescriptor, Device, GLApi, Surface};

#[cfg(not(target_os = "android"))]
use self::common::FilesystemResourceLoader;

#[cfg(not(target_os = "android"))]
use surfman::{ContextAttributeFlags, ContextAttributes, GLVersion};
#[cfg(not(target_os = "android"))]
use winit::{
    dpi::PhysicalSize,
    event::{DeviceEvent, Event, WindowEvent},
    event_loop::{ControlFlow, EventLoop},
    window::WindowBuilder,
};

use rwh_06::{HasDisplayHandle, HasWindowHandle};

pub mod common;

declare_surfman!();

const WINDOW_WIDTH: i32 = 800;
const WINDOW_HEIGHT: i32 = 600;

const SUBSCREEN_WIDTH: i32 = 256;
const SUBSCREEN_HEIGHT: i32 = 256;

const BALL_WIDTH: i32 = 192;
const BALL_HEIGHT: i32 = 192;

const CHECK_SIZE: f32 = 16.0;

const INITIAL_VELOCITY_X: f32 = 1.5;
const INITIAL_VELOCITY_Y: f32 = 0.0;
const GRAVITY: f32 = -0.2;
const INITIAL_ROTATION_X: f32 = 0.2;
const INITIAL_ROTATION_Y: f32 = 0.6;
const INITIAL_ROTATION_Z: f32 = 0.2;
const ROTATION_SPEED_X: f32 = 0.03;
const ROTATION_SPEED_Y: f32 = 0.01;
const ROTATION_SPEED_Z: f32 = 0.05;

const SPHERE_RADIUS: f32 = 96.0;

static QUAD_VERTEX_POSITIONS: [u8; 8] = [0, 0, 1, 0, 0, 1, 1, 1];

static CHECK_TRANSFORM: [f32; 4] = [
    SUBSCREEN_WIDTH as f32 / CHECK_SIZE as f32,
    0.0,
    0.0,
    SUBSCREEN_HEIGHT as f32 / CHECK_SIZE as f32,
];

static IDENTITY_TRANSFORM: [f32; 4] = [1.0, 0.0, 0.0, 1.0];
static ZERO_TRANSLATION: [f32; 2] = [0.0, 0.0];

static NDC_TRANSFORM: [f32; 4] = [2.0, 0.0, 0.0, 2.0];
static NDC_TRANSLATION: [f32; 2] = [-1.0, -1.0];

static CHECK_COLOR_A: [f32; 4] = [0.8, 0.0, 0.0, 1.0];
static CHECK_COLOR_B: [f32; 4] = [0.9, 0.9, 0.9, 1.0];

static CAMERA_POSITION: [f32; 3] = [400.0, 300.0, -1000.0];
static LIGHT_POSITION: [f32; 3] = [600.0, 450.0, -500.0];

static GRIDLINE_COLOR: [f32; 4] = [
    (0x9e as f32) / 255.0,
    (0x2b as f32) / 255.0,
    (0x86 as f32) / 255.0,
    1.0,
];
static BACKGROUND_COLOR: [f32; 4] = [
    (0xaa as f32) / 255.0,
    (0xaa as f32) / 255.0,
    (0xaa as f32) / 255.0,
    1.0,
];

fn make_connection(window: &winit::window::Window) -> surfman::Connection {
    let display_handle = window
        .display_handle()
        .expect("failed to get display handle from window");
    let connection = Connection::from_display_handle(display_handle).unwrap();
    connection
}

fn make_native_widget(
    window: &winit::window::Window,
    connection: &surfman::Connection,
    window_size: Size2D<i32>,
) -> surfman::NativeWidget {
    let raw_window_handle = window
        .window_handle()
        .expect("couldn't get window handle from window");
    let native_widget = connection
        .create_native_widget_from_window_handle(raw_window_handle, window_size)
        .unwrap();
    native_widget
}

#[cfg(target_os = "android")]
fn main() {}

#[cfg(not(target_os = "android"))]
fn main() {
    use winit::{
        event::RawKeyEvent,
        keyboard::{KeyCode, PhysicalKey},
    };

    let event_loop = EventLoop::new().expect("couldn't create eventloop");
    let physical_size = PhysicalSize::new(WINDOW_WIDTH, WINDOW_HEIGHT);

    let window = WindowBuilder::new()
        .with_title("Multithreaded example")
        .with_inner_size(physical_size)
        .build(&event_loop)
        .unwrap();

    window.set_visible(true);

    let connection = make_connection(&window);

    let window_size = window.inner_size();
    let window_size = Size2D::new(window_size.width as i32, window_size.height as i32);
    let native_widget = make_native_widget(&window, &connection, window_size);
    let adapter = connection.create_low_power_adapter().unwrap();
    let device = connection.create_device(&adapter).unwrap();

    let context_attributes = ContextAttributes {
        version: GLVersion::new(3, 0),
        flags: ContextAttributeFlags::ALPHA,
    };
    let context_descriptor = device
        .create_context_descriptor(&context_attributes)
        .unwrap();

    let surface_type = SurfaceType::Widget { native_widget };
    let mut context = device.create_context(&context_descriptor, None).unwrap();
    let surface = device
        .create_surface(&context, SurfaceAccess::GPUOnly, surface_type)
        .unwrap();
    device
        .bind_surface_to_context(&mut context, surface)
        .unwrap();
    device.make_context_current(&context).unwrap();

    let mut app = App::new(
        connection,
        adapter,
        device,
        context,
        Box::new(FilesystemResourceLoader),
        window_size,
    );

    event_loop
        .run(move |event, target| match event {
            Event::WindowEvent {
                event: WindowEvent::CloseRequested,
                ..
            }
            | Event::DeviceEvent {
                event:
                    DeviceEvent::Key(RawKeyEvent {
                        physical_key: PhysicalKey::Code(KeyCode::Escape),
                        ..
                    }),
                ..
            } => target.exit(),
            _ => {
                app.tick(true);
                target.set_control_flow(ControlFlow::Poll)
            }
        })
        .expect("failed to run event loop");
}
pub struct App {
    main_from_worker_receiver: Receiver<Frame>,
    main_to_worker_sender: Sender<Surface>,
    grid_vertex_array: GridVertexArray,
    blit_vertex_array: BlitVertexArray,
    device: Device,
    context: Context,
    texture: Option<SurfaceTexture>,
    frame: Frame,
    window_size: Size2D<i32>,
}

impl Drop for App {
    fn drop(&mut self) {
        self.device.destroy_context(&mut self.context).unwrap();
    }
}

impl App {
    pub fn new(
        connection: Connection,
        adapter: Adapter,
        device: Device,
        mut context: Context,
        resource_loader: Box<dyn ResourceLoader + Send>,
        window_size: Size2D<i32>,
    ) -> App {
        let context_descriptor = device.context_descriptor(&context);

        gl::load_with(|symbol_name| device.get_proc_address(&context, symbol_name));

        // Set up GL objects and state.
        let gl_api = device.gl_api();
        let surface_gl_texture_target = device.surface_gl_texture_target();
        let grid_vertex_array =
            GridVertexArray::new(gl_api, surface_gl_texture_target, &*resource_loader);
        let blit_vertex_array =
            BlitVertexArray::new(gl_api, surface_gl_texture_target, &*resource_loader);

        // Set up communication channels, and spawn our worker thread.
        let (worker_to_main_sender, main_from_worker_receiver) = mpsc::channel();
        let (main_to_worker_sender, worker_from_main_receiver) = mpsc::channel();
        thread::spawn(move || {
            worker_thread(
                connection,
                adapter,
                context_descriptor,
                window_size,
                resource_loader,
                worker_to_main_sender,
                worker_from_main_receiver,
            )
        });

        // Fetch our initial surface.
        let mut frame = match main_from_worker_receiver.recv() {
            Err(_) => panic!(),
            Ok(frame) => frame,
        };
        let texture = Some(
            device
                .create_surface_texture(&mut context, frame.surface.take().unwrap())
                .unwrap(),
        );

        App {
            main_from_worker_receiver,
            main_to_worker_sender,
            grid_vertex_array,
            blit_vertex_array,
            device,
            texture,
            frame,
            context,
            window_size,
        }
    }

    pub fn tick(&mut self, present: bool) {
        // Send back our old surface.
        let surface = self
            .device
            .destroy_surface_texture(&mut self.context, self.texture.take().unwrap())
            .unwrap();
        self.main_to_worker_sender.send(surface).unwrap();

        // Fetch a new frame.
        self.frame = self.main_from_worker_receiver.recv().unwrap();

        // Wrap it in a texture.
        self.texture = Some(
            self.device
                .create_surface_texture(&mut self.context, self.frame.surface.take().unwrap())
                .unwrap(),
        );

        unsafe {
            self.device.make_context_current(&self.context).unwrap();

            let framebuffer_object = self
                .device
                .context_surface_info(&self.context)
                .ok()
                .flatten()
                .and_then(|surface_info| surface_info.framebuffer_object);

            gl::BindFramebuffer(
                gl::FRAMEBUFFER,
                framebuffer_object.map_or(0, |fbo| fbo.0.get()),
            );
            gl::Viewport(0, 0, self.window_size.width, self.window_size.height);

            gl::ClearColor(0.0, 0.0, 1.0, 1.0);
            ck();
            gl::Clear(gl::COLOR_BUFFER_BIT);
            ck();

            // Draw gridlines.
            gl::BindVertexArray(self.grid_vertex_array.object);
            ck();
            gl::UseProgram(self.grid_vertex_array.grid_program.program.object);
            ck();
            gl::UniformMatrix2fv(
                self.grid_vertex_array.grid_program.transform_uniform,
                1,
                gl::FALSE,
                NDC_TRANSFORM.as_ptr(),
            );
            gl::Uniform2fv(
                self.grid_vertex_array.grid_program.translation_uniform,
                1,
                NDC_TRANSLATION.as_ptr(),
            );
            gl::UniformMatrix2fv(
                self.grid_vertex_array.grid_program.tex_transform_uniform,
                1,
                gl::FALSE,
                CHECK_TRANSFORM.as_ptr(),
            );
            gl::Uniform2fv(
                self.grid_vertex_array.grid_program.tex_translation_uniform,
                1,
                ZERO_TRANSLATION.as_ptr(),
            );
            gl::Uniform4fv(
                self.grid_vertex_array.grid_program.gridline_color_uniform,
                1,
                GRIDLINE_COLOR.as_ptr(),
            );
            gl::Uniform4fv(
                self.grid_vertex_array.grid_program.bg_color_uniform,
                1,
                BACKGROUND_COLOR.as_ptr(),
            );
            gl::Uniform2f(
                self.grid_vertex_array.grid_program.sphere_position_uniform,
                self.frame.sphere_position.x,
                self.frame.sphere_position.y,
            );
            gl::Uniform1f(
                self.grid_vertex_array.grid_program.radius_uniform,
                SPHERE_RADIUS,
            );
            gl::Uniform3fv(
                self.grid_vertex_array.grid_program.camera_position_uniform,
                1,
                CAMERA_POSITION.as_ptr(),
            );
            gl::Uniform3fv(
                self.grid_vertex_array.grid_program.light_position_uniform,
                1,
                LIGHT_POSITION.as_ptr(),
            );
            gl::DrawArrays(gl::TRIANGLE_STRIP, 0, 4);
            ck();

            // Draw subscreen.
            let blit_transform: [f32; 4] = [
                SUBSCREEN_WIDTH as f32 / self.window_size.width as f32 * 2.0,
                0.0,
                0.0,
                SUBSCREEN_HEIGHT as f32 / self.window_size.height as f32 * 2.0,
            ];

            let subscreen_translation = Point2D::new(
                self.frame.viewport_origin.x / self.window_size.width as f32 * 2.0 - 1.0,
                self.frame.viewport_origin.y / self.window_size.height as f32 * 2.0 - 1.0,
            );
            gl::BindVertexArray(self.blit_vertex_array.object);
            ck();
            gl::UseProgram(self.blit_vertex_array.blit_program.program.object);
            ck();
            gl::UniformMatrix2fv(
                self.blit_vertex_array.blit_program.transform_uniform,
                1,
                gl::FALSE,
                blit_transform.as_ptr(),
            );
            gl::Uniform2fv(
                self.blit_vertex_array.blit_program.translation_uniform,
                1,
                [subscreen_translation.x, subscreen_translation.y].as_ptr(),
            );
            gl::UniformMatrix2fv(
                self.blit_vertex_array.blit_program.tex_transform_uniform,
                1,
                gl::FALSE,
                IDENTITY_TRANSFORM.as_ptr(),
            );
            gl::Uniform2fv(
                self.blit_vertex_array.blit_program.tex_translation_uniform,
                1,
                ZERO_TRANSLATION.as_ptr(),
            );
            gl::ActiveTexture(gl::TEXTURE0);
            ck();
            gl::BindTexture(
                self.device.surface_gl_texture_target(),
                self.device
                    .surface_texture_object(self.texture.as_ref().unwrap())
                    .map_or(0, |tex| tex.0.get()),
            );
            gl::Uniform1i(self.blit_vertex_array.blit_program.source_uniform, 0);
            ck();
            gl::Enable(gl::BLEND);
            gl::BlendEquation(gl::FUNC_ADD);
            gl::BlendFunc(gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA);
            gl::DrawArrays(gl::TRIANGLE_STRIP, 0, 4);
            ck();
            gl::Disable(gl::BLEND);
        }

        if present {
            let mut surface = self
                .device
                .unbind_surface_from_context(&mut self.context)
                .unwrap()
                .unwrap();
            self.device
                .present_surface(&mut self.context, &mut surface)
                .unwrap();
            self.device
                .bind_surface_to_context(&mut self.context, surface)
                .unwrap();
        }
    }
}

fn worker_thread(
    connection: Connection,
    adapter: Adapter,
    context_descriptor: ContextDescriptor,
    window_size: Size2D<i32>,
    resource_loader: Box<dyn ResourceLoader>,
    worker_to_main_sender: Sender<Frame>,
    worker_from_main_receiver: Receiver<Surface>,
) {
    // Open the device, create a context, and make it current.
    let size = Size2D::new(SUBSCREEN_WIDTH, SUBSCREEN_HEIGHT);
    let surface_type = SurfaceType::Generic { size };
    let device = connection.create_device(&adapter).unwrap();
    let mut context = device.create_context(&context_descriptor, None).unwrap();
    let surface = device
        .create_surface(&context, SurfaceAccess::GPUOnly, surface_type)
        .unwrap();
    device
        .bind_surface_to_context(&mut context, surface)
        .unwrap();
    device.make_context_current(&context).unwrap();

    // Set up GL objects and state.
    let vertex_array = CheckVertexArray::new(
        device.gl_api(),
        device.surface_gl_texture_target(),
        &*resource_loader,
    );

    // Initialize our origin and size.
    let ball_origin = Point2D::new(
        window_size.width as f32 * 0.5 - BALL_WIDTH as f32 * 0.5,
        window_size.height as f32 * 0.65 - BALL_HEIGHT as f32 * 0.5,
    );
    let ball_size = Size2D::new(BALL_WIDTH as f32, BALL_HEIGHT as f32);
    let mut ball_rect = Rect::new(ball_origin, ball_size);
    let mut ball_velocity = Vector2D::new(INITIAL_VELOCITY_X, INITIAL_VELOCITY_Y);
    let subscreen_offset = (Point2D::new(SUBSCREEN_WIDTH as f32, SUBSCREEN_HEIGHT as f32)
        - Point2D::new(BALL_WIDTH as f32, BALL_HEIGHT as f32))
        * 0.5;

    // Initialize our rotation.
    let mut theta_x = INITIAL_ROTATION_X;
    let mut theta_y = INITIAL_ROTATION_Y;
    let mut theta_z = INITIAL_ROTATION_Z;

    // Send an initial surface back to the main thread.
    let surface_type = SurfaceType::Generic { size };
    let surface = Some(
        device
            .create_surface(&context, SurfaceAccess::GPUOnly, surface_type)
            .unwrap(),
    );
    worker_to_main_sender
        .send(Frame {
            surface,
            viewport_origin: ball_rect.origin - subscreen_offset,
            sphere_position: ball_rect.center(),
        })
        .unwrap();

    loop {
        // Render to the surface.
        unsafe {
            let framebuffer_object = device
                .context_surface_info(&context)
                .unwrap()
                .unwrap()
                .framebuffer_object;

            gl::BindFramebuffer(
                gl::FRAMEBUFFER,
                framebuffer_object.map_or(0, |fbo| fbo.0.get()),
            );
            gl::Viewport(0, 0, size.width, size.height);

            gl::ClearColor(0.0, 0.0, 0.0, 1.0);
            gl::Clear(gl::COLOR_BUFFER_BIT);

            gl::BindVertexArray(vertex_array.object);
            ck();
            gl::UseProgram(vertex_array.check_program.program.object);
            ck();
            gl::UniformMatrix2fv(
                vertex_array.check_program.transform_uniform,
                1,
                gl::FALSE,
                NDC_TRANSFORM.as_ptr(),
            );
            gl::Uniform2fv(
                vertex_array.check_program.translation_uniform,
                1,
                NDC_TRANSLATION.as_ptr(),
            );
            gl::UniformMatrix2fv(
                vertex_array.check_program.tex_transform_uniform,
                1,
                gl::FALSE,
                NDC_TRANSFORM.as_ptr(),
            );
            gl::Uniform2fv(
                vertex_array.check_program.tex_translation_uniform,
                1,
                NDC_TRANSLATION.as_ptr(),
            );
            gl::Uniform3fv(
                vertex_array.check_program.rotation_uniform,
                1,
                [theta_x, theta_y, theta_z].as_ptr(),
            );
            gl::Uniform4fv(
                vertex_array.check_program.color_a_uniform,
                1,
                CHECK_COLOR_A.as_ptr(),
            );
            gl::Uniform4fv(
                vertex_array.check_program.color_b_uniform,
                1,
                CHECK_COLOR_B.as_ptr(),
            );
            gl::Uniform2f(
                vertex_array.check_program.viewport_origin_uniform,
                ball_rect.origin.x - subscreen_offset.x,
                ball_rect.origin.y - subscreen_offset.y,
            );
            gl::Uniform1f(vertex_array.check_program.radius_uniform, SPHERE_RADIUS);
            gl::Uniform3fv(
                vertex_array.check_program.camera_position_uniform,
                1,
                CAMERA_POSITION.as_ptr(),
            );
            gl::Uniform3fv(
                vertex_array.check_program.light_position_uniform,
                1,
                LIGHT_POSITION.as_ptr(),
            );
            gl::Uniform2f(
                vertex_array.check_program.sphere_position_uniform,
                ball_rect.center().x,
                ball_rect.center().y,
            );
            gl::DrawArrays(gl::TRIANGLE_STRIP, 0, 4);
            ck();
        }

        let old_surface = device.unbind_surface_from_context(&mut context).unwrap();
        let new_surface = match worker_from_main_receiver.recv() {
            Ok(surface) => surface,
            Err(_) => break,
        };

        device
            .bind_surface_to_context(&mut context, new_surface)
            .unwrap();
        worker_to_main_sender
            .send(Frame {
                surface: old_surface,
                viewport_origin: ball_rect.origin - subscreen_offset,
                sphere_position: ball_rect.center(),
            })
            .unwrap();

        // Advance ball.
        ball_velocity += Vector2D::new(0.0, GRAVITY);
        ball_rect = ball_rect.translate(ball_velocity);

        // Bounce off edges.
        if ball_rect.origin.y <= 0.0 {
            ball_rect.origin.y = 0.0;
            ball_velocity.y = f32::abs(ball_velocity.y);
        }
        if ball_rect.origin.x <= 0.0 {
            ball_rect.origin.x = 0.0;
            ball_velocity.x = f32::abs(ball_velocity.x);
        }
        if ball_rect.max_x() >= window_size.width as f32 {
            ball_rect.origin.x = (window_size.width - BALL_WIDTH) as f32;
            ball_velocity.x = -f32::abs(ball_velocity.x);
        }

        // Rotate.
        theta_x += ROTATION_SPEED_X;
        theta_y += ROTATION_SPEED_Y;
        theta_z += ROTATION_SPEED_Z;
    }

    device.destroy_context(&mut context).unwrap();
}

struct Frame {
    surface: Option<Surface>,
    viewport_origin: Point2D<f32>,
    sphere_position: Point2D<f32>,
}

struct BlitVertexArray {
    object: GLuint,
    blit_program: BlitProgram,
    #[allow(dead_code)]
    position_buffer: Buffer,
}

impl BlitVertexArray {
    fn new(
        gl_api: GLApi,
        gl_texture_target: GLenum,
        resource_loader: &dyn ResourceLoader,
    ) -> BlitVertexArray {
        let blit_program = BlitProgram::new(gl_api, gl_texture_target, resource_loader);
        unsafe {
            let mut vertex_array = 0;
            gl::GenVertexArrays(1, &mut vertex_array);
            ck();
            gl::BindVertexArray(vertex_array);
            ck();

            let position_buffer = Buffer::from_data(&QUAD_VERTEX_POSITIONS);
            gl::BindBuffer(gl::ARRAY_BUFFER, position_buffer.object);
            ck();
            gl::VertexAttribPointer(
                blit_program.position_attribute as GLuint,
                2,
                gl::UNSIGNED_BYTE,
                gl::FALSE,
                2,
                0 as _,
            );
            ck();
            gl::EnableVertexAttribArray(blit_program.position_attribute as GLuint);
            ck();

            BlitVertexArray {
                object: vertex_array,
                blit_program,
                position_buffer,
            }
        }
    }
}

struct GridVertexArray {
    object: GLuint,
    grid_program: GridProgram,
    #[allow(dead_code)]
    position_buffer: Buffer,
}

impl GridVertexArray {
    fn new(
        gl_api: GLApi,
        gl_texture_target: GLenum,
        resource_loader: &dyn ResourceLoader,
    ) -> GridVertexArray {
        let grid_program = GridProgram::new(gl_api, gl_texture_target, resource_loader);
        unsafe {
            let mut vertex_array = 0;
            gl::GenVertexArrays(1, &mut vertex_array);
            ck();
            gl::BindVertexArray(vertex_array);
            ck();

            let position_buffer = Buffer::from_data(&QUAD_VERTEX_POSITIONS);
            gl::BindBuffer(gl::ARRAY_BUFFER, position_buffer.object);
            ck();
            gl::VertexAttribPointer(
                grid_program.position_attribute as GLuint,
                2,
                gl::UNSIGNED_BYTE,
                gl::FALSE,
                2,
                0 as _,
            );
            ck();
            gl::EnableVertexAttribArray(grid_program.position_attribute as GLuint);
            ck();

            GridVertexArray {
                object: vertex_array,
                grid_program,
                position_buffer,
            }
        }
    }
}

struct CheckVertexArray {
    object: GLuint,
    check_program: CheckProgram,
    #[allow(dead_code)]
    position_buffer: Buffer,
}

impl CheckVertexArray {
    fn new(
        gl_api: GLApi,
        gl_texture_target: GLenum,
        resource_loader: &dyn ResourceLoader,
    ) -> CheckVertexArray {
        let check_program = CheckProgram::new(gl_api, gl_texture_target, resource_loader);
        unsafe {
            let mut vertex_array = 0;
            gl::GenVertexArrays(1, &mut vertex_array);
            ck();
            gl::BindVertexArray(vertex_array);
            ck();

            let position_buffer = Buffer::from_data(&QUAD_VERTEX_POSITIONS);
            gl::BindBuffer(gl::ARRAY_BUFFER, position_buffer.object);
            ck();
            gl::VertexAttribPointer(
                check_program.position_attribute as GLuint,
                2,
                gl::UNSIGNED_BYTE,
                gl::FALSE,
                2,
                0 as _,
            );
            ck();
            gl::EnableVertexAttribArray(check_program.position_attribute as GLuint);
            ck();

            CheckVertexArray {
                object: vertex_array,
                check_program,
                position_buffer,
            }
        }
    }
}

struct BlitProgram {
    program: Program,
    position_attribute: GLint,
    transform_uniform: GLint,
    translation_uniform: GLint,
    tex_transform_uniform: GLint,
    tex_translation_uniform: GLint,
    source_uniform: GLint,
}

impl BlitProgram {
    fn new(
        gl_api: GLApi,
        gl_texture_target: GLenum,
        resource_loader: &dyn ResourceLoader,
    ) -> BlitProgram {
        let vertex_shader = Shader::new(
            "quad",
            ShaderKind::Vertex,
            gl_api,
            gl_texture_target,
            resource_loader,
        );
        let fragment_shader = Shader::new(
            "blit",
            ShaderKind::Fragment,
            gl_api,
            gl_texture_target,
            resource_loader,
        );
        let program = Program::new(vertex_shader, fragment_shader);
        unsafe {
            let position_attribute =
                gl::GetAttribLocation(program.object, c"aPosition".as_ptr().cast());
            ck();
            let transform_uniform =
                gl::GetUniformLocation(program.object, c"uTransform".as_ptr().cast());
            ck();
            let translation_uniform =
                gl::GetUniformLocation(program.object, c"uTranslation".as_ptr().cast());
            ck();
            let tex_transform_uniform =
                gl::GetUniformLocation(program.object, c"uTexTransform".as_ptr().cast());
            ck();
            let tex_translation_uniform =
                gl::GetUniformLocation(program.object, c"uTexTranslation".as_ptr().cast());
            ck();
            let source_uniform = gl::GetUniformLocation(program.object, c"uSource".as_ptr().cast());
            ck();
            BlitProgram {
                program,
                position_attribute,
                transform_uniform,
                translation_uniform,
                tex_transform_uniform,
                tex_translation_uniform,
                source_uniform,
            }
        }
    }
}

struct GridProgram {
    program: Program,
    position_attribute: GLint,
    transform_uniform: GLint,
    translation_uniform: GLint,
    tex_transform_uniform: GLint,
    tex_translation_uniform: GLint,
    gridline_color_uniform: GLint,
    bg_color_uniform: GLint,
    radius_uniform: GLint,
    sphere_position_uniform: GLint,
    camera_position_uniform: GLint,
    light_position_uniform: GLint,
}

impl GridProgram {
    fn new(
        gl_api: GLApi,
        gl_texture_target: GLenum,
        resource_loader: &dyn ResourceLoader,
    ) -> GridProgram {
        let vertex_shader = Shader::new(
            "quad",
            ShaderKind::Vertex,
            gl_api,
            gl_texture_target,
            resource_loader,
        );
        let fragment_shader = Shader::new(
            "grid",
            ShaderKind::Fragment,
            gl_api,
            gl_texture_target,
            resource_loader,
        );
        let program = Program::new(vertex_shader, fragment_shader);
        unsafe {
            let position_attribute =
                gl::GetAttribLocation(program.object, c"aPosition".as_ptr().cast());
            ck();
            let transform_uniform =
                gl::GetUniformLocation(program.object, c"uTransform".as_ptr().cast());
            ck();
            let translation_uniform =
                gl::GetUniformLocation(program.object, c"uTranslation".as_ptr().cast());
            ck();
            let tex_transform_uniform =
                gl::GetUniformLocation(program.object, c"uTexTransform".as_ptr().cast());
            ck();
            let tex_translation_uniform =
                gl::GetUniformLocation(program.object, c"uTexTranslation".as_ptr().cast());
            ck();
            let gridline_color_uniform =
                gl::GetUniformLocation(program.object, c"uGridlineColor".as_ptr().cast());
            ck();
            let bg_color_uniform =
                gl::GetUniformLocation(program.object, c"uBGColor".as_ptr().cast());
            ck();
            let radius_uniform = gl::GetUniformLocation(program.object, c"uRadius".as_ptr().cast());
            ck();
            let camera_position_uniform =
                gl::GetUniformLocation(program.object, c"uCameraPosition".as_ptr().cast());
            ck();
            let light_position_uniform =
                gl::GetUniformLocation(program.object, c"uLightPosition".as_ptr().cast());
            ck();
            let sphere_position_uniform =
                gl::GetUniformLocation(program.object, c"uSpherePosition".as_ptr().cast());
            ck();
            GridProgram {
                program,
                position_attribute,
                transform_uniform,
                translation_uniform,
                tex_transform_uniform,
                tex_translation_uniform,
                gridline_color_uniform,
                bg_color_uniform,
                radius_uniform,
                camera_position_uniform,
                light_position_uniform,
                sphere_position_uniform,
            }
        }
    }
}

struct CheckProgram {
    program: Program,
    position_attribute: GLint,
    transform_uniform: GLint,
    translation_uniform: GLint,
    tex_transform_uniform: GLint,
    tex_translation_uniform: GLint,
    rotation_uniform: GLint,
    color_a_uniform: GLint,
    color_b_uniform: GLint,
    viewport_origin_uniform: GLint,
    radius_uniform: GLint,
    camera_position_uniform: GLint,
    light_position_uniform: GLint,
    sphere_position_uniform: GLint,
}

impl CheckProgram {
    fn new(
        gl_api: GLApi,
        gl_texture_target: GLenum,
        resource_loader: &dyn ResourceLoader,
    ) -> CheckProgram {
        let vertex_shader = Shader::new(
            "quad",
            ShaderKind::Vertex,
            gl_api,
            gl_texture_target,
            resource_loader,
        );
        let fragment_shader = Shader::new(
            "check",
            ShaderKind::Fragment,
            gl_api,
            gl_texture_target,
            resource_loader,
        );
        let program = Program::new(vertex_shader, fragment_shader);
        unsafe {
            let position_attribute =
                gl::GetAttribLocation(program.object, c"aPosition".as_ptr().cast());
            ck();
            let transform_uniform =
                gl::GetUniformLocation(program.object, c"uTransform".as_ptr().cast());
            ck();
            let translation_uniform =
                gl::GetUniformLocation(program.object, c"uTranslation".as_ptr().cast());
            ck();
            let tex_transform_uniform =
                gl::GetUniformLocation(program.object, c"uTexTransform".as_ptr().cast());
            ck();
            let tex_translation_uniform =
                gl::GetUniformLocation(program.object, c"uTexTranslation".as_ptr().cast());
            ck();
            let rotation_uniform =
                gl::GetUniformLocation(program.object, c"uRotation".as_ptr().cast());
            ck();
            let color_a_uniform =
                gl::GetUniformLocation(program.object, c"uColorA".as_ptr().cast());
            ck();
            let color_b_uniform =
                gl::GetUniformLocation(program.object, c"uColorB".as_ptr().cast());
            ck();
            let viewport_origin_uniform =
                gl::GetUniformLocation(program.object, c"uViewportOrigin".as_ptr().cast());
            ck();
            let radius_uniform = gl::GetUniformLocation(program.object, c"uRadius".as_ptr().cast());
            ck();
            let camera_position_uniform =
                gl::GetUniformLocation(program.object, c"uCameraPosition".as_ptr().cast());
            ck();
            let light_position_uniform =
                gl::GetUniformLocation(program.object, c"uLightPosition".as_ptr().cast());
            ck();
            let sphere_position_uniform =
                gl::GetUniformLocation(program.object, c"uSpherePosition".as_ptr().cast());
            ck();
            CheckProgram {
                program,
                position_attribute,
                transform_uniform,
                translation_uniform,
                tex_transform_uniform,
                tex_translation_uniform,
                rotation_uniform,
                color_a_uniform,
                color_b_uniform,
                viewport_origin_uniform,
                radius_uniform,
                light_position_uniform,
                camera_position_uniform,
                sphere_position_uniform,
            }
        }
    }
}
