/* 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 http://mozilla.org/MPL/2.0/. */

use euclid::SideOffsets2D;
use gleam::gl;
use image::GenericImageView;
use crate::parse_function::parse_function;
use crate::premultiply::premultiply;
use std::collections::HashMap;
use std::convert::TryInto;
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::usize;
use webrender::api::*;
use webrender::render_api::*;
use webrender::api::units::*;
use webrender::api::FillRule;
use crate::wrench::{FontDescriptor, Wrench, WrenchThing, DisplayList};
use crate::yaml_helper::{StringEnum, YamlHelper, make_perspective};
use yaml_rust::{Yaml, YamlLoader};
use crate::PLATFORM_DEFAULT_FACE_NAME;

macro_rules! try_intersect {
    ($first: expr, $second: expr) => {
        if let Some(rect) = ($first).intersection($second) {
            rect
        } else {
            warn!("skipping item with non-intersecting bounds and clip_rect");
            return;
        }
    }
}

fn rsrc_path(item: &Yaml, aux_dir: &Path) -> PathBuf {
    let filename = item.as_str().unwrap();
    let mut file = aux_dir.to_path_buf();
    file.push(filename);
    file
}

impl FontDescriptor {
    fn from_yaml(item: &Yaml, aux_dir: &Path) -> FontDescriptor {
        if !item["family"].is_badvalue() {
            FontDescriptor::Properties {
                family: item["family"].as_str().unwrap().to_owned(),
                weight: item["weight"].as_i64().unwrap_or(400) as u32,
                style: item["style"].as_i64().unwrap_or(0) as u32,
                stretch: item["stretch"].as_i64().unwrap_or(5) as u32,
            }
        } else if !item["font"].is_badvalue() {
            let path = rsrc_path(&item["font"], aux_dir);
            FontDescriptor::Path {
                path,
                font_index: item["font-index"].as_i64().unwrap_or(0) as u32,
            }
        } else {
            FontDescriptor::Family {
                name: PLATFORM_DEFAULT_FACE_NAME.to_string(),
            }
        }
    }
}

struct LocalExternalImageHandler {
    texture_ids: Vec<(gl::GLuint, ImageDescriptor)>,
}

impl LocalExternalImageHandler {
    pub fn new() -> LocalExternalImageHandler {
        LocalExternalImageHandler {
            texture_ids: Vec::new(),
        }
    }

    fn init_gl_texture(
        id: gl::GLuint,
        gl_target: gl::GLuint,
        format_desc: webrender::FormatDesc,
        width: gl::GLint,
        height: gl::GLint,
        bytes: &[u8],
        gl: &dyn gl::Gl,
    ) {
        gl.bind_texture(gl_target, id);
        gl.tex_parameter_i(gl_target, gl::TEXTURE_MAG_FILTER, gl::LINEAR as gl::GLint);
        gl.tex_parameter_i(gl_target, gl::TEXTURE_MIN_FILTER, gl::LINEAR as gl::GLint);
        gl.tex_parameter_i(gl_target, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as gl::GLint);
        gl.tex_parameter_i(gl_target, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as gl::GLint);
        gl.tex_image_2d(
            gl_target,
            0,
            format_desc.internal as gl::GLint,
            width,
            height,
            0,
            format_desc.external,
            format_desc.pixel_type,
            Some(bytes),
        );
        gl.bind_texture(gl_target, 0);
    }

    pub fn add_image(&mut self,
        device: &webrender::Device,
        desc: ImageDescriptor,
        target: ImageBufferKind,
        image_data: ImageData,
    ) -> ImageData {
        let (image_id, channel_idx) = match image_data {
            ImageData::Raw(ref data) => {
                let gl = device.gl();
                let texture_ids = gl.gen_textures(1);
                let format_desc = if desc.format == ImageFormat::BGRA8 {
                    // Force BGRA8 data to RGBA8 layout to avoid potential
                    // need for usage of texture-swizzle.
                    webrender::FormatDesc {
                        external: gl::BGRA,
                        .. device.gl_describe_format(ImageFormat::RGBA8)
                    }
                } else {
                    device.gl_describe_format(desc.format)
                };

                LocalExternalImageHandler::init_gl_texture(
                    texture_ids[0],
                    webrender::get_gl_target(target),
                    format_desc,
                    desc.size.width as gl::GLint,
                    desc.size.height as gl::GLint,
                    data,
                    gl,
                );
                self.texture_ids.push((texture_ids[0], desc));
                (ExternalImageId((self.texture_ids.len() - 1) as u64), 0)
            },
            _ => panic!("unsupported!"),
        };

        ImageData::External(
            ExternalImageData {
                id: image_id,
                channel_index: channel_idx,
                image_type: ExternalImageType::TextureHandle(target),
                normalized_uvs: false,
            }
        )
    }
}

impl ExternalImageHandler for LocalExternalImageHandler {
    fn lock(
        &mut self,
        key: ExternalImageId,
        _channel_index: u8,
        _is_composited: bool,
    ) -> ExternalImage<'_> {
        let (id, desc) = self.texture_ids[key.0 as usize];
        ExternalImage {
            uv: TexelRect::new(0.0, 0.0, desc.size.width as f32, desc.size.height as f32),
            source: ExternalImageSource::NativeTexture(id),
        }
    }
    fn unlock(&mut self, _key: ExternalImageId, _channel_index: u8) {}
}

fn broadcast<T: Clone>(base_vals: &[T], num_items: usize) -> Vec<T> {
    if base_vals.len() == num_items {
        return base_vals.to_vec();
    }

    assert_eq!(
        num_items % base_vals.len(),
        0,
        "Cannot broadcast {} elements into {}",
        base_vals.len(),
        num_items
    );

    let mut vals = vec![];
    loop {
        if vals.len() == num_items {
            break;
        }
        vals.extend_from_slice(base_vals);
    }
    vals
}

enum CheckerboardKind {
    BlackGrey,
    BlackTransparent,
}

fn generate_checkerboard_image(
    border: u32,
    tile_x_size: u32,
    tile_y_size: u32,
    tile_x_count: u32,
    tile_y_count: u32,
    kind: CheckerboardKind,
) -> (ImageDescriptor, ImageData) {
    let width = 2 * border + tile_x_size * tile_x_count;
    let height = 2 * border + tile_y_size * tile_y_count;
    let mut pixels = Vec::new();

    for y in 0 .. height {
        for x in 0 .. width {
            if y < border || y >= (height - border) ||
               x < border || x >= (width - border) {
                pixels.push(0);
                pixels.push(0);
                pixels.push(0xff);
                pixels.push(0xff);
            } else {
                let xon = ((x - border) % (2 * tile_x_size)) < tile_x_size;
                let yon = ((y - border) % (2 * tile_y_size)) < tile_y_size;
                match kind {
                    CheckerboardKind::BlackGrey => {
                        let value = if xon ^ yon { 0xff } else { 0x7f };
                        pixels.push(value);
                        pixels.push(value);
                        pixels.push(value);
                        pixels.push(0xff);
                    }
                    CheckerboardKind::BlackTransparent => {
                        let value = if xon ^ yon { 0xff } else { 0x00 };
                        pixels.push(value);
                        pixels.push(value);
                        pixels.push(value);
                        pixels.push(value);
                    }
                }
            }
        }
    }

    let flags = match kind {
        CheckerboardKind::BlackGrey => ImageDescriptorFlags::IS_OPAQUE,
        CheckerboardKind::BlackTransparent => ImageDescriptorFlags::empty(),
    };

    (
        ImageDescriptor::new(width as i32, height as i32, ImageFormat::BGRA8, flags),
        ImageData::new(pixels),
    )
}

fn generate_xy_gradient_image(w: u32, h: u32) -> (ImageDescriptor, ImageData) {
    let mut pixels = Vec::with_capacity((w * h * 4) as usize);
    for y in 0 .. h {
        for x in 0 .. w {
            let grid = if x % 100 < 3 || y % 100 < 3 { 0.9 } else { 1.0 };
            pixels.push((y as f32 / h as f32 * 255.0 * grid) as u8);
            pixels.push(0);
            pixels.push((x as f32 / w as f32 * 255.0 * grid) as u8);
            pixels.push(255);
        }
    }

    (
        ImageDescriptor::new(w as i32, h as i32, ImageFormat::BGRA8, ImageDescriptorFlags::IS_OPAQUE),
        ImageData::new(pixels),
    )
}

fn generate_solid_color_image(
    r: u8,
    g: u8,
    b: u8,
    a: u8,
    w: u32,
    h: u32,
) -> (ImageDescriptor, ImageData) {
    let num_pixels: usize = (w * h).try_into().unwrap();
    let pixels = [b, g, r, a].repeat(num_pixels);

    let mut flags = ImageDescriptorFlags::empty();
    if a == 255 {
        flags |= ImageDescriptorFlags::IS_OPAQUE;
    }

    (
        ImageDescriptor::new(w as i32, h as i32, ImageFormat::BGRA8, flags),
        ImageData::new(pixels),
    )
}



fn is_image_opaque(format: ImageFormat, bytes: &[u8]) -> bool {
    match format {
        ImageFormat::BGRA8 |
        ImageFormat::RGBA8 => {
            let mut is_opaque = true;
            for i in 0 .. (bytes.len() / 4) {
                if bytes[i * 4 + 3] != 255 {
                    is_opaque = false;
                    break;
                }
            }
            is_opaque
        }
        ImageFormat::RG8 => true,
        ImageFormat::RG16 => true,
        ImageFormat::R8 => false,
        ImageFormat::R16 => false,
        ImageFormat::RGBAF32 |
        ImageFormat::RGBAI32 => unreachable!(),
    }
}

struct IsRoot(bool);

pub struct Snapshot {
    key: SnapshotImageKey,
    size: LayoutSize,
}

pub struct YamlFrameReader {
    yaml_path: PathBuf,
    aux_dir: PathBuf,
    frame_count: u32,

    display_lists: Vec<DisplayList>,

    watch_source: bool,
    list_resources: bool,

    /// A HashMap of offsets which specify what scroll offsets particular
    /// scroll layers should be initialized with.
    scroll_offsets: HashMap<ExternalScrollId, Vec<SampledScrollOffset>>,
    next_external_scroll_id: u64,

    image_map: HashMap<(PathBuf, Option<i64>), (ImageKey, LayoutSize)>,

    fonts: HashMap<FontDescriptor, FontKey>,
    font_instances: HashMap<(FontKey, FontSize, FontInstanceFlags, SyntheticItalics), FontInstanceKey>,
    font_render_mode: Option<FontRenderMode>,
    snapshots: HashMap<String, Snapshot>,
    allow_mipmaps: bool,

    /// A HashMap that allows specifying a numeric id for clip and clip chains in YAML
    /// and having each of those ids correspond to a unique ClipId.
    user_clip_id_map: HashMap<u64, ClipId>,
    user_clipchain_id_map: HashMap<u64, ClipChainId>,
    user_spatial_id_map: HashMap<u64, SpatialId>,

    spatial_id_stack: Vec<SpatialId>,

    requested_frame: usize,
    built_frame: usize,

    yaml_string: String,
    keyframes: Option<Yaml>,

    external_image_handler: Option<Box<LocalExternalImageHandler>>,

    next_spatial_key: u64,
}

impl YamlFrameReader {
    pub fn new(yaml_path: &Path) -> YamlFrameReader {
        YamlFrameReader {
            watch_source: false,
            list_resources: false,
            yaml_path: yaml_path.to_owned(),
            aux_dir: yaml_path.parent().unwrap().to_owned(),
            frame_count: 0,
            display_lists: Vec::new(),
            scroll_offsets: HashMap::new(),
            fonts: HashMap::new(),
            font_instances: HashMap::new(),
            font_render_mode: None,
            snapshots: HashMap::new(),
            allow_mipmaps: false,
            image_map: HashMap::new(),
            user_clip_id_map: HashMap::new(),
            user_clipchain_id_map: HashMap::new(),
            user_spatial_id_map: HashMap::new(),
            spatial_id_stack: Vec::new(),
            yaml_string: String::new(),
            requested_frame: 0,
            built_frame: usize::MAX,
            keyframes: None,
            external_image_handler: Some(Box::new(LocalExternalImageHandler::new())),
            next_external_scroll_id: 1000,      // arbitrary to easily see in logs which are implicit
            next_spatial_key: 0,
        }
    }

    pub fn deinit(mut self, wrench: &mut Wrench) {
        let mut txn = Transaction::new();

        for (_, font_instance) in self.font_instances.drain() {
            txn.delete_font_instance(font_instance);
        }

        for (_, font) in self.fonts.drain() {
            txn.delete_font(font);
        }

        wrench.api.send_transaction(wrench.document_id, txn);
    }

    fn top_space(&self) -> SpatialId {
        *self.spatial_id_stack.last().unwrap()
    }

    pub fn yaml_path(&self) -> &PathBuf {
        &self.yaml_path
    }

    pub fn new_from_args(args: &clap::ArgMatches) -> YamlFrameReader {
        let yaml_file = args.value_of("INPUT").map(PathBuf::from).unwrap();

        let mut y = YamlFrameReader::new(&yaml_file);

        y.keyframes = args.value_of("keyframes").map(|path| {
            let mut file = File::open(&path).unwrap();
            let mut keyframes_string = String::new();
            file.read_to_string(&mut keyframes_string).unwrap();
            YamlLoader::load_from_str(&keyframes_string)
                .expect("Failed to parse keyframes file")
                .pop()
                .unwrap()
        });
        y.list_resources = args.is_present("list-resources");
        y.watch_source = args.is_present("watch");
        y
    }

    pub fn reset(&mut self) {
        self.scroll_offsets.clear();
        self.display_lists.clear();
    }

    fn build(&mut self, wrench: &mut Wrench) {
        let yaml = YamlLoader::load_from_str(&self.yaml_string)
            .map(|mut yaml| {
                assert_eq!(yaml.len(), 1);
                yaml.pop().unwrap()
            })
            .expect("Failed to parse YAML file");

        self.reset();

        if let Some(pipelines) = yaml["pipelines"].as_vec() {
            for pipeline in pipelines {
                let pipeline_id = pipeline["id"].as_pipeline_id().unwrap();
                let mut builder = DisplayListBuilder::new(pipeline_id);
                self.build_pipeline(wrench, &mut builder, pipeline_id, false, pipeline);
            }
        }

        let mut builder = DisplayListBuilder::new(wrench.root_pipeline_id);

        if let Some(frames) = yaml["frames"].as_vec() {
            for frame in frames {
                self.build_pipeline(wrench, &mut builder, wrench.root_pipeline_id, true, frame);
            }
        } else {
            let root_stacking_context = &yaml["root"];
            assert_ne!(*root_stacking_context, Yaml::BadValue);
            self.build_pipeline(wrench, &mut builder, wrench.root_pipeline_id, true, root_stacking_context);
        }

        // If replaying the same frame during interactive use, the frame gets rebuilt,
        // but the external image handler has already been consumed by the renderer.
        if let Some(external_image_handler) = self.external_image_handler.take() {
            wrench.renderer.set_external_image_handler(external_image_handler);
        }
    }

    fn build_pipeline(
        &mut self,
        wrench: &mut Wrench,
        builder: &mut DisplayListBuilder,
        pipeline_id: PipelineId,
        send_transaction: bool,
        yaml: &Yaml
    ) {
        let offscreen = yaml["offscreen"].as_bool().unwrap_or(false);
        // By default, present if send_transaction is set to true. Can be overridden
        // by a field in the pipeline's root.
        let present = !offscreen && yaml["present"].as_bool().unwrap_or(send_transaction);

        // Don't allow referencing clips between pipelines for now.
        self.user_clip_id_map.clear();
        self.user_clipchain_id_map.clear();
        self.user_spatial_id_map.clear();
        self.spatial_id_stack.clear();
        self.spatial_id_stack.push(SpatialId::root_scroll_node(pipeline_id));

        builder.begin();
        let mut info = CommonItemProperties {
            clip_rect: LayoutRect::zero(),
            clip_chain_id: ClipChainId::INVALID,
            spatial_id: SpatialId::new(0, PipelineId::dummy()),
            flags: PrimitiveFlags::default(),
        };
        self.add_stacking_context_from_yaml(builder, wrench, yaml, IsRoot(true), &mut info);
        let (pipeline, payload) = builder.end();
        self.display_lists.push(DisplayList {
            pipeline,
            payload,
            present,
            send_transaction,
            render_offscreen: offscreen,
        });

        assert_eq!(self.spatial_id_stack.len(), 1);
    }

    fn to_clip_chain_id(
        &self,
        item: &Yaml,
        builder: &mut DisplayListBuilder,
    ) -> Option<ClipChainId> {
        match *item {
            Yaml::Integer(value) => {
                Some(self.user_clipchain_id_map[&(value as u64)])
            }
            Yaml::Array(ref array) => {
                let clip_ids: Vec<ClipId> = array
                    .iter()
                    .map(|id| {
                        let id = id.as_i64().expect("invalid clip id") as u64;
                        self.user_clip_id_map[&id]
                    })
                    .collect();

                Some(builder.define_clip_chain(None, clip_ids))
            }
            _ => None,
        }
    }

    fn to_spatial_id(&self, item: &Yaml, pipeline_id: PipelineId) -> Option<SpatialId> {
        match *item {
            Yaml::Integer(value) => Some(self.user_spatial_id_map[&(value as u64)]),
            Yaml::String(ref id_string) if id_string == "root-reference-frame" =>
                Some(SpatialId::root_reference_frame(pipeline_id)),
            Yaml::String(ref id_string) if id_string == "root-scroll-node" =>
                Some(SpatialId::root_scroll_node(pipeline_id)),
            Yaml::BadValue => None,
            _ => {
                println!("Unable to parse SpatialId {:?}", item);
                None
            }
        }
    }

    fn add_clip_id_mapping(&mut self, numeric_id: u64, real_id: ClipId) {
        assert_ne!(numeric_id, 0, "id=0 is reserved for the root clip");
        self.user_clip_id_map.insert(numeric_id, real_id);
    }

    fn add_clip_chain_id_mapping(&mut self, numeric_id: u64, real_id: ClipChainId) {
        assert_ne!(numeric_id, 0, "id=0 is reserved for the root clip-chain");
        self.user_clipchain_id_map.insert(numeric_id, real_id);
    }

    fn add_spatial_id_mapping(&mut self, numeric_id: u64, real_id: SpatialId) {
        assert_ne!(numeric_id, 0, "id=0 is reserved for the root reference frame");
        assert_ne!(numeric_id, 1, "id=1 is reserved for the root scroll node");
        self.user_spatial_id_map.insert(numeric_id, real_id);
    }

    fn to_hit_testing_tag(&self, item: &Yaml) -> Option<ItemTag> {
        match *item {
            Yaml::Array(ref array) if array.len() == 2 => {
                match (array[0].as_i64(), array[1].as_i64()) {
                    (Some(first), Some(second)) => Some((first as u64, second as u16)),
                    _ => None,
                }
            }
            _ => None,
        }

    }

    fn add_or_get_image(
        &mut self,
        file: &Path,
        tiling: Option<i64>,
        item: &Yaml,
        wrench: &mut Wrench,
    ) -> (ImageKey, LayoutSize) {
        let key = (file.to_owned(), tiling);
        if let Some(k) = self.image_map.get(&key) {
            return *k;
        }

        if self.list_resources { println!("{}", file.to_string_lossy()); }
        let (descriptor, image_data) = match image::open(file) {
            Ok(image) => {
                let (image_width, image_height) = image.dimensions();
                let (format, bytes) = match image {
                    image::DynamicImage::ImageLuma8(_) => {
                        (ImageFormat::R8, image.to_bytes())
                    }
                    image::DynamicImage::ImageRgba8(_) => {
                        let mut pixels = image.to_bytes();
                        premultiply(pixels.as_mut_slice());
                        (ImageFormat::BGRA8, pixels)
                    }
                    image::DynamicImage::ImageRgb8(_) => {
                        let bytes = image.to_bytes();
                        let mut pixels = Vec::with_capacity(image_width as usize * image_height as usize * 4);
                        for bgr in bytes.chunks(3) {
                            pixels.extend_from_slice(&[
                                bgr[2],
                                bgr[1],
                                bgr[0],
                                0xff
                            ]);
                        }
                        (ImageFormat::BGRA8, pixels)
                    }
                    _ => panic!("We don't support whatever your crazy image type is, come on"),
                };
                let mut flags = ImageDescriptorFlags::empty();
                if is_image_opaque(format, &bytes[..]) {
                    flags |= ImageDescriptorFlags::IS_OPAQUE;
                }
                if self.allow_mipmaps {
                    flags |= ImageDescriptorFlags::ALLOW_MIPMAPS;
                }
                let descriptor = ImageDescriptor::new(
                    image_width as i32,
                    image_height as i32,
                    format,
                    flags,
                );
                let data = ImageData::new(bytes);
                (descriptor, data)
            }
            _ => {
                // This is a hack but it is convenient when generating test cases and avoids
                // bloating the repository.
                match parse_function(
                    file.components()
                        .last()
                        .unwrap()
                        .as_os_str()
                        .to_str()
                        .unwrap(),
                ) {
                    ("xy-gradient", args, _) => generate_xy_gradient_image(
                        args.get(0).unwrap_or(&"1000").parse::<u32>().unwrap(),
                        args.get(1).unwrap_or(&"1000").parse::<u32>().unwrap(),
                    ),
                    ("solid-color", args, _) => generate_solid_color_image(
                        args.get(0).unwrap_or(&"255").parse::<u8>().unwrap(),
                        args.get(1).unwrap_or(&"255").parse::<u8>().unwrap(),
                        args.get(2).unwrap_or(&"255").parse::<u8>().unwrap(),
                        args.get(3).unwrap_or(&"255").parse::<u8>().unwrap(),
                        args.get(4).unwrap_or(&"1000").parse::<u32>().unwrap(),
                        args.get(5).unwrap_or(&"1000").parse::<u32>().unwrap(),
                    ),
                    (name @ "transparent-checkerboard", args, _) |
                    (name @ "checkerboard", args, _) => {
                        let border = args.get(0).unwrap_or(&"4").parse::<u32>().unwrap();

                        let (x_size, y_size, x_count, y_count) = match args.len() {
                            3 => {
                                let size = args.get(1).unwrap_or(&"32").parse::<u32>().unwrap();
                                let count = args.get(2).unwrap_or(&"8").parse::<u32>().unwrap();
                                (size, size, count, count)
                            }
                            5 => {
                                let x_size = args.get(1).unwrap_or(&"32").parse::<u32>().unwrap();
                                let y_size = args.get(2).unwrap_or(&"32").parse::<u32>().unwrap();
                                let x_count = args.get(3).unwrap_or(&"8").parse::<u32>().unwrap();
                                let y_count = args.get(4).unwrap_or(&"8").parse::<u32>().unwrap();
                                (x_size, y_size, x_count, y_count)
                            }
                            _ => {
                                panic!("invalid checkerboard function");
                            }
                        };

                        let kind = if name == "transparent-checkerboard" {
                            CheckerboardKind::BlackTransparent
                        } else {
                            CheckerboardKind::BlackGrey
                        };

                        generate_checkerboard_image(
                            border,
                            x_size,
                            y_size,
                            x_count,
                            y_count,
                            kind,
                        )
                    }
                    ("snapshot", args, _) => {
                        let snapshot = self.snapshots
                            .get(args[0])
                            .expect("Missing snapshot");
                        return (
                            snapshot.key.as_image(),
                            snapshot.size,
                        );
                    }
                    _ => {
                        panic!("Failed to load image {:?}", file.to_str());
                    }
                }
            }
        };
        let tiling = tiling.map(|tile_size| tile_size as u16);
        let image_key = wrench.api.generate_image_key();
        let mut txn = Transaction::new();

        let external = item["external"].as_bool().unwrap_or(false);
        if external {
            // This indicates we want to simulate an external texture,
            // ensure it gets created as such
            let external_target = match item["external-target"].as_str() {
                Some("2d") => ImageBufferKind::Texture2D,
                Some("rect") => ImageBufferKind::TextureRect,
                Some(t) => panic!("Unsupported external texture target: {}", t),
                None => ImageBufferKind::Texture2D,
            };

            let external_image_data =
                self.external_image_handler.as_mut().unwrap().add_image(
                    &wrench.renderer.device,
                    descriptor,
                    external_target,
                    image_data
                );
            txn.add_image(image_key, descriptor, external_image_data, tiling);
        } else {
            txn.add_image(image_key, descriptor, image_data, tiling);
        }

        wrench.api.send_transaction(wrench.document_id, txn);
        let val = (
            image_key,
            LayoutSize::new(descriptor.size.width as f32, descriptor.size.height as f32),
        );
        self.image_map.insert(key, val);
        val
    }

    fn get_or_create_font(&mut self, desc: FontDescriptor, wrench: &mut Wrench) -> FontKey {
        let list_resources = self.list_resources;
        *self.fonts
            .entry(desc.clone())
            .or_insert_with(|| match desc {
                FontDescriptor::Path {
                    ref path,
                    font_index,
                } => {
                    if list_resources { println!("{}", path.to_string_lossy()); }
                    let mut file = File::open(path).expect("Couldn't open font file");
                    let mut bytes = vec![];
                    file.read_to_end(&mut bytes)
                        .expect("failed to read font file");
                    wrench.font_key_from_bytes(bytes, font_index)
                }
                FontDescriptor::Family { ref name } => wrench.font_key_from_name(name),
                FontDescriptor::Properties {
                    ref family,
                    weight,
                    style,
                    stretch,
                } => wrench.font_key_from_properties(family, weight, style, stretch),
            })
    }

    pub fn allow_mipmaps(&mut self, allow_mipmaps: bool) {
        self.allow_mipmaps = allow_mipmaps;
    }

    pub fn set_font_render_mode(&mut self, render_mode: Option<FontRenderMode>) {
        self.font_render_mode = render_mode;
    }

    fn get_or_create_font_instance(
        &mut self,
        font_key: FontKey,
        size: f32,
        flags: FontInstanceFlags,
        synthetic_italics: SyntheticItalics,
        wrench: &mut Wrench,
    ) -> FontInstanceKey {
        let font_render_mode = self.font_render_mode;

        *self.font_instances
            .entry((font_key, size.into(), flags, synthetic_italics))
            .or_insert_with(|| {
                wrench.add_font_instance(
                    font_key,
                    size,
                    flags,
                    font_render_mode,
                    synthetic_italics,
                )
            })
    }

    fn as_image_mask(&mut self, item: &Yaml, wrench: &mut Wrench) -> Option<ImageMask> {
        item.as_hash()?;

        let tiling = item["tile-size"].as_i64();

        let (image_key, image_dims) = match item["image"].as_str() {
            Some("invalid") => (ImageKey::DUMMY, LayoutSize::new(100.0, 100.0)),
            Some(filename) => {
                let mut file = self.aux_dir.clone();
                file.push(filename);
                self.add_or_get_image(&file, tiling, item, wrench)
            }
            None => {
                warn!("No image provided for the image-mask!");
                return None;
            }
        };

        let image_rect = item["rect"]
            .as_rect()
            .unwrap_or_else(|| LayoutRect::from_size(image_dims));
        Some(ImageMask {
            image: image_key,
            rect: image_rect,
        })
    }

    fn handle_rect(
        &self,
        dl: &mut DisplayListBuilder,
        item: &Yaml,
        info: &CommonItemProperties,
    ) {
        let bounds_key = if item["type"].is_badvalue() {
            "rect"
        } else {
            "bounds"
        };

        let bounds = self.resolve_rect(&item[bounds_key]);
        let color = self.resolve_colorf(&item["color"]).unwrap_or(ColorF::BLACK);
        dl.push_rect(info, bounds, color);
    }

    fn handle_clear_rect(
        &self,
        dl: &mut DisplayListBuilder,
        item: &Yaml,
        info: &CommonItemProperties,
    ) {
        let bounds = item["bounds"].as_rect().expect("clear-rect type must have bounds");
        dl.push_clear_rect(info, bounds);
    }

    fn handle_hit_test(
        &mut self,
        dl: &mut DisplayListBuilder,
        item: &Yaml,
        info: &mut CommonItemProperties,
    ) {
        info.clip_rect = try_intersect!(
            item["bounds"].as_rect().expect("hit-test type must have bounds"),
            &info.clip_rect
        );

        if let Some(tag) = self.to_hit_testing_tag(&item["hit-testing-tag"]) {
            dl.push_hit_test(
                info.clip_rect,
                info.clip_chain_id,
                info.spatial_id,
                info.flags,
                tag,
            );
        }
    }

    fn handle_line(
        &mut self,
        dl: &mut DisplayListBuilder,
        item: &Yaml,
        info: &mut CommonItemProperties,
    ) {
        let color = item["color"].as_colorf().unwrap_or(ColorF::BLACK);
        let orientation = item["orientation"]
            .as_str()
            .and_then(LineOrientation::from_str)
            .expect("line must have orientation");
        let style = item["style"]
            .as_str()
            .and_then(LineStyle::from_str)
            .expect("line must have style");

        let wavy_line_thickness = if let LineStyle::Wavy = style {
            item["thickness"].as_f32().expect("wavy lines must have a thickness")
        } else {
            0.0
        };

        let area = if item["baseline"].is_badvalue() {
            let bounds_key = if item["type"].is_badvalue() {
                "rect"
            } else {
                "bounds"
            };

            item[bounds_key]
                .as_rect()
                .expect("line type must have bounds")
        } else {
            // Legacy line representation
            let baseline = item["baseline"].as_f32().expect("line must have baseline");
            let start = item["start"].as_f32().expect("line must have start");
            let end = item["end"].as_f32().expect("line must have end");
            let width = item["width"].as_f32().expect("line must have width");

            match orientation {
                LineOrientation::Horizontal => {
                    LayoutRect::from_origin_and_size(
                        LayoutPoint::new(start, baseline),
                        LayoutSize::new(end - start, width),
                    )
                }
                LineOrientation::Vertical => {
                    LayoutRect::from_origin_and_size(
                        LayoutPoint::new(baseline, start),
                        LayoutSize::new(width, end - start),
                    )
                }
            }
            };

        dl.push_line(
            info,
            &area,
            wavy_line_thickness,
            orientation,
            &color,
            style,
        );
    }

    fn handle_gradient(
        &mut self,
        dl: &mut DisplayListBuilder,
        item: &Yaml,
        info: &mut CommonItemProperties,
    ) {
        let bounds_key = if item["type"].is_badvalue() {
            "gradient"
        } else {
            "bounds"
        };
        let bounds = item[bounds_key]
            .as_rect()
            .expect("gradient must have bounds");

        let gradient = item.as_gradient(dl);
        let tile_size = item["tile-size"].as_size().unwrap_or_else(|| bounds.size());
        let tile_spacing = item["tile-spacing"].as_size().unwrap_or_else(LayoutSize::zero);

        dl.push_gradient(
            info,
            bounds,
            gradient,
            tile_size,
            tile_spacing
        );
    }

    fn handle_radial_gradient(
        &mut self,
        dl: &mut DisplayListBuilder,
        item: &Yaml,
        info: &mut CommonItemProperties,
    ) {
        let bounds_key = if item["type"].is_badvalue() {
            "radial-gradient"
        } else {
            "bounds"
        };
        let bounds = item[bounds_key]
            .as_rect()
            .expect("radial gradient must have bounds");
        let gradient = item.as_radial_gradient(dl);
        let tile_size = item["tile-size"].as_size().unwrap_or_else(|| bounds.size());
        let tile_spacing = item["tile-spacing"].as_size().unwrap_or_else(LayoutSize::zero);

        dl.push_radial_gradient(
            info,
            bounds,
            gradient,
            tile_size,
            tile_spacing,
        );
    }

    fn handle_conic_gradient(
        &mut self,
        dl: &mut DisplayListBuilder,
        item: &Yaml,
        info: &mut CommonItemProperties,
    ) {
        let bounds_key = if item["type"].is_badvalue() {
            "conic-gradient"
        } else {
            "bounds"
        };
        let bounds = item[bounds_key]
            .as_rect()
            .expect("conic gradient must have bounds");
        let gradient = item.as_conic_gradient(dl);
        let tile_size = item["tile-size"].as_size().unwrap_or_else(|| bounds.size());
        let tile_spacing = item["tile-spacing"].as_size().unwrap_or_else(LayoutSize::zero);

        dl.push_conic_gradient(
            info,
            bounds,
            gradient,
            tile_size,
            tile_spacing,
        );
    }

    fn handle_border(
        &mut self,
        dl: &mut DisplayListBuilder,
        wrench: &mut Wrench,
        item: &Yaml,
        info: &mut CommonItemProperties,
    ) {
        let bounds_key = if item["type"].is_badvalue() {
            "border"
        } else {
            "bounds"
        };
        let bounds = item[bounds_key].as_rect().expect("borders must have bounds");
        let widths = item["width"]
            .as_vec_f32()
            .expect("borders must have width(s)");
        let widths = broadcast(&widths, 4);
        let widths = LayoutSideOffsets::new(widths[0], widths[3], widths[2], widths[1]);
        let border_details = if let Some(border_type) = item["border-type"].as_str() {
            match border_type {
                "normal" => {
                    let colors = item["color"]
                        .as_vec_colorf()
                        .expect("borders must have color(s)");
                    let styles = item["style"]
                        .as_vec_string()
                        .expect("borders must have style(s)");
                    let styles = styles
                        .iter()
                        .map(|s| match s.as_str() {
                            "none" => BorderStyle::None,
                            "solid" => BorderStyle::Solid,
                            "double" => BorderStyle::Double,
                            "dotted" => BorderStyle::Dotted,
                            "dashed" => BorderStyle::Dashed,
                            "hidden" => BorderStyle::Hidden,
                            "ridge" => BorderStyle::Ridge,
                            "inset" => BorderStyle::Inset,
                            "outset" => BorderStyle::Outset,
                            "groove" => BorderStyle::Groove,
                            s => {
                                panic!("Unknown border style '{}'", s);
                            }
                        })
                        .collect::<Vec<BorderStyle>>();
                    let radius = item["radius"]
                        .as_border_radius()
                        .unwrap_or_else(BorderRadius::zero);

                    let colors = broadcast(&colors, 4);
                    let styles = broadcast(&styles, 4);

                    let top = BorderSide {
                        color: colors[0],
                        style: styles[0],
                    };
                    let right = BorderSide {
                        color: colors[1],
                        style: styles[1],
                    };
                    let bottom = BorderSide {
                        color: colors[2],
                        style: styles[2],
                    };
                    let left = BorderSide {
                        color: colors[3],
                        style: styles[3],
                    };
                    let do_aa = item["do_aa"].as_bool().unwrap_or(true);
                    Some(BorderDetails::Normal(NormalBorder {
                        top,
                        left,
                        bottom,
                        right,
                        radius,
                        do_aa,
                    }))
                }
                "image" | "gradient" | "radial-gradient" | "conic-gradient" => {
                    let image_width = item["image-width"]
                        .as_i64()
                        .unwrap_or(bounds.width() as i64);
                    let image_height = item["image-height"]
                        .as_i64()
                        .unwrap_or(bounds.height() as i64);
                    let fill = item["fill"].as_bool().unwrap_or(false);

                    let slice = if let Some(slice) = item["slice"].as_vec_u32() {
                        broadcast(&slice, 4)
                    } else {
                        vec![widths.top as u32, widths.left as u32, widths.bottom as u32, widths.right as u32]
                    };

                    let repeat_horizontal = match item["repeat-horizontal"]
                        .as_str()
                        .unwrap_or("stretch")
                    {
                        "stretch" => RepeatMode::Stretch,
                        "repeat" => RepeatMode::Repeat,
                        "round" => RepeatMode::Round,
                        "space" => RepeatMode::Space,
                        s => panic!("Unknown box border image repeat mode {}", s),
                    };
                    let repeat_vertical = match item["repeat-vertical"]
                        .as_str()
                        .unwrap_or("stretch")
                    {
                        "stretch" => RepeatMode::Stretch,
                        "repeat" => RepeatMode::Repeat,
                        "round" => RepeatMode::Round,
                        "space" => RepeatMode::Space,
                        s => panic!("Unknown box border image repeat mode {}", s),
                    };
                    let source = match border_type {
                        "image" => {
                            let file = rsrc_path(&item["image-source"], &self.aux_dir);
                            let (image_key, _) = self
                                .add_or_get_image(&file, None, item, wrench);
                            NinePatchBorderSource::Image(image_key, ImageRendering::Auto)
                        }
                        "gradient" => {
                            let gradient = item.as_gradient(dl);
                            NinePatchBorderSource::Gradient(gradient)
                        }
                        "radial-gradient" => {
                            let gradient = item.as_radial_gradient(dl);
                            NinePatchBorderSource::RadialGradient(gradient)
                        }
                        "conic-gradient" => {
                            let gradient = item.as_conic_gradient(dl);
                            NinePatchBorderSource::ConicGradient(gradient)
                        }
                        _ => unreachable!("Unexpected border type"),
                    };

                    Some(BorderDetails::NinePatch(NinePatchBorder {
                        source,
                        width: image_width as i32,
                        height: image_height as i32,
                        slice: SideOffsets2D::new(slice[0] as i32, slice[1] as i32, slice[2] as i32, slice[3] as i32),
                        fill,
                        repeat_horizontal,
                        repeat_vertical,
                    }))
                }
                _ => {
                    println!("Unable to parse border {:?}", item);
                    None
                }
            }
        } else {
            println!("Unable to parse border {:?}", item);
            None
        };
        if let Some(details) = border_details {
            dl.push_border(info, bounds, widths, details);
        }
    }

    fn handle_box_shadow(
        &mut self,
        dl: &mut DisplayListBuilder,
        item: &Yaml,
        info: &mut CommonItemProperties,
    ) {
        let bounds_key = if item["type"].is_badvalue() {
            "box-shadow"
        } else {
            "bounds"
        };
        let bounds = item[bounds_key]
            .as_rect()
            .expect("box shadow must have bounds");
        let box_bounds = item["box-bounds"].as_rect().unwrap_or(bounds);
        let offset = self.resolve_vector(&item["offset"], LayoutVector2D::zero());
        let color = item["color"]
            .as_colorf()
            .unwrap_or_else(|| ColorF::new(0.0, 0.0, 0.0, 1.0));
        let blur_radius = item["blur-radius"].as_force_f32().unwrap_or(0.0);
        let spread_radius = item["spread-radius"].as_force_f32().unwrap_or(0.0);
        let border_radius = item["border-radius"]
            .as_border_radius()
            .unwrap_or_else(BorderRadius::zero);
        let clip_mode = if let Some(mode) = item["clip-mode"].as_str() {
            match mode {
                "outset" => BoxShadowClipMode::Outset,
                "inset" => BoxShadowClipMode::Inset,
                s => panic!("Unknown box shadow clip mode {}", s),
            }
        } else {
            BoxShadowClipMode::Outset
        };

        dl.push_box_shadow(
            info,
            box_bounds,
            offset,
            color,
            blur_radius,
            spread_radius,
            border_radius,
            clip_mode,
        );
    }

    fn handle_yuv_image(
        &mut self,
        dl: &mut DisplayListBuilder,
        wrench: &mut Wrench,
        item: &Yaml,
        info: &mut CommonItemProperties,
    ) {
        // TODO(gw): Support other YUV color depth and spaces.
        let color_depth = ColorDepth::Color8;
        let color_space = YuvColorSpace::Rec709;
        let color_range = ColorRange::Limited;

        let yuv_data = match item["format"].as_str().expect("no format supplied") {
            "planar" => {
                let y_path = rsrc_path(&item["src-y"], &self.aux_dir);
                let (y_key, _) = self.add_or_get_image(&y_path, None, item, wrench);

                let u_path = rsrc_path(&item["src-u"], &self.aux_dir);
                let (u_key, _) = self.add_or_get_image(&u_path, None, item, wrench);

                let v_path = rsrc_path(&item["src-v"], &self.aux_dir);
                let (v_key, _) = self.add_or_get_image(&v_path, None, item, wrench);

                YuvData::PlanarYCbCr(y_key, u_key, v_key)
            }
            "nv12" => {
                let y_path = rsrc_path(&item["src-y"], &self.aux_dir);
                let (y_key, _) = self.add_or_get_image(&y_path, None, item, wrench);

                let uv_path = rsrc_path(&item["src-uv"], &self.aux_dir);
                let (uv_key, _) = self.add_or_get_image(&uv_path, None, item, wrench);

                YuvData::NV12(y_key, uv_key)
            }
            "p010" => {
                let y_path = rsrc_path(&item["src-y"], &self.aux_dir);
                let (y_key, _) = self.add_or_get_image(&y_path, None, item, wrench);

                let uv_path = rsrc_path(&item["src-uv"], &self.aux_dir);
                let (uv_key, _) = self.add_or_get_image(&uv_path, None, item, wrench);

                YuvData::P010(y_key, uv_key)
            }
            "nv16" => {
                let y_path = rsrc_path(&item["src-y"], &self.aux_dir);
                let (y_key, _) = self.add_or_get_image(&y_path, None, item, wrench);

                let uv_path = rsrc_path(&item["src-uv"], &self.aux_dir);
                let (uv_key, _) = self.add_or_get_image(&uv_path, None, item, wrench);

                YuvData::NV16(y_key, uv_key)
            }
            "interleaved" => {
                let yuv_path = rsrc_path(&item["src"], &self.aux_dir);
                let (yuv_key, _) = self.add_or_get_image(&yuv_path, None, item, wrench);

                YuvData::InterleavedYCbCr(yuv_key)
            }
            _ => {
                panic!("unexpected yuv format");
            }
        };

        let bounds = item["bounds"].as_vec_f32().unwrap();
        let bounds = LayoutRect::from_origin_and_size(
            LayoutPoint::new(bounds[0], bounds[1]),
            LayoutSize::new(bounds[2], bounds[3]),
        );

        dl.push_yuv_image(
            info,
            bounds,
            yuv_data,
            color_depth,
            color_space,
            color_range,
            ImageRendering::Auto,
        );
    }

    fn handle_image(
        &mut self,
        dl: &mut DisplayListBuilder,
        wrench: &mut Wrench,
        item: &Yaml,
        info: &mut CommonItemProperties,
    ) {
        let filename = &item[if item["type"].is_badvalue() {
                                 "image"
                             } else {
                                 "src"
                             }];
        let tiling = item["tile-size"].as_i64();

        let file = rsrc_path(filename, &self.aux_dir);
        let (image_key, image_dims) =
            self.add_or_get_image(&file, tiling, item, wrench);

        let bounds_raws = item["bounds"].as_vec_f32().unwrap();
        let bounds = if bounds_raws.len() == 2 {
            LayoutRect::from_origin_and_size(LayoutPoint::new(bounds_raws[0], bounds_raws[1]), image_dims)
        } else if bounds_raws.len() == 4 {
            LayoutRect::from_origin_and_size(
                LayoutPoint::new(bounds_raws[0], bounds_raws[1]),
                LayoutSize::new(bounds_raws[2], bounds_raws[3]),
            )
        } else {
            panic!(
                "image expected 2 or 4 values in bounds, got '{:?}'",
                item["bounds"]
            );
        };
        let rendering = match item["rendering"].as_str() {
            Some("auto") | None => ImageRendering::Auto,
            Some("crisp-edges") => ImageRendering::CrispEdges,
            Some("pixelated") => ImageRendering::Pixelated,
            Some(_) => panic!(
                "ImageRendering can be auto, crisp-edges, or pixelated -- got {:?}",
                item
            ),
        };
        let alpha_type = match item["alpha-type"].as_str() {
            Some("premultiplied-alpha") | None => AlphaType::PremultipliedAlpha,
            Some("alpha") => AlphaType::Alpha,
            Some(_) => panic!(
                "AlphaType can be premultiplied-alpha or alpha -- got {:?}",
                item
            ),
        };
        let color = item["color"]
            .as_colorf()
            .unwrap_or_else(|| ColorF::WHITE);
        let stretch_size = item["stretch-size"].as_size();
        let tile_spacing = item["tile-spacing"].as_size();
        if stretch_size.is_none() && tile_spacing.is_none() {
            dl.push_image(
                info,
                bounds,
                rendering,
                alpha_type,
                image_key,
                color,
           );
        } else {
            dl.push_repeating_image(
                info,
                bounds,
                stretch_size.unwrap_or(image_dims),
                tile_spacing.unwrap_or_else(LayoutSize::zero),
                rendering,
                alpha_type,
                image_key,
                color,
           );
        }
    }

    fn handle_text(
        &mut self,
        dl: &mut DisplayListBuilder,
        wrench: &mut Wrench,
        item: &Yaml,
        info: &mut CommonItemProperties,
    ) {
        let size = item["size"].as_pt_to_f32().unwrap_or(16.0);
        let color = item["color"].as_colorf().unwrap_or(ColorF::BLACK);
        let synthetic_italics = if let Some(angle) = item["synthetic-italics"].as_f32() {
            SyntheticItalics::from_degrees(angle)
        } else if item["synthetic-italics"].as_bool().unwrap_or(false) {
            SyntheticItalics::enabled()
        } else {
            SyntheticItalics::disabled()
        };

        let mut flags = FontInstanceFlags::empty();
        if item["synthetic-bold"].as_bool().unwrap_or(false) {
            flags |= FontInstanceFlags::SYNTHETIC_BOLD;
        }
        if item["embedded-bitmaps"].as_bool().unwrap_or(false) {
            flags |= FontInstanceFlags::EMBEDDED_BITMAPS;
        }
        if item["transpose"].as_bool().unwrap_or(false) {
            flags |= FontInstanceFlags::TRANSPOSE;
        }
        if item["flip-x"].as_bool().unwrap_or(false) {
            flags |= FontInstanceFlags::FLIP_X;
        }
        if item["flip-y"].as_bool().unwrap_or(false) {
            flags |= FontInstanceFlags::FLIP_Y;
        }

        assert!(
            item["blur-radius"].is_badvalue(),
            "text no longer has a blur radius, use PushShadow and PopAllShadows"
        );

        let desc = FontDescriptor::from_yaml(item, &self.aux_dir);
        let font_key = self.get_or_create_font(desc, wrench);
        let font_instance_key = self.get_or_create_font_instance(font_key,
                                                                 size,
                                                                 flags,
                                                                 synthetic_italics,
                                                                 wrench);

        assert!(
            !(item["glyphs"].is_badvalue() && item["text"].is_badvalue()),
            "text item had neither text nor glyphs!"
        );

        let (glyphs, rect) = if item["text"].is_badvalue() {
            // if glyphs are specified, then the glyph positions can have the
            // origin baked in.
            let origin = item["origin"]
                .as_point()
                .unwrap_or(LayoutPoint::new(0.0, 0.0));
            let glyph_indices = item["glyphs"].as_vec_u32().unwrap();
            let glyph_offsets = item["offsets"].as_vec_f32().unwrap();
            assert_eq!(glyph_offsets.len(), glyph_indices.len() * 2);

            let glyphs = glyph_indices
                .iter()
                .enumerate()
                .map(|k| {
                    GlyphInstance {
                        index: *k.1,
                        // In the future we want to change the API to be relative, eliminating this
                        point: LayoutPoint::new(
                            origin.x + glyph_offsets[k.0 * 2],
                            origin.y + glyph_offsets[k.0 * 2 + 1],
                        ),
                    }
                })
                .collect::<Vec<_>>();
            // TODO(gw): We could optionally use the WR API to query glyph dimensions
            //           here and calculate the bounding region here if we want to.
            let rect = item["bounds"]
                .as_rect()
                .expect("Text items with glyphs require bounds [for now]");
            (glyphs, rect)
        } else {
            let text = item["text"].as_str().unwrap();
            let origin = item["origin"]
                .as_point()
                .expect("origin required for text without glyphs");
            let (glyph_indices, glyph_positions, bounds) = wrench.layout_simple_ascii(
                font_key,
                font_instance_key,
                text,
                size,
                origin,
                flags,
            );

            let glyphs = glyph_indices
                .iter()
                .zip(glyph_positions)
                .map(|arg| {
                    GlyphInstance {
                        index: *arg.0 as u32,
                        point: arg.1,
                    }
                })
                .collect::<Vec<_>>();
            (glyphs, bounds)
        };

        dl.push_text(
            info,
            rect,
            &glyphs,
            font_instance_key,
            color,
            None,
        );
    }

    fn handle_iframe(
        &mut self,
        dl: &mut DisplayListBuilder,
        item: &Yaml,
        info: &mut CommonItemProperties,
    ) {
        let bounds = item["bounds"].as_rect().expect("iframe must have bounds");
        let pipeline_id = item["id"].as_pipeline_id().unwrap();
        let ignore = item["ignore_missing_pipeline"].as_bool().unwrap_or(true);
        dl.push_iframe(
            bounds,
            info.clip_rect,
            &SpaceAndClipInfo {
                spatial_id: info.spatial_id,
                clip_chain_id: info.clip_chain_id
            },
            pipeline_id,
            ignore
        );
    }

    fn get_item_type_from_yaml(item: &Yaml) -> &str {
        let shorthands = [
            "rect",
            "image",
            "text",
            "glyphs",
            "box-shadow", // Note: box_shadow shorthand check has to come before border.
            "border",
            "gradient",
            "radial-gradient",
            "conic-gradient"
        ];

        for shorthand in shorthands.iter() {
            if !item[*shorthand].is_badvalue() {
                return shorthand;
            }
        }
        item["type"].as_str().unwrap_or("unknown")
    }

    fn add_display_list_items_from_yaml(
        &mut self,
        dl: &mut DisplayListBuilder,
        wrench: &mut Wrench,
        yaml_items: &[Yaml],
    ) {
        // A very large number (but safely far away from finite limits of f32)
        let big_number = 1.0e30;
        // A rect that should in practical terms serve as a no-op for clipping
        let full_clip = LayoutRect::from_origin_and_size(
            LayoutPoint::new(-big_number / 2.0, -big_number / 2.0),
            LayoutSize::new(big_number, big_number));

        for item in yaml_items {
            let item_type = Self::get_item_type_from_yaml(item);

            let spatial_id = self.to_spatial_id(&item["spatial-id"], dl.pipeline_id);

            if let Some(spatial_id) = spatial_id {
                self.spatial_id_stack.push(spatial_id);
            }

            let clip_rect = item["clip-rect"].as_rect().unwrap_or(full_clip);
            let clip_chain_id = self.to_clip_chain_id(&item["clip-chain"], dl).unwrap_or(ClipChainId::INVALID);

            let mut flags = PrimitiveFlags::default();
            for (key, flag) in [
                ("backface-visible", PrimitiveFlags::IS_BACKFACE_VISIBLE),
                ("scrollbar-container", PrimitiveFlags::IS_SCROLLBAR_CONTAINER),
                ("prefer-compositor-surface", PrimitiveFlags::PREFER_COMPOSITOR_SURFACE),
            ] {
                if let Some(value) = item[key].as_bool() {
                    flags.set(flag, value);
                }
            }


            let mut info = CommonItemProperties {
                clip_rect,
                clip_chain_id,
                spatial_id: self.top_space(),
                flags,
            };

            match item_type {
                "rect" => self.handle_rect(dl, item, &info),
                "hit-test" => self.handle_hit_test(dl, item, &mut info),
                "clear-rect" => self.handle_clear_rect(dl, item, &info),
                "line" => self.handle_line(dl, item, &mut info),
                "image" => self.handle_image(dl, wrench, item, &mut info),
                "yuv-image" => self.handle_yuv_image(dl, wrench, item, &mut info),
                "text" | "glyphs" => self.handle_text(dl, wrench, item, &mut info),
                "scroll-frame" => self.handle_scroll_frame(dl, wrench, item),
                "sticky-frame" => self.handle_sticky_frame(dl, wrench, item),
                "clip" => self.handle_clip(dl, wrench, item),
                "clip-chain" => self.handle_clip_chain(dl, item),
                "border" => self.handle_border(dl, wrench, item, &mut info),
                "gradient" => self.handle_gradient(dl, item, &mut info),
                "radial-gradient" => self.handle_radial_gradient(dl, item, &mut info),
                "conic-gradient" => self.handle_conic_gradient(dl, item, &mut info),
                "box-shadow" => self.handle_box_shadow(dl, item, &mut info),
                "iframe" => self.handle_iframe(dl, item, &mut info),
                "stacking-context" => {
                    self.add_stacking_context_from_yaml(dl, wrench, item, IsRoot(false), &mut info)
                }
                "reference-frame" => self.handle_reference_frame(dl, wrench, item),
                "computed-frame" => self.handle_computed_frame(dl, wrench, item),
                "shadow" => self.handle_push_shadow(dl, item, &mut info),
                "pop-all-shadows" => self.handle_pop_all_shadows(dl),
                "backdrop-filter" => self.handle_backdrop_filter(dl, item, &mut info),
                _ => println!("Skipping unknown item type: {:?}", item),
            }

            if spatial_id.is_some() {
                self.spatial_id_stack.pop().unwrap();
            }
        }
    }

    fn next_spatial_key(&mut self) -> SpatialTreeItemKey {
        let key = SpatialTreeItemKey::new(self.next_spatial_key, 0);
        self.next_spatial_key += 1;
        key
    }

    fn handle_scroll_frame(
        &mut self,
        dl: &mut DisplayListBuilder,
        wrench: &mut Wrench,
        yaml: &Yaml,
    ) {
        let clip_rect = yaml["bounds"]
            .as_rect()
            .expect("scroll frame must have a bounds");
        let content_size = yaml["content-size"].as_size().unwrap_or_else(|| clip_rect.size());
        let content_rect = LayoutRect::from_origin_and_size(clip_rect.min, content_size);
        let external_scroll_offset = yaml["external-scroll-offset"].as_vector().unwrap_or_else(LayoutVector2D::zero);
        let scroll_generation = yaml["scroll-generation"].as_i64().map_or(APZScrollGeneration::default(), |v| v as u64);
        let has_scroll_linked_effect =
        yaml["has-scroll-linked-effect"].as_bool().map_or(HasScrollLinkedEffect::default(),
            |v| if v { HasScrollLinkedEffect::Yes } else { HasScrollLinkedEffect::No }
        );

        let numeric_id = yaml["id"].as_i64().map(|id| id as u64);

        let external_id = ExternalScrollId(self.next_external_scroll_id, dl.pipeline_id);
        self.next_external_scroll_id += 1;

        if let Some(vector) = yaml["scroll-offset"].as_vector() {
            self.scroll_offsets.insert(
                external_id,
                vec![SampledScrollOffset {
                    offset: vector,
                    generation: APZScrollGeneration::default(),
                }],
            );
        }

        if !yaml["scroll-offsets"].is_badvalue() {
            let mut offsets = Vec::new();
            for entry in yaml["scroll-offsets"].as_vec().unwrap() {
                let offset = entry["offset"].as_vector().unwrap_or(LayoutVector2D::zero());
                let generation = entry["generation"].as_i64().map_or(APZScrollGeneration::default(), |v| v as u64);
                offsets.push(SampledScrollOffset { offset, generation });
            }
            self.scroll_offsets.insert(external_id, offsets);
        }

        let clip_to_frame = yaml["clip-to-frame"].as_bool().unwrap_or(false);

        let clip_id = if clip_to_frame {
            Some(dl.define_clip_rect(
                self.top_space(),
                clip_rect,
            ))
        } else {
            None
        };

        let spatial_id = dl.define_scroll_frame(
            self.top_space(),
            external_id,
            content_rect,
            clip_rect,
            external_scroll_offset,
            scroll_generation,
            has_scroll_linked_effect,
            self.next_spatial_key(),
        );
        if let Some(numeric_id) = numeric_id {
            self.add_spatial_id_mapping(numeric_id, spatial_id);
            if let Some(clip_id) = clip_id {
                self.add_clip_id_mapping(numeric_id, clip_id);
            }
        }

        if let Some(yaml_items) = yaml["items"].as_vec() {
            self.spatial_id_stack.push(spatial_id);
            self.add_display_list_items_from_yaml(dl, wrench, yaml_items);
            self.spatial_id_stack.pop().unwrap();
        }
    }

    fn handle_sticky_frame(
        &mut self,
        dl: &mut DisplayListBuilder,
        wrench: &mut Wrench,
        yaml: &Yaml,
    ) {
        let bounds = yaml["bounds"].as_rect().expect("sticky frame must have a bounds");
        let numeric_id = yaml["id"].as_i64().map(|id| id as u64);

        let real_id = dl.define_sticky_frame(
            *self.spatial_id_stack.last().unwrap(),
            bounds,
            SideOffsets2D::new(
                yaml["margin-top"].as_f32(),
                yaml["margin-right"].as_f32(),
                yaml["margin-bottom"].as_f32(),
                yaml["margin-left"].as_f32(),
            ),
            yaml["vertical-offset-bounds"].as_sticky_offset_bounds(),
            yaml["horizontal-offset-bounds"].as_sticky_offset_bounds(),
            yaml["previously-applied-offset"].as_vector().unwrap_or_else(LayoutVector2D::zero),
            self.next_spatial_key(),
            None,
        );

        if let Some(numeric_id) = numeric_id {
            self.add_spatial_id_mapping(numeric_id, real_id);
        }

        if let Some(yaml_items) = yaml["items"].as_vec() {
            self.spatial_id_stack.push(real_id);
            self.add_display_list_items_from_yaml(dl, wrench, yaml_items);
            self.spatial_id_stack.pop().unwrap();
        }
    }

    fn resolve_binding<'a>(
        &'a self,
        yaml: &'a Yaml,
    ) -> &'a Yaml {
        if let Some(keyframes) = &self.keyframes {
            if let Some(s) = yaml.as_str() {
                const PREFIX: &str = "key(";
                const SUFFIX: &str = ")";
                if let Some(key) = s.strip_prefix(PREFIX).and_then(|s| s.strip_suffix(SUFFIX)) {
                    return &keyframes[key][self.requested_frame];
                }
            }
        }

        yaml
    }

    fn resolve_colorf(
        &self,
        yaml: &Yaml,
    ) -> Option<ColorF> {
        self.resolve_binding(yaml)
            .as_colorf()
    }

    fn resolve_rect(
        &self,
        yaml: &Yaml,
    ) -> LayoutRect {
        self.resolve_binding(yaml)
            .as_rect()
            .unwrap_or_else(|| panic!("invalid rect {:?}", yaml))
    }

    fn resolve_vector(
        &self,
        yaml: &Yaml,
        default: LayoutVector2D,
    ) -> LayoutVector2D {
        self.resolve_binding(yaml)
            .as_vector()
            .unwrap_or(default)
    }

    fn handle_push_shadow(
        &mut self,
        dl: &mut DisplayListBuilder,
        yaml: &Yaml,
        info: &mut CommonItemProperties,
    ) {
        let blur_radius = yaml["blur-radius"].as_f32().unwrap_or(0.0);
        let offset = yaml["offset"].as_vector().unwrap_or_else(LayoutVector2D::zero);
        let color = yaml["color"].as_colorf().unwrap_or(ColorF::BLACK);

        dl.push_shadow(
            &SpaceAndClipInfo { spatial_id: info.spatial_id, clip_chain_id: info.clip_chain_id },
            Shadow {
                blur_radius,
                offset,
                color,
            },
            true,
        );
    }

    fn handle_pop_all_shadows(&mut self, dl: &mut DisplayListBuilder) {
        dl.pop_all_shadows();
    }

    fn handle_clip_chain(&mut self, builder: &mut DisplayListBuilder, yaml: &Yaml) {
        let numeric_id = yaml["id"].as_i64().expect("clip chains must have an id");
        let clip_ids: Vec<ClipId> = yaml["clips"]
            .as_vec_u64()
            .unwrap_or_default()
            .iter()
            .map(|id| self.user_clip_id_map[id])
            .collect();

        let parent = self.to_clip_chain_id(&yaml["parent"], builder);
        let real_id = builder.define_clip_chain(parent, clip_ids);
        self.add_clip_chain_id_mapping(numeric_id as u64, real_id);
    }

    fn handle_clip(&mut self, dl: &mut DisplayListBuilder, wrench: &mut Wrench, yaml: &Yaml) {
        let numeric_id = yaml["id"].as_i64();
        let spatial_id = self.top_space();
        let complex_clips = yaml["complex"].as_complex_clip_regions();
        let mut clip_id = None;

        if let Some(clip_rect) = yaml["bounds"].as_rect() {
            clip_id = Some(dl.define_clip_rect(
                spatial_id,
                clip_rect,
            ));
        }

        if let Some(image_mask) = self.as_image_mask(&yaml["image-mask"], wrench) {
            assert!(clip_id.is_none(), "invalid clip definition");

            clip_id = Some(dl.define_clip_image_mask(
                spatial_id,
                image_mask,
                &[],
                FillRule::Nonzero,
            ));
        }

        if !complex_clips.is_empty() {
            // Only 1 complex clip is supported per clip (todo: change yaml format)
            assert_eq!(complex_clips.len(), 1);
            assert!(clip_id.is_none(), "invalid clip definition");

            clip_id = Some(dl.define_clip_rounded_rect(
                spatial_id,
                complex_clips[0],
            ));
        }

        if let Some(clip_id) = clip_id {
            if let Some(numeric_id) = numeric_id {
                self.add_clip_id_mapping(numeric_id as u64, clip_id);
            }
        }
    }

    fn push_reference_frame(
        &mut self,
        dl: &mut DisplayListBuilder,
        default_bounds: impl Fn() -> LayoutRect,
        yaml: &Yaml,
    ) -> SpatialId {
        let bounds = yaml["bounds"].as_rect().unwrap_or_else(default_bounds);
        let default_transform_origin = LayoutPoint::new(
            bounds.min.x + bounds.width() * 0.5,
            bounds.min.y + bounds.height() * 0.5,
        );

        let transform_style = yaml["transform-style"]
            .as_transform_style()
            .unwrap_or(TransformStyle::Flat);

        let transform_origin = yaml["transform-origin"]
            .as_point()
            .unwrap_or(default_transform_origin);

        assert!(
            yaml["transform"].is_badvalue() ||
            yaml["perspective"].is_badvalue(),
            "Should have one of either transform or perspective"
        );

        let perspective_origin = yaml["perspective-origin"]
            .as_point()
            .unwrap_or(default_transform_origin);

        let reference_frame_kind = if !yaml["perspective"].is_badvalue() {
            ReferenceFrameKind::Perspective { scrolling_relative_to: None }
        } else {
            ReferenceFrameKind::Transform {
                is_2d_scale_translation: false,
                should_snap: false,
                paired_with_perspective: yaml["paired-with-perspective"].as_bool().unwrap_or(false),
            }
        };

        let transform = yaml["transform"]
            .as_transform(&transform_origin);

        let perspective = match yaml["perspective"].as_f32() {
            Some(value) if value != 0.0 => {
                Some(make_perspective(perspective_origin, value as f32))
            }
            Some(..) => None,
            _ => yaml["perspective"].as_matrix4d(),
        };

        let reference_frame_id = dl.push_reference_frame(
            bounds.min,
            *self.spatial_id_stack.last().unwrap(),
            transform_style,
            transform.or(perspective).unwrap_or_default().into(),
            reference_frame_kind,
            self.next_spatial_key(),
        );

        let numeric_id = yaml["id"].as_i64();
        if let Some(numeric_id) = numeric_id {
            self.add_spatial_id_mapping(numeric_id as u64, reference_frame_id);
        }

        reference_frame_id
    }

    fn handle_reference_frame(
        &mut self,
        dl: &mut DisplayListBuilder,
        wrench: &mut Wrench,
        yaml: &Yaml,
    ) {
        let default_bounds = || LayoutRect::from_size(wrench.window_size_f32());
        let real_id = self.push_reference_frame(dl, default_bounds, yaml);
        self.spatial_id_stack.push(real_id);

        if let Some(yaml_items) = yaml["items"].as_vec() {
            self.add_display_list_items_from_yaml(dl, wrench, yaml_items);
        }

        self.spatial_id_stack.pop().unwrap();
        dl.pop_reference_frame();
    }

    fn push_computed_frame(
        &mut self,
        dl: &mut DisplayListBuilder,
        default_bounds: impl Fn() -> LayoutRect,
        yaml: &Yaml,
    ) -> SpatialId {
        let bounds = yaml["bounds"].as_rect().unwrap_or_else(default_bounds);

        let scale_from = yaml["scale-from"].as_size();
        let vertical_flip = yaml["vertical-flip"].as_bool().unwrap_or(false);
        let rotation = yaml["rotation"].as_rotation().unwrap_or(Rotation::Degree0);

        let reference_frame_id = dl.push_computed_frame(
            bounds.min,
            *self.spatial_id_stack.last().unwrap(),
            scale_from,
            vertical_flip,
            rotation,
            self.next_spatial_key(),
        );

        let numeric_id = yaml["id"].as_i64();
        if let Some(numeric_id) = numeric_id {
            self.add_spatial_id_mapping(numeric_id as u64, reference_frame_id);
        }

        reference_frame_id
    }

    fn handle_computed_frame(
        &mut self,
        dl: &mut DisplayListBuilder,
        wrench: &mut Wrench,
        yaml: &Yaml,
    ) {
        let default_bounds = || LayoutRect::from_size(wrench.window_size_f32());
        let real_id = self.push_computed_frame(dl, default_bounds, yaml);
        self.spatial_id_stack.push(real_id);

        if let Some(yaml_items) = yaml["items"].as_vec() {
            self.add_display_list_items_from_yaml(dl, wrench, yaml_items);
        }

        self.spatial_id_stack.pop().unwrap();
        dl.pop_reference_frame();
    }

    fn add_stacking_context_from_yaml(
        &mut self,
        dl: &mut DisplayListBuilder,
        wrench: &mut Wrench,
        yaml: &Yaml,
        IsRoot(is_root): IsRoot,
        info: &mut CommonItemProperties,
    ) {
        let default_bounds = || LayoutRect::from_size(wrench.window_size_f32());
        let mut bounds = yaml["bounds"].as_rect().unwrap_or_else(default_bounds);

        let pushed_reference_frame =
            if !yaml["transform"].is_badvalue() || !yaml["perspective"].is_badvalue() {
                let reference_frame_id = self.push_reference_frame(dl, default_bounds, yaml);
                self.spatial_id_stack.push(reference_frame_id);
                bounds.max -= bounds.min.to_vector();
                bounds.min = LayoutPoint::zero();
                true
            } else {
                false
            };

        let clip_chain_id = self.to_clip_chain_id(&yaml["clip-chain"], dl);

        let transform_style = yaml["transform-style"]
            .as_transform_style()
            .unwrap_or(TransformStyle::Flat);
        let mix_blend_mode = yaml["mix-blend-mode"]
            .as_mix_blend_mode()
            .unwrap_or(MixBlendMode::Normal);
        let raster_space = yaml["raster-space"]
            .as_raster_space()
            .unwrap_or(RasterSpace::Screen);
        let is_blend_container = yaml["blend-container"].as_bool().unwrap_or(false);
        let wraps_backdrop_filter = yaml["wraps-backdrop-filter"].as_bool().unwrap_or(false);

        if is_root {
            if let Some(vector) = yaml["scroll-offset"].as_vector() {
                let external_id = ExternalScrollId(0, dl.pipeline_id);
                self.scroll_offsets.insert(
                    external_id,
                    vec![SampledScrollOffset {
                        offset: vector,
                        generation: APZScrollGeneration::default(),
                    }],
                );
            }
        }

        let filters = yaml["filters"].as_vec_filter_op().unwrap_or_default();
        let filter_datas = yaml["filter-datas"].as_vec_filter_data().unwrap_or_default();
        let filter_primitives = yaml["filter-primitives"].as_vec_filter_primitive().unwrap_or_default();

        let snapshot = if !yaml["snapshot"].is_badvalue() {
            let yaml = &yaml["snapshot"];
            let name = yaml["name"].as_str().unwrap_or("snapshot");
            let area = yaml["area"].as_rect().unwrap_or(bounds);
            let detached = yaml["detached"].as_bool().unwrap_or(false);

            let key = SnapshotImageKey(wrench.api.generate_image_key());
            self.snapshots.insert(name.to_string(), Snapshot {
                key,
                size: bounds.size(),
            });

            let mut txn = Transaction::new();
            txn.add_snapshot_image(key);
            wrench.api.send_transaction(wrench.document_id, txn);

            Some(SnapshotInfo { key, area, detached })
        } else {
            None
        };

        let mut flags = StackingContextFlags::empty();
        flags.set(StackingContextFlags::IS_BLEND_CONTAINER, is_blend_container);
        flags.set(StackingContextFlags::WRAPS_BACKDROP_FILTER, wraps_backdrop_filter);

        dl.push_stacking_context(
            bounds.min,
            *self.spatial_id_stack.last().unwrap(),
            info.flags,
            clip_chain_id,
            transform_style,
            mix_blend_mode,
            &filters,
            &filter_datas,
            &filter_primitives,
            raster_space,
            flags,
            snapshot,
        );

        if let Some(yaml_items) = yaml["items"].as_vec() {
            self.add_display_list_items_from_yaml(dl, wrench, yaml_items);
        }

        dl.pop_stacking_context();

        if pushed_reference_frame {
            self.spatial_id_stack.pop().unwrap();
            dl.pop_reference_frame();
        }
    }

    fn handle_backdrop_filter(
        &mut self,
        dl: &mut DisplayListBuilder,
        item: &Yaml,
        info: &mut CommonItemProperties,
    ) {
        info.clip_rect = try_intersect!(
            self.resolve_rect(&item["bounds"]),
            &info.clip_rect
        );

        let filters = item["filters"].as_vec_filter_op().unwrap_or_default();
        let filter_datas = item["filter-datas"].as_vec_filter_data().unwrap_or_default();
        let filter_primitives = item["filter-primitives"].as_vec_filter_primitive().unwrap_or_default();

        dl.push_backdrop_filter(
            info,
            &filters,
            &filter_datas,
            &filter_primitives,
        );
    }
}

impl WrenchThing for YamlFrameReader {
    fn do_frame(&mut self, wrench: &mut Wrench) -> u32 {
        let mut should_build_yaml = false;

        // If YAML isn't read yet, or watching source file, reload from disk.
        if self.yaml_string.is_empty() || self.watch_source {
            self.yaml_string = std::fs::read_to_string(&self.yaml_path)
                 .unwrap_or_else(|_| panic!("YAML '{:?}' doesn't exist", self.yaml_path));
            should_build_yaml = true;
        }

        // Evaluate conditions that require parsing the YAML.
        if self.built_frame != self.requested_frame {
            // Requested frame has changed
            should_build_yaml = true;
        }

        // Build the DL from YAML if required
        if should_build_yaml {
            self.build(wrench);
        }

        // Determine whether to send a new DL, or just refresh.
        if should_build_yaml || wrench.should_rebuild_display_lists() {
            wrench.begin_frame();
            wrench.send_lists(
                &mut self.frame_count,
                self.display_lists.clone(),
                &self.scroll_offsets,
            );
        } else {
            wrench.refresh();
        }

        self.frame_count
    }

    fn next_frame(&mut self) {
        let mut max_frame_count = 0;
        if let Some(ref keyframes) = self.keyframes {
            for (_, values) in keyframes.as_hash().unwrap() {
                max_frame_count = max_frame_count.max(values.as_vec().unwrap().len());
            }
        }
        if self.requested_frame + 1 < max_frame_count {
            self.requested_frame += 1;
        }
    }

    fn prev_frame(&mut self) {
        if self.requested_frame > 0 {
            self.requested_frame -= 1;
        }
    }
}
