/* 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 crate::api::TileSize;
use crate::api::units::*;
use crate::segment::EdgeAaSegmentMask;
use euclid::{point2, size2};
use std::i32;
use std::ops::Range;

/// If repetitions are far enough apart that only one is within
/// the primitive rect, then we can simplify the parameters and
/// treat the primitive as not repeated.
/// This can let us avoid unnecessary work later to handle some
/// of the parameters.
pub fn simplify_repeated_primitive(
    stretch_size: &LayoutSize,
    tile_spacing: &mut LayoutSize,
    prim_rect: &mut LayoutRect,
) {
    let stride = *stretch_size + *tile_spacing;

    if stride.width >= prim_rect.width() {
        tile_spacing.width = 0.0;
        prim_rect.max.x = f32::min(prim_rect.min.x + stretch_size.width, prim_rect.max.x);
    }
    if stride.height >= prim_rect.height() {
        tile_spacing.height = 0.0;
        prim_rect.max.y = f32::min(prim_rect.min.y + stretch_size.height, prim_rect.max.y);
    }
}

pub struct Repetition {
    pub origin: LayoutPoint,
    pub edge_flags: EdgeAaSegmentMask,
}

pub struct RepetitionIterator {
    current_x: i32,
    x_count: i32,
    current_y: i32,
    y_count: i32,
    row_flags: EdgeAaSegmentMask,
    current_origin: LayoutPoint,
    initial_origin: LayoutPoint,
    stride: LayoutSize,
}

impl RepetitionIterator {
    pub fn num_repetitions(&self) -> usize {
        (self.y_count * self.x_count) as usize
    }
}

impl Iterator for RepetitionIterator {
    type Item = Repetition;

    fn next(&mut self) -> Option<Self::Item> {
        if self.current_x == self.x_count {
            self.current_y += 1;
            if self.current_y >= self.y_count {
                return None;
            }
            self.current_x = 0;

            self.row_flags = EdgeAaSegmentMask::empty();
            if self.current_y == self.y_count - 1 {
                self.row_flags |= EdgeAaSegmentMask::BOTTOM;
            }

            self.current_origin.x = self.initial_origin.x;
            self.current_origin.y += self.stride.height;
        }

        let mut edge_flags = self.row_flags;
        if self.current_x == 0 {
            edge_flags |= EdgeAaSegmentMask::LEFT;
        }

        if self.current_x == self.x_count - 1 {
            edge_flags |= EdgeAaSegmentMask::RIGHT;
        }

        let repetition = Repetition {
            origin: self.current_origin,
            edge_flags,
        };

        self.current_origin.x += self.stride.width;
        self.current_x += 1;

        Some(repetition)
    }
}

pub fn repetitions(
    prim_rect: &LayoutRect,
    visible_rect: &LayoutRect,
    stride: LayoutSize,
) -> RepetitionIterator {
    let visible_rect = match prim_rect.intersection(&visible_rect) {
        Some(rect) => rect,
        None => {
            return RepetitionIterator {
                current_origin: LayoutPoint::zero(),
                initial_origin: LayoutPoint::zero(),
                current_x: 0,
                current_y: 0,
                x_count: 0,
                y_count: 0,
                stride,
                row_flags: EdgeAaSegmentMask::empty(),
            }
        }
    };

    assert!(stride.width > 0.0);
    assert!(stride.height > 0.0);

    let nx = if visible_rect.min.x > prim_rect.min.x {
        f32::floor((visible_rect.min.x - prim_rect.min.x) / stride.width)
    } else {
        0.0
    };

    let ny = if visible_rect.min.y > prim_rect.min.y {
        f32::floor((visible_rect.min.y - prim_rect.min.y) / stride.height)
    } else {
        0.0
    };

    let x0 = prim_rect.min.x + nx * stride.width;
    let y0 = prim_rect.min.y + ny * stride.height;

    let x_most = visible_rect.max.x;
    let y_most = visible_rect.max.y;

    let x_count = f32::ceil((x_most - x0) / stride.width) as i32;
    let y_count = f32::ceil((y_most - y0) / stride.height) as i32;

    let mut row_flags = EdgeAaSegmentMask::TOP;
    if y_count == 1 {
        row_flags |= EdgeAaSegmentMask::BOTTOM;
    }

    RepetitionIterator {
        current_origin: LayoutPoint::new(x0, y0),
        initial_origin: LayoutPoint::new(x0, y0),
        current_x: 0,
        current_y: 0,
        x_count,
        y_count,
        row_flags,
        stride,
    }
}

#[derive(Debug)]
pub struct Tile {
    pub rect: LayoutRect,
    pub offset: TileOffset,
    pub edge_flags: EdgeAaSegmentMask,
}

#[derive(Debug)]
pub struct TileIteratorExtent {
    /// Range of visible tiles to iterate over in number of tiles.
    tile_range: Range<i32>,
    /// Range of tiles of the full image including tiles that are culled out.
    image_tiles: Range<i32>,
    /// Size of the first tile in layout space.
    first_tile_layout_size: f32,
    /// Size of the last tile in layout space.
    last_tile_layout_size: f32,
    /// Position of blob point (0, 0) in layout space.
    layout_tiling_origin: f32,
    /// Position of the top-left corner of the primitive rect in layout space.
    layout_prim_start: f32,
}

#[derive(Debug)]
pub struct TileIterator {
    current_tile: TileOffset,
    x: TileIteratorExtent,
    y: TileIteratorExtent,
    regular_tile_size: LayoutSize,
}

impl Iterator for TileIterator {
    type Item = Tile;

    fn next(&mut self) -> Option<Self::Item> {
        // If we reach the end of a row, reset to the beginning of the next row.
        if self.current_tile.x >= self.x.tile_range.end {
            self.current_tile.y += 1;
            self.current_tile.x = self.x.tile_range.start;
        }

        // Stop iterating if we reach the last tile. We may start here if there
        // were no tiles to iterate over.
        if self.current_tile.x >= self.x.tile_range.end || self.current_tile.y >= self.y.tile_range.end {
            return None;
        }

        let tile_offset = self.current_tile;

        let mut segment_rect = LayoutRect::from_origin_and_size(
            LayoutPoint::new(
                self.x.layout_tiling_origin + tile_offset.x as f32 * self.regular_tile_size.width,
                self.y.layout_tiling_origin + tile_offset.y as f32 * self.regular_tile_size.height,
            ),
            self.regular_tile_size,
        );

        let mut edge_flags = EdgeAaSegmentMask::empty();

        if tile_offset.x == self.x.image_tiles.start {
            edge_flags |= EdgeAaSegmentMask::LEFT;
            segment_rect.min.x = self.x.layout_prim_start;
            // TODO(nical) we may not need to do this.
            segment_rect.max.x = segment_rect.min.x + self.x.first_tile_layout_size;
        }
        if tile_offset.x == self.x.image_tiles.end - 1 {
            edge_flags |= EdgeAaSegmentMask::RIGHT;
            segment_rect.max.x = segment_rect.min.x + self.x.last_tile_layout_size;
        }

        if tile_offset.y == self.y.image_tiles.start {
            segment_rect.min.y = self.y.layout_prim_start;
            segment_rect.max.y = segment_rect.min.y + self.y.first_tile_layout_size;
            edge_flags |= EdgeAaSegmentMask::TOP;
        }
        if tile_offset.y == self.y.image_tiles.end - 1 {
            segment_rect.max.y = segment_rect.min.y + self.y.last_tile_layout_size;
            edge_flags |= EdgeAaSegmentMask::BOTTOM;
        }

        assert!(tile_offset.y < self.y.tile_range.end);
        let tile = Tile {
            rect: segment_rect,
            offset: tile_offset,
            edge_flags,
        };

        self.current_tile.x += 1;

        Some(tile)
    }
}

pub fn tiles(
    prim_rect: &LayoutRect,
    visible_rect: &LayoutRect,
    image_rect: &DeviceIntRect,
    device_tile_size: i32,
) -> TileIterator {
    // The image resource is tiled. We have to generate an image primitive
    // for each tile.
    // We need to do this because the image is broken up into smaller tiles in the texture
    // cache and the image shader is not able to work with this type of sparse representation.

    // The tiling logic works as follows:
    //
    //  +-#################-+  -+
    //  | #//|    |    |//# |   | image size
    //  | #//|    |    |//# |   |
    //  +-#--+----+----+--#-+   |  -+
    //  | #//|    |    |//# |   |   | regular tile size
    //  | #//|    |    |//# |   |   |
    //  +-#--+----+----+--#-+   |  -+-+
    //  | #//|////|////|//# |   |     | "leftover" height
    //  | ################# |  -+  ---+
    //  +----+----+----+----+
    //
    // In the ascii diagram above, a large image is split into tiles of almost regular size.
    // The tiles on the edges (hatched in the diagram) can be smaller than the regular tiles
    // and are handled separately in the code (we'll call them boundary tiles).
    //
    // Each generated segment corresponds to a tile in the texture cache, with the
    // assumption that the boundary tiles are sized to fit their own irregular size in the
    // texture cache.
    //
    // Because we can have very large virtual images we iterate over the visible portion of
    // the image in layer space instead of iterating over all device tiles.

    let visible_rect = match prim_rect.intersection(&visible_rect) {
        Some(rect) => rect,
        None => {
            return TileIterator {
                current_tile: TileOffset::zero(),
                x: TileIteratorExtent {
                    tile_range: 0..0,
                    image_tiles: 0..0,
                    first_tile_layout_size: 0.0,
                    last_tile_layout_size: 0.0,
                    layout_tiling_origin: 0.0,
                    layout_prim_start: prim_rect.min.x,
                },
                y: TileIteratorExtent {
                    tile_range: 0..0,
                    image_tiles: 0..0,
                    first_tile_layout_size: 0.0,
                    last_tile_layout_size: 0.0,
                    layout_tiling_origin: 0.0,
                    layout_prim_start: prim_rect.min.y,
                },
                regular_tile_size: LayoutSize::zero(),
            }
        }
    };

    // Size of regular tiles in layout space.
    let layout_tile_size = LayoutSize::new(
        device_tile_size as f32 / image_rect.width() as f32 * prim_rect.width(),
        device_tile_size as f32 / image_rect.height() as f32 * prim_rect.height(),
    );

    // The decomposition logic is exactly the same on each axis so we reduce
    // this to a 1-dimensional problem in an attempt to make the code simpler.

    let x_extent = tiles_1d(
        layout_tile_size.width,
        visible_rect.x_range(),
        prim_rect.min.x,
        image_rect.x_range(),
        device_tile_size,
    );

    let y_extent = tiles_1d(
        layout_tile_size.height,
        visible_rect.y_range(),
        prim_rect.min.y,
        image_rect.y_range(),
        device_tile_size,
    );

    TileIterator {
        current_tile: point2(
            x_extent.tile_range.start,
            y_extent.tile_range.start,
        ),
        x: x_extent,
        y: y_extent,
        regular_tile_size: layout_tile_size,
    }
}

/// Decompose tiles along an arbitrary axis.
///
/// This does most of the heavy lifting needed for `tiles` but in a single dimension for
/// the sake of simplicity since the problem is independent on the x and y axes.
fn tiles_1d(
    layout_tile_size: f32,
    layout_visible_range: Range<f32>,
    layout_prim_start: f32,
    device_image_range: Range<i32>,
    device_tile_size: i32,
) -> TileIteratorExtent {
    // A few sanity checks.
    debug_assert!(layout_tile_size > 0.0);
    debug_assert!(layout_visible_range.end >= layout_visible_range.start);
    debug_assert!(device_image_range.end > device_image_range.start);
    debug_assert!(device_tile_size > 0);

    // Sizes of the boundary tiles in pixels.
    let first_tile_device_size = first_tile_size_1d(&device_image_range, device_tile_size);
    let last_tile_device_size = last_tile_size_1d(&device_image_range, device_tile_size);

    // [start..end[ Range of tiles of this row/column (in number of tiles) without
    // taking culling into account.
    let image_tiles = tile_range_1d(&device_image_range, device_tile_size);

    // Layout offset of tile (0, 0) with respect to the top-left corner of the display item.
    let layout_offset = device_image_range.start as f32 * layout_tile_size / device_tile_size as f32;
    // Position in layout space of tile (0, 0).
    let layout_tiling_origin = layout_prim_start - layout_offset;

    // [start..end[ Range of the visible tiles (because of culling).
    let visible_tiles_start = f32::floor((layout_visible_range.start - layout_tiling_origin) / layout_tile_size) as i32;
    let visible_tiles_end = f32::ceil((layout_visible_range.end - layout_tiling_origin) / layout_tile_size) as i32;

    // Combine the above two to get the tiles in the image that are visible this frame.
    let mut tiles_start = i32::max(image_tiles.start, visible_tiles_start);
    let tiles_end = i32::min(image_tiles.end, visible_tiles_end);
    if tiles_start > tiles_end {
        tiles_start = tiles_end;
    }

    // The size in layout space of the boundary tiles.
    let first_tile_layout_size = if tiles_start == image_tiles.start {
        first_tile_device_size as f32 * layout_tile_size / device_tile_size as f32
    } else {
        // boundary tile was culled out, so the new first tile is a regularly sized tile.
        layout_tile_size
    };

    // Same here.
    let last_tile_layout_size = if tiles_end == image_tiles.end {
        last_tile_device_size as f32 * layout_tile_size / device_tile_size as f32
    } else {
        layout_tile_size
    };

    TileIteratorExtent {
        tile_range: tiles_start..tiles_end,
        image_tiles,
        first_tile_layout_size,
        last_tile_layout_size,
        layout_tiling_origin,
        layout_prim_start,
    }
}

/// Compute the range of tiles (in number of tiles) that intersect the provided
/// image range (in pixels) in an arbitrary dimension.
///
/// ```ignore
///
///         0
///         :
///   #-+---+---+---+---+---+--#
///   # |   |   |   |   |   |  #
///   #-+---+---+---+---+---+--#
/// ^       :                   ^
///
///  +------------------------+  image_range
///        +---+  regular_tile_size
///
/// ```
fn tile_range_1d(
    image_range: &Range<i32>,
    regular_tile_size: i32,
) -> Range<i32> {
    // Integer division truncates towards zero so with negative values if the first/last
    // tile isn't a full tile we can get offset by one which we account for here.

    let mut start = image_range.start / regular_tile_size;
    if image_range.start % regular_tile_size < 0 {
        start -= 1;
    }

    let mut end = image_range.end / regular_tile_size;
    if image_range.end % regular_tile_size > 0 {
        end += 1;
    }

    start..end
}

// Sizes of the first boundary tile in pixels.
//
// It can be smaller than the regular tile size if the image is not a multiple
// of the regular tile size.
fn first_tile_size_1d(
    image_range: &Range<i32>,
    regular_tile_size: i32,
) -> i32 {
    // We have to account for how the % operation behaves for negative values.
    let image_size = image_range.end - image_range.start;
    i32::min(
        match image_range.start % regular_tile_size {
            //             .      #------+------+      .
            //             .      #//////|      |      .
            0 => regular_tile_size,
            //   (zero) -> 0      .   #--+------+      .
            //             .      .   #//|      |      .
            // %(m):                  ~~>
            m if m > 0 => regular_tile_size - m,
            //             .      .   #--+------+      0 <- (zero)
            //             .      .   #//|      |      .
            // %(m):                  <~~
            m => -m,
        },
        image_size
    )
}

// Sizes of the last boundary tile in pixels.
//
// It can be smaller than the regular tile size if the image is not a multiple
// of the regular tile size.
fn last_tile_size_1d(
    image_range: &Range<i32>,
    regular_tile_size: i32,
) -> i32 {
    // We have to account for how the modulo operation behaves for negative values.
    let image_size = image_range.end - image_range.start;
    i32::min(
        match image_range.end % regular_tile_size {
            //                    +------+------#      .
            // tiles:      .      |      |//////#      .
            0 => regular_tile_size,
            //             .      +------+--#   .      0 <- (zero)
            //             .      |      |//#   .      .
            // modulo (m):                   <~~
            m if m < 0 => regular_tile_size + m,
            //   (zero) -> 0      +------+--#   .      .
            //             .      |      |//#   .      .
            // modulo (m):                ~~>
            m => m,
        },
        image_size,
    )
}

pub fn compute_tile_rect(
    image_rect: &DeviceIntRect,
    regular_tile_size: TileSize,
    tile: TileOffset,
) -> DeviceIntRect {
    let regular_tile_size = regular_tile_size as i32;
    DeviceIntRect::from_origin_and_size(
        point2(
            compute_tile_origin_1d(image_rect.x_range(), regular_tile_size, tile.x as i32),
            compute_tile_origin_1d(image_rect.y_range(), regular_tile_size, tile.y as i32),
        ),
        size2(
            compute_tile_size_1d(image_rect.x_range(), regular_tile_size, tile.x as i32),
            compute_tile_size_1d(image_rect.y_range(), regular_tile_size, tile.y as i32),
        ),
    )
}

fn compute_tile_origin_1d(
    img_range: Range<i32>,
    regular_tile_size: i32,
    tile_offset: i32,
) -> i32 {
    let tile_range = tile_range_1d(&img_range, regular_tile_size);
    if tile_offset == tile_range.start {
        img_range.start
    } else {
        tile_offset * regular_tile_size
    }
}

// Compute the width and height in pixels of a tile depending on its position in the image.
pub fn compute_tile_size(
    image_rect: &DeviceIntRect,
    regular_tile_size: TileSize,
    tile: TileOffset,
) -> DeviceIntSize {
    let regular_tile_size = regular_tile_size as i32;
    size2(
        compute_tile_size_1d(image_rect.x_range(), regular_tile_size, tile.x as i32),
        compute_tile_size_1d(image_rect.y_range(), regular_tile_size, tile.y as i32),
    )
}

fn compute_tile_size_1d(
    img_range: Range<i32>,
    regular_tile_size: i32,
    tile_offset: i32,
) -> i32 {
    let tile_range = tile_range_1d(&img_range, regular_tile_size);

    // Most tiles are going to have base_size as width and height,
    // except for tiles around the edges that are shrunk to fit the image data.
    let actual_size = if tile_offset == tile_range.start {
        first_tile_size_1d(&img_range, regular_tile_size)
    } else if tile_offset == tile_range.end - 1 {
        last_tile_size_1d(&img_range, regular_tile_size)
    } else {
        regular_tile_size
    };

    assert!(actual_size > 0);

    actual_size
}

pub fn compute_tile_range(
    visible_area: &DeviceIntRect,
    tile_size: u16,
) -> TileRange {
    let tile_size = tile_size as i32;
    let x_range = tile_range_1d(&visible_area.x_range(), tile_size);
    let y_range = tile_range_1d(&visible_area.y_range(), tile_size);

    TileRange {
        min: point2(x_range.start, y_range.start),
        max: point2(x_range.end, y_range.end),
    }
}

pub fn for_each_tile_in_range(
    range: &TileRange,
    mut callback: impl FnMut(TileOffset),
) {
    for y in range.y_range() {
        for x in range.x_range() {
            callback(point2(x, y));
        }
    }
}

pub fn compute_valid_tiles_if_bounds_change(
    prev_rect: &DeviceIntRect,
    new_rect: &DeviceIntRect,
    tile_size: u16,
) -> Option<TileRange> {
    let intersection = match prev_rect.intersection(new_rect) {
        Some(rect) => rect,
        None => {
            return Some(TileRange::zero());
        }
    };

    let left = prev_rect.min.x != new_rect.min.x;
    let right = prev_rect.max.x != new_rect.max.x;
    let top = prev_rect.min.y != new_rect.min.y;
    let bottom = prev_rect.max.y != new_rect.max.y;

    if !left && !right && !top && !bottom {
        // Bounds have not changed.
        return None;
    }

    let tw = 1.0 / (tile_size as f32);
    let th = 1.0 / (tile_size as f32);

    let tiles = intersection
        .cast::<f32>()
        .scale(tw, th);

    let min_x = if left { f32::ceil(tiles.min.x) } else { f32::floor(tiles.min.x) };
    let min_y = if top { f32::ceil(tiles.min.y) } else { f32::floor(tiles.min.y) };
    let max_x = if right { f32::floor(tiles.max.x) } else { f32::ceil(tiles.max.x) };
    let max_y = if bottom { f32::floor(tiles.max.y) } else { f32::ceil(tiles.max.y) };

    Some(TileRange {
        min: point2(min_x as i32, min_y as i32),
        max: point2(max_x as i32, max_y as i32),
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashSet;
    use euclid::rect;

    // this checks some additional invariants
    fn checked_for_each_tile(
        prim_rect: &LayoutRect,
        visible_rect: &LayoutRect,
        device_image_rect: &DeviceIntRect,
        device_tile_size: i32,
        callback: &mut dyn FnMut(&LayoutRect, TileOffset, EdgeAaSegmentMask),
    ) {
        let mut coverage = LayoutRect::zero();
        let mut seen_tiles = HashSet::new();
        for tile in tiles(
            prim_rect,
            visible_rect,
            device_image_rect,
            device_tile_size,
        ) {
            // make sure we don't get sent duplicate tiles
            assert!(!seen_tiles.contains(&tile.offset));
            seen_tiles.insert(tile.offset);
            coverage = coverage.union(&tile.rect);
            assert!(prim_rect.contains_box(&tile.rect));
            callback(&tile.rect, tile.offset, tile.edge_flags);
        }
        assert!(prim_rect.contains_box(&coverage));
        assert!(coverage.contains_box(&visible_rect.intersection(&prim_rect).unwrap_or(LayoutRect::zero())));
    }

    #[test]
    fn basic() {
        let mut count = 0;
        checked_for_each_tile(&rect(0., 0., 1000., 1000.).to_box2d(),
            &rect(75., 75., 400., 400.).to_box2d(),
            &rect(0, 0, 400, 400).to_box2d(),
            36,
            &mut |_tile_rect, _tile_offset, _tile_flags| {
                count += 1;
            },
        );
        assert_eq!(count, 36);
    }

    #[test]
    fn empty() {
        let mut count = 0;
        checked_for_each_tile(&rect(0., 0., 74., 74.).to_box2d(),
            &rect(75., 75., 400., 400.).to_box2d(),
            &rect(0, 0, 400, 400).to_box2d(),
            36,
            &mut |_tile_rect, _tile_offset, _tile_flags| {
              count += 1;
            },
        );
        assert_eq!(count, 0);
    }

    #[test]
    fn test_tiles_1d() {
        // Exactly one full tile at positive offset.
        let result = tiles_1d(64.0, -10000.0..10000.0, 0.0, 0..64, 64);
        assert_eq!(result.tile_range.start, 0);
        assert_eq!(result.tile_range.end, 1);
        assert_eq!(result.first_tile_layout_size, 64.0);
        assert_eq!(result.last_tile_layout_size, 64.0);

        // Exactly one full tile at negative offset.
        let result = tiles_1d(64.0, -10000.0..10000.0, -64.0, -64..0, 64);
        assert_eq!(result.tile_range.start, -1);
        assert_eq!(result.tile_range.end, 0);
        assert_eq!(result.first_tile_layout_size, 64.0);
        assert_eq!(result.last_tile_layout_size, 64.0);

        // Two full tiles at negative and positive offsets.
        let result = tiles_1d(64.0, -10000.0..10000.0, -64.0, -64..64, 64);
        assert_eq!(result.tile_range.start, -1);
        assert_eq!(result.tile_range.end, 1);
        assert_eq!(result.first_tile_layout_size, 64.0);
        assert_eq!(result.last_tile_layout_size, 64.0);

        // One partial tile at positive offset, non-zero origin, culled out.
        let result = tiles_1d(64.0, -100.0..10.0, 64.0, 64..310, 64);
        assert_eq!(result.tile_range.start, result.tile_range.end);

        // Two tiles at negative and positive offsets, one of which is culled out.
        // The remaining tile is partially culled but it should still generate a full tile.
        let result = tiles_1d(64.0, 10.0..10000.0, -64.0, -64..64, 64);
        assert_eq!(result.tile_range.start, 0);
        assert_eq!(result.tile_range.end, 1);
        assert_eq!(result.first_tile_layout_size, 64.0);
        assert_eq!(result.last_tile_layout_size, 64.0);
        let result = tiles_1d(64.0, -10000.0..-10.0, -64.0, -64..64, 64);
        assert_eq!(result.tile_range.start, -1);
        assert_eq!(result.tile_range.end, 0);
        assert_eq!(result.first_tile_layout_size, 64.0);
        assert_eq!(result.last_tile_layout_size, 64.0);

        // Stretched tile in layout space device tile size is 64 and layout tile size is 128.
        // So the resulting tile sizes in layout space should be multiplied by two.
        let result = tiles_1d(128.0, -10000.0..10000.0, -64.0, -64..32, 64);
        assert_eq!(result.tile_range.start, -1);
        assert_eq!(result.tile_range.end, 1);
        assert_eq!(result.first_tile_layout_size, 128.0);
        assert_eq!(result.last_tile_layout_size, 64.0);

        // Two visible tiles (the rest is culled out).
        let result = tiles_1d(10.0, 0.0..20.0, 0.0, 0..64, 64);
        assert_eq!(result.tile_range.start, 0);
        assert_eq!(result.tile_range.end, 1);
        assert_eq!(result.first_tile_layout_size, 10.0);
        assert_eq!(result.last_tile_layout_size, 10.0);

        // Two visible tiles at negative layout offsets (the rest is culled out).
        let result = tiles_1d(10.0, -20.0..0.0, -20.0, 0..64, 64);
        assert_eq!(result.tile_range.start, 0);
        assert_eq!(result.tile_range.end, 1);
        assert_eq!(result.first_tile_layout_size, 10.0);
        assert_eq!(result.last_tile_layout_size, 10.0);
    }

    #[test]
    fn test_tile_range_1d() {
        assert_eq!(tile_range_1d(&(0..256), 256), 0..1);
        assert_eq!(tile_range_1d(&(0..257), 256), 0..2);
        assert_eq!(tile_range_1d(&(-1..257), 256), -1..2);
        assert_eq!(tile_range_1d(&(-256..256), 256), -1..1);
        assert_eq!(tile_range_1d(&(-20..-10), 6), -4..-1);
        assert_eq!(tile_range_1d(&(20..100), 256), 0..1);
    }

    #[test]
    fn test_first_last_tile_size_1d() {
        assert_eq!(first_tile_size_1d(&(0..10), 64), 10);
        assert_eq!(first_tile_size_1d(&(-20..0), 64), 20);

        assert_eq!(last_tile_size_1d(&(0..10), 64), 10);
        assert_eq!(last_tile_size_1d(&(-20..0), 64), 20);
    }

    #[test]
    fn doubly_partial_tiles() {
        // In the following tests the image is a single tile and none of the sides of the tile
        // align with the tile grid.
        // This can only happen when we have a single non-aligned partial tile and no regular
        // tiles.
        assert_eq!(first_tile_size_1d(&(300..310), 64), 10);
        assert_eq!(first_tile_size_1d(&(-20..-10), 64), 10);

        assert_eq!(last_tile_size_1d(&(300..310), 64), 10);
        assert_eq!(last_tile_size_1d(&(-20..-10), 64), 10);


        // One partial tile at positve offset, non-zero origin.
        let result = tiles_1d(64.0, -10000.0..10000.0, 0.0, 300..310, 64);
        assert_eq!(result.tile_range.start, 4);
        assert_eq!(result.tile_range.end, 5);
        assert_eq!(result.first_tile_layout_size, 10.0);
        assert_eq!(result.last_tile_layout_size, 10.0);
    }

    #[test]
    fn smaller_than_tile_size_at_origin() {
        let r = compute_tile_rect(
            &rect(0, 0, 80, 80).to_box2d(),
            256,
            point2(0, 0),
        );

        assert_eq!(r, rect(0, 0, 80, 80).to_box2d());
    }

    #[test]
    fn smaller_than_tile_size_with_offset() {
        let r = compute_tile_rect(
            &rect(20, 20, 80, 80).to_box2d(),
            256,
            point2(0, 0),
        );

        assert_eq!(r, rect(20, 20, 80, 80).to_box2d());
    }
}
