use super::{atlas::AtlasAllocator, image_cache::ImageCache, sprite_cache::SpriteCache};
use crate::{
    color::Color,
    geometry::{
        rect::RectF,
        vector::{vec2f, vec2i, Vector2F},
    },
    platform,
    scene::{Glyph, Icon, Image, ImageGlyph, Layer, Quad, Scene, Shadow, Underline},
};
use cocoa::{
    base::{NO, YES},
    foundation::NSUInteger,
    quartzcore::AutoresizingMask,
};
use core_foundation::base::TCFType;
use foreign_types::ForeignTypeRef;
use log::warn;
use media::core_video::{self, CVMetalTextureCache};
use metal::{CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange};
use objc::{self, msg_send, sel, sel_impl};
use shaders::ToFloat2 as _;
use std::{collections::HashMap, ffi::c_void, iter::Peekable, mem, ptr, sync::Arc, vec};

const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib"));
const INSTANCE_BUFFER_SIZE: usize = 8192 * 1024; // This is an arbitrary decision. There's probably a more optimal value.

pub struct Renderer {
    layer: metal::MetalLayer,
    command_queue: CommandQueue,
    sprite_cache: SpriteCache,
    image_cache: ImageCache,
    path_atlases: AtlasAllocator,
    quad_pipeline_state: metal::RenderPipelineState,
    shadow_pipeline_state: metal::RenderPipelineState,
    sprite_pipeline_state: metal::RenderPipelineState,
    image_pipeline_state: metal::RenderPipelineState,
    surface_pipeline_state: metal::RenderPipelineState,
    path_atlas_pipeline_state: metal::RenderPipelineState,
    underline_pipeline_state: metal::RenderPipelineState,
    unit_vertices: metal::Buffer,
    instances: metal::Buffer,
    cv_texture_cache: core_video::CVMetalTextureCache,
}

struct PathSprite {
    layer_id: usize,
    atlas_id: usize,
    shader_data: shaders::GPUISprite,
}

pub struct Surface {
    pub bounds: RectF,
    pub image_buffer: core_video::CVImageBuffer,
}

impl Renderer {
    pub fn new(is_opaque: bool, fonts: Arc<dyn platform::FontSystem>) -> Self {
        const PIXEL_FORMAT: MTLPixelFormat = MTLPixelFormat::BGRA8Unorm;

        let device: metal::Device = if let Some(device) = metal::Device::system_default() {
            device
        } else {
            log::error!("unable to access a compatible graphics device");
            std::process::exit(1);
        };

        let layer = metal::MetalLayer::new();
        layer.set_device(&device);
        layer.set_pixel_format(PIXEL_FORMAT);
        layer.set_presents_with_transaction(true);
        layer.set_opaque(is_opaque);
        unsafe {
            let _: () = msg_send![&*layer, setAllowsNextDrawableTimeout: NO];
            let _: () = msg_send![&*layer, setNeedsDisplayOnBoundsChange: YES];
            let _: () = msg_send![
                &*layer,
                setAutoresizingMask: AutoresizingMask::WIDTH_SIZABLE
                    | AutoresizingMask::HEIGHT_SIZABLE
            ];
        }

        let library = device
            .new_library_with_data(SHADERS_METALLIB)
            .expect("error building metal library");

        let unit_vertices = [
            (0., 0.).to_float2(),
            (1., 0.).to_float2(),
            (0., 1.).to_float2(),
            (0., 1.).to_float2(),
            (1., 0.).to_float2(),
            (1., 1.).to_float2(),
        ];
        let unit_vertices = device.new_buffer_with_data(
            unit_vertices.as_ptr() as *const c_void,
            (unit_vertices.len() * mem::size_of::<shaders::vector_float2>()) as u64,
            MTLResourceOptions::StorageModeManaged,
        );
        let instances = device.new_buffer(
            INSTANCE_BUFFER_SIZE as u64,
            MTLResourceOptions::StorageModeManaged,
        );

        let sprite_cache = SpriteCache::new(device.clone(), vec2i(1024, 768), 1., fonts.clone());
        let image_cache = ImageCache::new(device.clone(), vec2i(1024, 768), 1., fonts);
        let path_atlases =
            AtlasAllocator::new(device.clone(), build_path_atlas_texture_descriptor());
        let quad_pipeline_state = build_pipeline_state(
            &device,
            &library,
            "quad",
            "quad_vertex",
            "quad_fragment",
            PIXEL_FORMAT,
        );
        let shadow_pipeline_state = build_pipeline_state(
            &device,
            &library,
            "shadow",
            "shadow_vertex",
            "shadow_fragment",
            PIXEL_FORMAT,
        );
        let sprite_pipeline_state = build_pipeline_state(
            &device,
            &library,
            "sprite",
            "sprite_vertex",
            "sprite_fragment",
            PIXEL_FORMAT,
        );
        let image_pipeline_state = build_pipeline_state(
            &device,
            &library,
            "image",
            "image_vertex",
            "image_fragment",
            PIXEL_FORMAT,
        );
        let surface_pipeline_state = build_pipeline_state(
            &device,
            &library,
            "surface",
            "surface_vertex",
            "surface_fragment",
            PIXEL_FORMAT,
        );
        let path_atlas_pipeline_state = build_path_atlas_pipeline_state(
            &device,
            &library,
            "path_atlas",
            "path_atlas_vertex",
            "path_atlas_fragment",
            MTLPixelFormat::R16Float,
        );
        let underline_pipeline_state = build_pipeline_state(
            &device,
            &library,
            "underline",
            "underline_vertex",
            "underline_fragment",
            PIXEL_FORMAT,
        );
        let cv_texture_cache = CVMetalTextureCache::new(device.as_ptr()).unwrap();
        Self {
            layer,
            command_queue: device.new_command_queue(),
            sprite_cache,
            image_cache,
            path_atlases,
            quad_pipeline_state,
            shadow_pipeline_state,
            sprite_pipeline_state,
            image_pipeline_state,
            surface_pipeline_state,
            path_atlas_pipeline_state,
            underline_pipeline_state,
            unit_vertices,
            instances,
            cv_texture_cache,
        }
    }

    pub fn layer(&self) -> &metal::MetalLayerRef {
        &*self.layer
    }

    pub fn render(&mut self, scene: &Scene) {
        let layer = self.layer.clone();
        let drawable_size = layer.drawable_size();
        let drawable = if let Some(drawable) = layer.next_drawable() {
            drawable
        } else {
            log::error!(
                "failed to retrieve next drawable, drawable size: {:?}",
                drawable_size
            );
            return;
        };
        let command_queue = self.command_queue.clone();
        let command_buffer = command_queue.new_command_buffer();

        self.sprite_cache.set_scale_factor(scene.scale_factor());
        self.image_cache.set_scale_factor(scene.scale_factor());

        let mut offset = 0;

        let path_sprites = self.render_path_atlases(scene, &mut offset, command_buffer);
        self.render_layers(
            scene,
            path_sprites,
            &mut offset,
            vec2f(drawable_size.width as f32, drawable_size.height as f32),
            command_buffer,
            drawable.texture(),
        );
        self.instances.did_modify_range(NSRange {
            location: 0,
            length: offset as NSUInteger,
        });
        self.image_cache.finish_frame();

        command_buffer.commit();
        command_buffer.wait_until_completed();
        drawable.present();
    }

    fn render_path_atlases(
        &mut self,
        scene: &Scene,
        offset: &mut usize,
        command_buffer: &metal::CommandBufferRef,
    ) -> Vec<PathSprite> {
        self.path_atlases.clear();
        let mut sprites = Vec::new();
        let mut vertices = Vec::<shaders::GPUIPathVertex>::new();
        let mut current_atlas_id = None;
        for (layer_id, layer) in scene.layers().enumerate() {
            for path in layer.paths() {
                let origin = path.bounds.origin() * scene.scale_factor();
                let size = (path.bounds.size() * scene.scale_factor()).ceil();

                let path_allocation = self.path_atlases.allocate(size.to_i32());
                if path_allocation.is_none() {
                    // Path size was likely zero.
                    warn!("could not allocate path texture of size {:?}", size);
                    continue;
                }
                let (alloc_id, atlas_origin) = path_allocation.unwrap();
                let atlas_origin = atlas_origin.to_f32();
                sprites.push(PathSprite {
                    layer_id,
                    atlas_id: alloc_id.atlas_id,
                    shader_data: shaders::GPUISprite {
                        origin: origin.floor().to_float2(),
                        target_size: size.to_float2(),
                        source_size: size.to_float2(),
                        atlas_origin: atlas_origin.to_float2(),
                        color: path.color.to_uchar4(),
                        compute_winding: 1,
                    },
                });

                if let Some(current_atlas_id) = current_atlas_id {
                    if alloc_id.atlas_id != current_atlas_id {
                        self.render_paths_to_atlas(
                            offset,
                            &vertices,
                            current_atlas_id,
                            command_buffer,
                        );
                        vertices.clear();
                    }
                }

                current_atlas_id = Some(alloc_id.atlas_id);

                for vertex in &path.vertices {
                    let xy_position =
                        (vertex.xy_position - path.bounds.origin()) * scene.scale_factor();
                    vertices.push(shaders::GPUIPathVertex {
                        xy_position: (atlas_origin + xy_position).to_float2(),
                        st_position: vertex.st_position.to_float2(),
                        clip_rect_origin: atlas_origin.to_float2(),
                        clip_rect_size: size.to_float2(),
                    });
                }
            }
        }

        if let Some(atlas_id) = current_atlas_id {
            self.render_paths_to_atlas(offset, &vertices, atlas_id, command_buffer);
        }

        sprites
    }

    fn render_paths_to_atlas(
        &mut self,
        offset: &mut usize,
        vertices: &[shaders::GPUIPathVertex],
        atlas_id: usize,
        command_buffer: &metal::CommandBufferRef,
    ) {
        align_offset(offset);
        let next_offset = *offset + vertices.len() * mem::size_of::<shaders::GPUIPathVertex>();
        assert!(
            next_offset <= INSTANCE_BUFFER_SIZE,
            "instance buffer exhausted"
        );

        let render_pass_descriptor = metal::RenderPassDescriptor::new();
        let color_attachment = render_pass_descriptor
            .color_attachments()
            .object_at(0)
            .unwrap();
        let texture = self.path_atlases.texture(atlas_id).unwrap();
        color_attachment.set_texture(Some(texture));
        color_attachment.set_load_action(metal::MTLLoadAction::Clear);
        color_attachment.set_store_action(metal::MTLStoreAction::Store);
        color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., 1.));

        let path_atlas_command_encoder =
            command_buffer.new_render_command_encoder(render_pass_descriptor);
        path_atlas_command_encoder.set_render_pipeline_state(&self.path_atlas_pipeline_state);
        path_atlas_command_encoder.set_vertex_buffer(
            shaders::GPUIPathAtlasVertexInputIndex_GPUIPathAtlasVertexInputIndexVertices as u64,
            Some(&self.instances),
            *offset as u64,
        );
        path_atlas_command_encoder.set_vertex_bytes(
            shaders::GPUIPathAtlasVertexInputIndex_GPUIPathAtlasVertexInputIndexAtlasSize as u64,
            mem::size_of::<shaders::vector_float2>() as u64,
            [vec2i(texture.width() as i32, texture.height() as i32).to_float2()].as_ptr()
                as *const c_void,
        );

        let buffer_contents = unsafe {
            (self.instances.contents() as *mut u8).add(*offset) as *mut shaders::GPUIPathVertex
        };

        for (ix, vertex) in vertices.iter().enumerate() {
            unsafe {
                *buffer_contents.add(ix) = *vertex;
            }
        }

        path_atlas_command_encoder.draw_primitives(
            metal::MTLPrimitiveType::Triangle,
            0,
            vertices.len() as u64,
        );
        path_atlas_command_encoder.end_encoding();
        *offset = next_offset;
    }

    fn render_layers(
        &mut self,
        scene: &Scene,
        path_sprites: Vec<PathSprite>,
        offset: &mut usize,
        drawable_size: Vector2F,
        command_buffer: &metal::CommandBufferRef,
        output: &metal::TextureRef,
    ) {
        let render_pass_descriptor = metal::RenderPassDescriptor::new();
        let color_attachment = render_pass_descriptor
            .color_attachments()
            .object_at(0)
            .unwrap();
        color_attachment.set_texture(Some(output));
        color_attachment.set_load_action(metal::MTLLoadAction::Clear);
        color_attachment.set_store_action(metal::MTLStoreAction::Store);
        let alpha = if self.layer.is_opaque() { 1. } else { 0. };
        color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., alpha));
        let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor);

        command_encoder.set_viewport(metal::MTLViewport {
            originX: 0.0,
            originY: 0.0,
            width: drawable_size.x() as f64,
            height: drawable_size.y() as f64,
            znear: 0.0,
            zfar: 1.0,
        });

        let scale_factor = scene.scale_factor();
        let mut path_sprites = path_sprites.into_iter().peekable();
        for (layer_id, layer) in scene.layers().enumerate() {
            self.clip(scene, layer, drawable_size, command_encoder);
            self.render_shadows(
                layer.shadows(),
                scale_factor,
                offset,
                drawable_size,
                command_encoder,
            );
            self.render_quads(
                layer.quads(),
                scale_factor,
                offset,
                drawable_size,
                command_encoder,
            );
            self.render_path_sprites(
                layer_id,
                &mut path_sprites,
                offset,
                drawable_size,
                command_encoder,
            );
            self.render_underlines(
                layer.underlines(),
                scale_factor,
                offset,
                drawable_size,
                command_encoder,
            );
            self.render_sprites(
                layer.glyphs(),
                layer.icons(),
                scale_factor,
                offset,
                drawable_size,
                command_encoder,
            );
            self.render_images(
                layer.images(),
                layer.image_glyphs(),
                scale_factor,
                offset,
                drawable_size,
                command_encoder,
            );
            self.render_surfaces(
                layer.surfaces(),
                scale_factor,
                offset,
                drawable_size,
                command_encoder,
            );
        }

        command_encoder.end_encoding();
    }

    fn clip(
        &mut self,
        scene: &Scene,
        layer: &Layer,
        drawable_size: Vector2F,
        command_encoder: &metal::RenderCommandEncoderRef,
    ) {
        let clip_bounds = (layer
            .clip_bounds()
            .unwrap_or_else(|| RectF::new(vec2f(0., 0.), drawable_size / scene.scale_factor()))
            * scene.scale_factor())
        .round();
        command_encoder.set_scissor_rect(metal::MTLScissorRect {
            x: clip_bounds.origin_x() as NSUInteger,
            y: clip_bounds.origin_y() as NSUInteger,
            width: clip_bounds.width() as NSUInteger,
            height: clip_bounds.height() as NSUInteger,
        });
    }

    fn render_shadows(
        &mut self,
        shadows: &[Shadow],
        scale_factor: f32,
        offset: &mut usize,
        drawable_size: Vector2F,
        command_encoder: &metal::RenderCommandEncoderRef,
    ) {
        if shadows.is_empty() {
            return;
        }

        align_offset(offset);
        let next_offset = *offset + shadows.len() * mem::size_of::<shaders::GPUIShadow>();
        assert!(
            next_offset <= INSTANCE_BUFFER_SIZE,
            "instance buffer exhausted"
        );

        command_encoder.set_render_pipeline_state(&self.shadow_pipeline_state);
        command_encoder.set_vertex_buffer(
            shaders::GPUIShadowInputIndex_GPUIShadowInputIndexVertices as u64,
            Some(&self.unit_vertices),
            0,
        );
        command_encoder.set_vertex_buffer(
            shaders::GPUIShadowInputIndex_GPUIShadowInputIndexShadows as u64,
            Some(&self.instances),
            *offset as u64,
        );
        command_encoder.set_vertex_bytes(
            shaders::GPUIShadowInputIndex_GPUIShadowInputIndexUniforms as u64,
            mem::size_of::<shaders::GPUIUniforms>() as u64,
            [shaders::GPUIUniforms {
                viewport_size: drawable_size.to_float2(),
            }]
            .as_ptr() as *const c_void,
        );

        let buffer_contents = unsafe {
            (self.instances.contents() as *mut u8).add(*offset) as *mut shaders::GPUIShadow
        };
        for (ix, shadow) in shadows.iter().enumerate() {
            let shape_bounds = shadow.bounds * scale_factor;
            let corner_radii = shadow.corner_radii * scale_factor;
            let shader_shadow = shaders::GPUIShadow {
                origin: shape_bounds.origin().to_float2(),
                size: shape_bounds.size().to_float2(),
                corner_radius_top_left: corner_radii.top_left,
                corner_radius_top_right: corner_radii.top_right,
                corner_radius_bottom_right: corner_radii.bottom_right,
                corner_radius_bottom_left: corner_radii.bottom_left,
                sigma: shadow.sigma,
                color: shadow.color.to_uchar4(),
            };
            unsafe {
                *(buffer_contents.add(ix)) = shader_shadow;
            }
        }

        command_encoder.draw_primitives_instanced(
            metal::MTLPrimitiveType::Triangle,
            0,
            6,
            shadows.len() as u64,
        );
        *offset = next_offset;
    }

    fn render_quads(
        &mut self,
        quads: &[Quad],
        scale_factor: f32,
        offset: &mut usize,
        drawable_size: Vector2F,
        command_encoder: &metal::RenderCommandEncoderRef,
    ) {
        if quads.is_empty() {
            return;
        }
        align_offset(offset);
        let next_offset = *offset + quads.len() * mem::size_of::<shaders::GPUIQuad>();
        assert!(
            next_offset <= INSTANCE_BUFFER_SIZE,
            "instance buffer exhausted"
        );

        command_encoder.set_render_pipeline_state(&self.quad_pipeline_state);
        command_encoder.set_vertex_buffer(
            shaders::GPUIQuadInputIndex_GPUIQuadInputIndexVertices as u64,
            Some(&self.unit_vertices),
            0,
        );
        command_encoder.set_vertex_buffer(
            shaders::GPUIQuadInputIndex_GPUIQuadInputIndexQuads as u64,
            Some(&self.instances),
            *offset as u64,
        );
        command_encoder.set_vertex_bytes(
            shaders::GPUIQuadInputIndex_GPUIQuadInputIndexUniforms as u64,
            mem::size_of::<shaders::GPUIUniforms>() as u64,
            [shaders::GPUIUniforms {
                viewport_size: drawable_size.to_float2(),
            }]
            .as_ptr() as *const c_void,
        );

        let buffer_contents = unsafe {
            (self.instances.contents() as *mut u8).add(*offset) as *mut shaders::GPUIQuad
        };
        for (ix, quad) in quads.iter().enumerate() {
            let bounds = quad.bounds * scale_factor;
            let shader_quad = shaders::GPUIQuad {
                origin: bounds.origin().round().to_float2(),
                size: bounds.size().round().to_float2(),
                background_color: quad
                    .background
                    .unwrap_or_else(Color::transparent_black)
                    .to_uchar4(),
                border_top: quad.border.top * scale_factor,
                border_right: quad.border.right * scale_factor,
                border_bottom: quad.border.bottom * scale_factor,
                border_left: quad.border.left * scale_factor,
                border_color: quad.border.color.to_uchar4(),
                corner_radius_top_left: quad.corner_radii.top_left * scale_factor,
                corner_radius_top_right: quad.corner_radii.top_right * scale_factor,
                corner_radius_bottom_right: quad.corner_radii.bottom_right * scale_factor,
                corner_radius_bottom_left: quad.corner_radii.bottom_left * scale_factor,
            };
            unsafe {
                *(buffer_contents.add(ix)) = shader_quad;
            }
        }

        command_encoder.draw_primitives_instanced(
            metal::MTLPrimitiveType::Triangle,
            0,
            6,
            quads.len() as u64,
        );
        *offset = next_offset;
    }

    fn render_sprites(
        &mut self,
        glyphs: &[Glyph],
        icons: &[Icon],
        scale_factor: f32,
        offset: &mut usize,
        drawable_size: Vector2F,
        command_encoder: &metal::RenderCommandEncoderRef,
    ) {
        if glyphs.is_empty() && icons.is_empty() {
            return;
        }

        let mut sprites_by_atlas = HashMap::new();

        for glyph in glyphs {
            if let Some(sprite) = self.sprite_cache.render_glyph(
                glyph.font_id,
                glyph.font_size,
                glyph.id,
                glyph.origin,
            ) {
                // Snap sprite to pixel grid.
                let origin = (glyph.origin * scale_factor).floor() + sprite.offset.to_f32();
                sprites_by_atlas
                    .entry(sprite.atlas_id)
                    .or_insert_with(Vec::new)
                    .push(shaders::GPUISprite {
                        origin: origin.to_float2(),
                        target_size: sprite.size.to_float2(),
                        source_size: sprite.size.to_float2(),
                        atlas_origin: sprite.atlas_origin.to_float2(),
                        color: glyph.color.to_uchar4(),
                        compute_winding: 0,
                    });
            }
        }

        for icon in icons {
            // Snap sprite to pixel grid.
            let origin = (icon.bounds.origin() * scale_factor).floor();
            let target_size = (icon.bounds.size() * scale_factor).ceil();
            let source_size = (target_size * 2.).to_i32();

            let sprite =
                self.sprite_cache
                    .render_icon(source_size, icon.path.clone(), icon.svg.clone());
            if sprite.is_none() {
                continue;
            }
            let sprite = sprite.unwrap();

            sprites_by_atlas
                .entry(sprite.atlas_id)
                .or_insert_with(Vec::new)
                .push(shaders::GPUISprite {
                    origin: origin.to_float2(),
                    target_size: target_size.to_float2(),
                    source_size: sprite.size.to_float2(),
                    atlas_origin: sprite.atlas_origin.to_float2(),
                    color: icon.color.to_uchar4(),
                    compute_winding: 0,
                });
        }

        command_encoder.set_render_pipeline_state(&self.sprite_pipeline_state);
        command_encoder.set_vertex_buffer(
            shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexVertices as u64,
            Some(&self.unit_vertices),
            0,
        );
        command_encoder.set_vertex_bytes(
            shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexViewportSize as u64,
            mem::size_of::<shaders::vector_float2>() as u64,
            [drawable_size.to_float2()].as_ptr() as *const c_void,
        );

        for (atlas_id, sprites) in sprites_by_atlas {
            align_offset(offset);
            let next_offset = *offset + sprites.len() * mem::size_of::<shaders::GPUISprite>();
            assert!(
                next_offset <= INSTANCE_BUFFER_SIZE,
                "instance buffer exhausted"
            );

            let texture = self.sprite_cache.atlas_texture(atlas_id).unwrap();
            command_encoder.set_vertex_buffer(
                shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexSprites as u64,
                Some(&self.instances),
                *offset as u64,
            );
            command_encoder.set_vertex_bytes(
                shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexAtlasSize as u64,
                mem::size_of::<shaders::vector_float2>() as u64,
                [vec2i(texture.width() as i32, texture.height() as i32).to_float2()].as_ptr()
                    as *const c_void,
            );

            command_encoder.set_fragment_texture(
                shaders::GPUISpriteFragmentInputIndex_GPUISpriteFragmentInputIndexAtlas as u64,
                Some(texture),
            );

            unsafe {
                let buffer_contents =
                    (self.instances.contents() as *mut u8).add(*offset) as *mut shaders::GPUISprite;
                std::ptr::copy_nonoverlapping(sprites.as_ptr(), buffer_contents, sprites.len());
            }

            command_encoder.draw_primitives_instanced(
                metal::MTLPrimitiveType::Triangle,
                0,
                6,
                sprites.len() as u64,
            );
            *offset = next_offset;
        }
    }

    fn render_images(
        &mut self,
        images: &[Image],
        image_glyphs: &[ImageGlyph],
        scale_factor: f32,
        offset: &mut usize,
        drawable_size: Vector2F,
        command_encoder: &metal::RenderCommandEncoderRef,
    ) {
        if images.is_empty() && image_glyphs.is_empty() {
            return;
        }

        let mut images_by_atlas = HashMap::new();
        for image in images {
            let origin = image.bounds.origin() * scale_factor;
            let target_size = image.bounds.size() * scale_factor;
            let corner_radii = image.corner_radii * scale_factor;
            let (alloc_id, atlas_bounds) = self.image_cache.render(&image.data);
            images_by_atlas
                .entry(alloc_id.atlas_id)
                .or_insert_with(Vec::new)
                .push(shaders::GPUIImage {
                    origin: origin.to_float2(),
                    target_size: target_size.to_float2(),
                    source_size: atlas_bounds.size().to_float2(),
                    atlas_origin: atlas_bounds.origin().to_float2(),
                    border_top: image.border.top * scale_factor,
                    border_right: image.border.right * scale_factor,
                    border_bottom: image.border.bottom * scale_factor,
                    border_left: image.border.left * scale_factor,
                    border_color: image.border.color.to_uchar4(),
                    corner_radius_top_left: corner_radii.top_left,
                    corner_radius_top_right: corner_radii.top_right,
                    corner_radius_bottom_right: corner_radii.bottom_right,
                    corner_radius_bottom_left: corner_radii.bottom_left,
                    grayscale: image.grayscale as u8,
                });
        }

        for image_glyph in image_glyphs {
            let origin = (image_glyph.origin * scale_factor).floor();
            if let Some((alloc_id, atlas_bounds, glyph_origin)) =
                self.image_cache.render_glyph(image_glyph)
            {
                images_by_atlas
                    .entry(alloc_id.atlas_id)
                    .or_insert_with(Vec::new)
                    .push(shaders::GPUIImage {
                        origin: (origin + glyph_origin.to_f32()).to_float2(),
                        target_size: atlas_bounds.size().to_float2(),
                        source_size: atlas_bounds.size().to_float2(),
                        atlas_origin: atlas_bounds.origin().to_float2(),
                        border_top: 0.,
                        border_right: 0.,
                        border_bottom: 0.,
                        border_left: 0.,
                        border_color: Default::default(),
                        corner_radius_top_left: 0.,
                        corner_radius_top_right: 0.,
                        corner_radius_bottom_right: 0.,
                        corner_radius_bottom_left: 0.,
                        grayscale: false as u8,
                    });
            } else {
                log::warn!("could not render glyph with id {}", image_glyph.id);
            }
        }

        command_encoder.set_render_pipeline_state(&self.image_pipeline_state);
        command_encoder.set_vertex_buffer(
            shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexVertices as u64,
            Some(&self.unit_vertices),
            0,
        );
        command_encoder.set_vertex_bytes(
            shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexViewportSize as u64,
            mem::size_of::<shaders::vector_float2>() as u64,
            [drawable_size.to_float2()].as_ptr() as *const c_void,
        );

        for (atlas_id, images) in images_by_atlas {
            align_offset(offset);
            let next_offset = *offset + images.len() * mem::size_of::<shaders::GPUIImage>();
            assert!(
                next_offset <= INSTANCE_BUFFER_SIZE,
                "instance buffer exhausted"
            );

            let texture = self.image_cache.atlas_texture(atlas_id).unwrap();
            command_encoder.set_vertex_buffer(
                shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexImages as u64,
                Some(&self.instances),
                *offset as u64,
            );
            command_encoder.set_vertex_bytes(
                shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexAtlasSize as u64,
                mem::size_of::<shaders::vector_float2>() as u64,
                [vec2i(texture.width() as i32, texture.height() as i32).to_float2()].as_ptr()
                    as *const c_void,
            );
            command_encoder.set_fragment_texture(
                shaders::GPUIImageFragmentInputIndex_GPUIImageFragmentInputIndexAtlas as u64,
                Some(texture),
            );

            unsafe {
                let buffer_contents =
                    (self.instances.contents() as *mut u8).add(*offset) as *mut shaders::GPUIImage;
                std::ptr::copy_nonoverlapping(images.as_ptr(), buffer_contents, images.len());
            }

            command_encoder.draw_primitives_instanced(
                metal::MTLPrimitiveType::Triangle,
                0,
                6,
                images.len() as u64,
            );
            *offset = next_offset;
        }
    }

    fn render_surfaces(
        &mut self,
        surfaces: &[Surface],
        scale_factor: f32,
        offset: &mut usize,
        drawable_size: Vector2F,
        command_encoder: &metal::RenderCommandEncoderRef,
    ) {
        if surfaces.is_empty() {
            return;
        }

        command_encoder.set_render_pipeline_state(&self.surface_pipeline_state);
        command_encoder.set_vertex_buffer(
            shaders::GPUISurfaceVertexInputIndex_GPUISurfaceVertexInputIndexVertices as u64,
            Some(&self.unit_vertices),
            0,
        );
        command_encoder.set_vertex_bytes(
            shaders::GPUISurfaceVertexInputIndex_GPUISurfaceVertexInputIndexViewportSize as u64,
            mem::size_of::<shaders::vector_float2>() as u64,
            [drawable_size.to_float2()].as_ptr() as *const c_void,
        );

        for surface in surfaces {
            let origin = surface.bounds.origin() * scale_factor;
            let source_size = vec2i(
                surface.image_buffer.width() as i32,
                surface.image_buffer.height() as i32,
            );
            let target_size = surface.bounds.size() * scale_factor;

            assert_eq!(
                surface.image_buffer.pixel_format_type(),
                core_video::kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
            );

            let y_texture = self
                .cv_texture_cache
                .create_texture_from_image(
                    surface.image_buffer.as_concrete_TypeRef(),
                    ptr::null(),
                    MTLPixelFormat::R8Unorm,
                    surface.image_buffer.plane_width(0),
                    surface.image_buffer.plane_height(0),
                    0,
                )
                .unwrap();
            let cb_cr_texture = self
                .cv_texture_cache
                .create_texture_from_image(
                    surface.image_buffer.as_concrete_TypeRef(),
                    ptr::null(),
                    MTLPixelFormat::RG8Unorm,
                    surface.image_buffer.plane_width(1),
                    surface.image_buffer.plane_height(1),
                    1,
                )
                .unwrap();

            align_offset(offset);
            let next_offset = *offset + mem::size_of::<shaders::GPUISurface>();
            assert!(
                next_offset <= INSTANCE_BUFFER_SIZE,
                "instance buffer exhausted"
            );

            command_encoder.set_vertex_buffer(
                shaders::GPUISurfaceVertexInputIndex_GPUISurfaceVertexInputIndexSurfaces as u64,
                Some(&self.instances),
                *offset as u64,
            );
            command_encoder.set_vertex_bytes(
                shaders::GPUISurfaceVertexInputIndex_GPUISurfaceVertexInputIndexAtlasSize as u64,
                mem::size_of::<shaders::vector_float2>() as u64,
                [source_size.to_float2()].as_ptr() as *const c_void,
            );
            command_encoder.set_fragment_texture(
                shaders::GPUISurfaceFragmentInputIndex_GPUISurfaceFragmentInputIndexYAtlas as u64,
                Some(y_texture.as_texture_ref()),
            );
            command_encoder.set_fragment_texture(
                shaders::GPUISurfaceFragmentInputIndex_GPUISurfaceFragmentInputIndexCbCrAtlas
                    as u64,
                Some(cb_cr_texture.as_texture_ref()),
            );

            unsafe {
                let buffer_contents = (self.instances.contents() as *mut u8).add(*offset)
                    as *mut shaders::GPUISurface;
                std::ptr::write(
                    buffer_contents,
                    shaders::GPUISurface {
                        origin: origin.to_float2(),
                        target_size: target_size.to_float2(),
                        source_size: source_size.to_float2(),
                    },
                );
            }

            command_encoder.draw_primitives(metal::MTLPrimitiveType::Triangle, 0, 6);
            *offset = next_offset;
        }
    }

    fn render_path_sprites(
        &mut self,
        layer_id: usize,
        sprites: &mut Peekable<vec::IntoIter<PathSprite>>,
        offset: &mut usize,
        drawable_size: Vector2F,
        command_encoder: &metal::RenderCommandEncoderRef,
    ) {
        command_encoder.set_render_pipeline_state(&self.sprite_pipeline_state);
        command_encoder.set_vertex_buffer(
            shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexVertices as u64,
            Some(&self.unit_vertices),
            0,
        );
        command_encoder.set_vertex_bytes(
            shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexViewportSize as u64,
            mem::size_of::<shaders::vector_float2>() as u64,
            [drawable_size.to_float2()].as_ptr() as *const c_void,
        );

        let mut atlas_id = None;
        let mut atlas_sprite_count = 0;
        align_offset(offset);

        while let Some(sprite) = sprites.peek() {
            if sprite.layer_id != layer_id {
                break;
            }

            let sprite = sprites.next().unwrap();
            if let Some(atlas_id) = atlas_id.as_mut() {
                if sprite.atlas_id != *atlas_id {
                    self.render_path_sprites_for_atlas(
                        offset,
                        *atlas_id,
                        atlas_sprite_count,
                        command_encoder,
                    );

                    *atlas_id = sprite.atlas_id;
                    atlas_sprite_count = 0;
                    align_offset(offset);
                }
            } else {
                atlas_id = Some(sprite.atlas_id);
            }

            unsafe {
                let buffer_contents =
                    (self.instances.contents() as *mut u8).add(*offset) as *mut shaders::GPUISprite;
                *buffer_contents.add(atlas_sprite_count) = sprite.shader_data;
            }

            atlas_sprite_count += 1;
        }

        if let Some(atlas_id) = atlas_id {
            self.render_path_sprites_for_atlas(
                offset,
                atlas_id,
                atlas_sprite_count,
                command_encoder,
            );
        }
    }

    fn render_path_sprites_for_atlas(
        &mut self,
        offset: &mut usize,
        atlas_id: usize,
        sprite_count: usize,
        command_encoder: &metal::RenderCommandEncoderRef,
    ) {
        let next_offset = *offset + sprite_count * mem::size_of::<shaders::GPUISprite>();
        assert!(
            next_offset <= INSTANCE_BUFFER_SIZE,
            "instance buffer exhausted"
        );
        command_encoder.set_vertex_buffer(
            shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexSprites as u64,
            Some(&self.instances),
            *offset as u64,
        );
        let texture = self.path_atlases.texture(atlas_id).unwrap();
        command_encoder.set_fragment_texture(
            shaders::GPUISpriteFragmentInputIndex_GPUISpriteFragmentInputIndexAtlas as u64,
            Some(texture),
        );
        command_encoder.set_vertex_bytes(
            shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexAtlasSize as u64,
            mem::size_of::<shaders::vector_float2>() as u64,
            [vec2i(texture.width() as i32, texture.height() as i32).to_float2()].as_ptr()
                as *const c_void,
        );

        command_encoder.draw_primitives_instanced(
            metal::MTLPrimitiveType::Triangle,
            0,
            6,
            sprite_count as u64,
        );
        *offset = next_offset;
    }

    fn render_underlines(
        &mut self,
        underlines: &[Underline],
        scale_factor: f32,
        offset: &mut usize,
        drawable_size: Vector2F,
        command_encoder: &metal::RenderCommandEncoderRef,
    ) {
        if underlines.is_empty() {
            return;
        }
        align_offset(offset);
        let next_offset = *offset + underlines.len() * mem::size_of::<shaders::GPUIUnderline>();
        assert!(
            next_offset <= INSTANCE_BUFFER_SIZE,
            "instance buffer exhausted"
        );

        command_encoder.set_render_pipeline_state(&self.underline_pipeline_state);
        command_encoder.set_vertex_buffer(
            shaders::GPUIUnderlineInputIndex_GPUIUnderlineInputIndexVertices as u64,
            Some(&self.unit_vertices),
            0,
        );
        command_encoder.set_vertex_buffer(
            shaders::GPUIUnderlineInputIndex_GPUIUnderlineInputIndexUnderlines as u64,
            Some(&self.instances),
            *offset as u64,
        );
        command_encoder.set_vertex_bytes(
            shaders::GPUIUnderlineInputIndex_GPUIUnderlineInputIndexUniforms as u64,
            mem::size_of::<shaders::GPUIUniforms>() as u64,
            [shaders::GPUIUniforms {
                viewport_size: drawable_size.to_float2(),
            }]
            .as_ptr() as *const c_void,
        );

        let buffer_contents = unsafe {
            (self.instances.contents() as *mut u8).add(*offset) as *mut shaders::GPUIUnderline
        };
        for (ix, underline) in underlines.iter().enumerate() {
            let origin = underline.origin * scale_factor;
            let mut height = underline.thickness;
            if underline.squiggly {
                height *= 3.;
            }
            let size = vec2f(underline.width, height) * scale_factor;
            let shader_underline = shaders::GPUIUnderline {
                origin: origin.round().to_float2(),
                size: size.round().to_float2(),
                thickness: underline.thickness * scale_factor,
                color: underline.color.to_uchar4(),
                squiggly: underline.squiggly as u8,
            };
            unsafe {
                *(buffer_contents.add(ix)) = shader_underline;
            }
        }

        command_encoder.draw_primitives_instanced(
            metal::MTLPrimitiveType::Triangle,
            0,
            6,
            underlines.len() as u64,
        );
        *offset = next_offset;
    }
}

fn build_path_atlas_texture_descriptor() -> metal::TextureDescriptor {
    let texture_descriptor = metal::TextureDescriptor::new();
    texture_descriptor.set_width(2048);
    texture_descriptor.set_height(2048);
    texture_descriptor.set_pixel_format(MTLPixelFormat::R16Float);
    texture_descriptor
        .set_usage(metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead);
    texture_descriptor.set_storage_mode(metal::MTLStorageMode::Private);
    texture_descriptor
}

fn align_offset(offset: &mut usize) {
    let r = *offset % 256;
    if r > 0 {
        *offset += 256 - r; // Align to a multiple of 256 to make Metal happy
    }
}

fn build_pipeline_state(
    device: &metal::DeviceRef,
    library: &metal::LibraryRef,
    label: &str,
    vertex_fn_name: &str,
    fragment_fn_name: &str,
    pixel_format: metal::MTLPixelFormat,
) -> metal::RenderPipelineState {
    let vertex_fn = library
        .get_function(vertex_fn_name, None)
        .expect("error locating vertex function");
    let fragment_fn = library
        .get_function(fragment_fn_name, None)
        .expect("error locating fragment function");

    let descriptor = metal::RenderPipelineDescriptor::new();
    descriptor.set_label(label);
    descriptor.set_vertex_function(Some(vertex_fn.as_ref()));
    descriptor.set_fragment_function(Some(fragment_fn.as_ref()));
    let color_attachment = descriptor.color_attachments().object_at(0).unwrap();
    color_attachment.set_pixel_format(pixel_format);
    color_attachment.set_blending_enabled(true);
    color_attachment.set_rgb_blend_operation(metal::MTLBlendOperation::Add);
    color_attachment.set_alpha_blend_operation(metal::MTLBlendOperation::Add);
    color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::SourceAlpha);
    color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One);
    color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha);
    color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::One);

    device
        .new_render_pipeline_state(&descriptor)
        .expect("could not create render pipeline state")
}

fn build_path_atlas_pipeline_state(
    device: &metal::DeviceRef,
    library: &metal::LibraryRef,
    label: &str,
    vertex_fn_name: &str,
    fragment_fn_name: &str,
    pixel_format: metal::MTLPixelFormat,
) -> metal::RenderPipelineState {
    let vertex_fn = library
        .get_function(vertex_fn_name, None)
        .expect("error locating vertex function");
    let fragment_fn = library
        .get_function(fragment_fn_name, None)
        .expect("error locating fragment function");

    let descriptor = metal::RenderPipelineDescriptor::new();
    descriptor.set_label(label);
    descriptor.set_vertex_function(Some(vertex_fn.as_ref()));
    descriptor.set_fragment_function(Some(fragment_fn.as_ref()));
    let color_attachment = descriptor.color_attachments().object_at(0).unwrap();
    color_attachment.set_pixel_format(pixel_format);
    color_attachment.set_blending_enabled(true);
    color_attachment.set_rgb_blend_operation(metal::MTLBlendOperation::Add);
    color_attachment.set_alpha_blend_operation(metal::MTLBlendOperation::Add);
    color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::One);
    color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One);
    color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::One);
    color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::One);

    device
        .new_render_pipeline_state(&descriptor)
        .expect("could not create render pipeline state")
}

mod shaders {
    #![allow(non_upper_case_globals)]
    #![allow(non_camel_case_types)]
    #![allow(non_snake_case)]

    use crate::{
        color::Color,
        geometry::vector::{Vector2F, Vector2I},
    };
    use std::mem;

    include!(concat!(env!("OUT_DIR"), "/shaders.rs"));

    pub trait ToFloat2 {
        fn to_float2(&self) -> vector_float2;
    }

    impl ToFloat2 for (f32, f32) {
        fn to_float2(&self) -> vector_float2 {
            unsafe {
                let mut output = mem::transmute::<_, u32>(self.1.to_bits()) as vector_float2;
                output <<= 32;
                output |= mem::transmute::<_, u32>(self.0.to_bits()) as vector_float2;
                output
            }
        }
    }

    impl ToFloat2 for Vector2F {
        fn to_float2(&self) -> vector_float2 {
            unsafe {
                let mut output = mem::transmute::<_, u32>(self.y().to_bits()) as vector_float2;
                output <<= 32;
                output |= mem::transmute::<_, u32>(self.x().to_bits()) as vector_float2;
                output
            }
        }
    }

    impl ToFloat2 for Vector2I {
        fn to_float2(&self) -> vector_float2 {
            self.to_f32().to_float2()
        }
    }

    impl Color {
        pub fn to_uchar4(&self) -> vector_uchar4 {
            let mut vec = self.a as vector_uchar4;
            vec <<= 8;
            vec |= self.b as vector_uchar4;
            vec <<= 8;
            vec |= self.g as vector_uchar4;
            vec <<= 8;
            vec |= self.r as vector_uchar4;
            vec
        }
    }
}
