use criterion::{criterion_group, criterion_main, Criterion};
use nalgebra::{Matrix3, Vector2};

use rsvg::{
    filters::lighting::Normal,
    surface_utils::{
        iterators::{PixelRectangle, Pixels},
        shared_surface::{SharedImageSurface, SurfaceType},
        EdgeMode,
    },
    IRect,
};

/// Computes and returns the normal vector for the light filters.
fn normal(surface: &SharedImageSurface, bounds: IRect, x: u32, y: u32) -> Normal {
    assert!(x as i32 >= bounds.x0);
    assert!(y as i32 >= bounds.y0);
    assert!((x as i32) < bounds.x1);
    assert!((y as i32) < bounds.y1);

    // Get the correct sobel kernel and factor for the pixel position.
    // Performance note: it's possible to replace the matrices with normal arrays.
    #[rustfmt::skip]
    let (factor_x, kx, factor_y, ky) = match (x as i32, y as i32) {
        (x, y) if (x, y) == (bounds.x0, bounds.y0) => (
            2. / 3.,
            Matrix3::new(
                0,  0, 0,
                0, -2, 2,
                0, -1, 1,
            ),
            2. / 3.,
            Matrix3::new(
                0,  0,  0,
                0, -2, -1,
                0,  2,  1,
            ),
        ),
        (x, y) if (x + 1, y) == (bounds.x1, bounds.y0) => (
            2. / 3.,
            Matrix3::new(
                0,  0, 0,
                -2, 2, 0,
                -1, 1, 0,
            ),
            2. / 3.,
            Matrix3::new(
                 0,  0, 0,
                -1, -2, 0,
                 1,  2, 0,
            ),
        ),
        (x, y) if (x, y + 1) == (bounds.x0, bounds.y1) => (
            2. / 3.,
            Matrix3::new(
                0, -1, 1,
                0, -2, 2,
                0,  0, 0,
            ),
            2. / 3.,
            Matrix3::new(
                0, -2, -1,
                0,  2,  1,
                0,  0,  0,
            ),
        ),
        (x, y) if (x + 1, y + 1) == (bounds.x1, bounds.y1) => (
            2. / 3.,
            Matrix3::new(
                -1, 1, 0,
                -2, 2, 0,
                 0, 0, 0,
            ),
            2. / 3.,
            Matrix3::new(
                -1, -2, 0,
                 1,  2, 0,
                 0,  0, 0,
            ),
        ),
        (_, y) if y == bounds.y0 => (
            1. / 3.,
            Matrix3::new(
                 0, 0, 0,
                -2, 0, 2,
                -1, 0, 1,
            ),
            1. / 2.,
            Matrix3::new(
                 0,  0,  0,
                -1, -2, -1,
                 1,  2,  1,
            ),
        ),
        (x, _) if x == bounds.x0 => (
            1. / 2.,
            Matrix3::new(
                0, -1, 1,
                0, -2, 2,
                0, -1, 1,
            ),
            1. / 3.,
            Matrix3::new(
                0, -2, -1,
                0,  0,  0,
                0,  2,  1,
            ),
        ),
        (x, _) if x + 1 == bounds.x1 => (
            1. / 2.,
            Matrix3::new(
                -1, 1, 0,
                -2, 2, 0,
                -1, 1, 0,
            ),
            1. / 3.,
            Matrix3::new(
                -1, -2, 0,
                 0,  0, 0,
                 1,  2, 0,
            ),
        ),
        (_, y) if y + 1 == bounds.y1 => (
            1. / 3.,
            Matrix3::new(
                -1, 0, 1,
                -2, 0, 2,
                 0, 0, 0,
            ),
            1. / 2.,
            Matrix3::new(
                -1, -2, -1,
                 1,  2,  1,
                 0,  0,  0,
            ),
        ),
        _ => (
            1. / 4.,
            Matrix3::new(
                -1, 0, 1,
                -2, 0, 2,
                -1, 0, 1,
            ),
            1. / 4.,
            Matrix3::new(
                -1, -2, -1,
                 0,  0,  0,
                 1,  2,  1,
            ),
        ),
    };

    let kernel_bounds = IRect::new(x as i32 - 1, y as i32 - 1, x as i32 + 2, y as i32 + 2);

    let mut nx = 0;
    let mut ny = 0;
    for (x, y, pixel) in PixelRectangle::within(surface, bounds, kernel_bounds, EdgeMode::None) {
        let kernel_x = (x - kernel_bounds.x0) as usize;
        let kernel_y = (y - kernel_bounds.y0) as usize;

        nx += i16::from(pixel.a) * kx[(kernel_y, kernel_x)];
        ny += i16::from(pixel.a) * ky[(kernel_y, kernel_x)];
    }

    // Negative nx and ny to account for the different coordinate system.
    Normal {
        factor: Vector2::new(factor_x, factor_y),
        normal: Vector2::new(-nx, -ny),
    }
}

const SURFACE_SIDE: i32 = 512;
const BOUNDS: IRect = IRect {
    x0: 64,
    y0: 64,
    x1: 64 + 64,
    y1: 64 + 64,
};

fn bench_normal(c: &mut Criterion) {
    c.bench_function("normal", |b| {
        let surface =
            SharedImageSurface::empty(SURFACE_SIDE, SURFACE_SIDE, SurfaceType::SRgb).unwrap();

        b.iter(|| {
            let mut z = 0;
            for (x, y, _pixel) in Pixels::within(&surface, BOUNDS) {
                let n = normal(&surface, BOUNDS, x, y);
                z += n.normal.x;
            }
            z
        })
    });

    c.bench_function("normal unrolled", |b| {
        let surface =
            SharedImageSurface::empty(SURFACE_SIDE, SURFACE_SIDE, SurfaceType::SRgb).unwrap();

        b.iter(|| {
            let mut z = 0;

            // Top left.
            {
                let n = Normal::top_left(&surface, BOUNDS);
                z += n.normal.x;
            }

            // Top right.
            {
                let n = Normal::top_right(&surface, BOUNDS);
                z += n.normal.x;
            }

            // Bottom left.
            {
                let n = Normal::bottom_left(&surface, BOUNDS);
                z += n.normal.x;
            }

            // Bottom right.
            {
                let n = Normal::bottom_right(&surface, BOUNDS);
                z += n.normal.x;
            }

            // Top row.
            for x in BOUNDS.x0 as u32 + 1..BOUNDS.x1 as u32 - 1 {
                let n = Normal::top_row(&surface, BOUNDS, x);
                z += n.normal.x;
            }

            // Bottom row.
            for x in BOUNDS.x0 as u32 + 1..BOUNDS.x1 as u32 - 1 {
                let n = Normal::bottom_row(&surface, BOUNDS, x);
                z += n.normal.x;
            }

            // Left column.
            for y in BOUNDS.y0 as u32 + 1..BOUNDS.y1 as u32 - 1 {
                let n = Normal::left_column(&surface, BOUNDS, y);
                z += n.normal.x;
            }

            // Right column.
            for y in BOUNDS.y0 as u32 + 1..BOUNDS.y1 as u32 - 1 {
                let n = Normal::right_column(&surface, BOUNDS, y);
                z += n.normal.x;
            }

            // Interior pixels.
            for y in BOUNDS.y0 as u32 + 1..BOUNDS.y1 as u32 - 1 {
                for x in BOUNDS.x0 as u32 + 1..BOUNDS.x1 as u32 - 1 {
                    let n = Normal::interior(&surface, BOUNDS, x, y);
                    z += n.normal.x;
                }
            }

            z
        })
    });
}

criterion_group!(benches, bench_normal);
criterion_main!(benches);
