// 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::{
    cache::SvgrCache,
    render::{Canvas, RenderState},
    ConvTransform, OptionLog,
};

pub fn fill(
    tree: &usvgr::Tree,
    fill: &usvgr::Fill,
    bbox: usvgr::PathBbox,
    path: &tiny_skia::Path,
    anti_alias: bool,
    blend_mode: tiny_skia::BlendMode,
    canvas: &mut Canvas,
    cache: &mut SvgrCache,
) -> Option<()> {
    let pattern_pixmap;

    let mut paint = tiny_skia::Paint::default();

    let opacity = fill.opacity;
    match fill.paint {
        usvgr::Paint::Color(c) => {
            paint.set_color_rgba8(c.red, c.green, c.blue, opacity.to_u8());
        }
        usvgr::Paint::LinearGradient(ref lg) => {
            prepare_linear(lg, opacity, bbox, &mut paint);
        }
        usvgr::Paint::RadialGradient(ref rg) => {
            prepare_radial(rg, opacity, bbox, &mut paint);
        }
        usvgr::Paint::Pattern(ref pattern) => {
            let global_ts = usvgr::Transform::from_native(canvas.transform);
            let (patt_pix, patt_ts) =
                prepare_pattern_pixmap(tree, pattern, &global_ts, bbox, cache)?;

            pattern_pixmap = patt_pix;
            paint.shader = prepare_pattern(&pattern_pixmap, patt_ts, opacity);
        }
    }

    paint.anti_alias = anti_alias;
    paint.blend_mode = blend_mode;

    let rule = if fill.rule == usvgr::FillRule::NonZero {
        tiny_skia::FillRule::Winding
    } else {
        tiny_skia::FillRule::EvenOdd
    };

    canvas
        .pixmap
        .fill_path(path, &paint, rule, canvas.transform, canvas.clip.as_ref());

    Some(())
}

pub fn stroke(
    tree: &usvgr::Tree,
    stroke: &Option<usvgr::Stroke>,
    bbox: usvgr::PathBbox,
    path: &tiny_skia::Path,
    anti_alias: bool,
    blend_mode: tiny_skia::BlendMode,
    canvas: &mut Canvas,
    cache: &mut SvgrCache,
) -> Option<()> {
    let pattern_pixmap;

    let mut paint = tiny_skia::Paint::default();
    let mut props = tiny_skia::Stroke::default();

    if let Some(ref stroke) = stroke {
        let opacity = stroke.opacity;
        match stroke.paint {
            usvgr::Paint::Color(c) => {
                paint.set_color_rgba8(c.red, c.green, c.blue, opacity.to_u8());
            }
            usvgr::Paint::LinearGradient(ref lg) => {
                prepare_linear(lg, opacity, bbox, &mut paint);
            }
            usvgr::Paint::RadialGradient(ref rg) => {
                prepare_radial(rg, opacity, bbox, &mut paint);
            }
            usvgr::Paint::Pattern(ref pattern) => {
                let global_ts = usvgr::Transform::from_native(canvas.transform);
                let (patt_pix, patt_ts) =
                    prepare_pattern_pixmap(tree, pattern, &global_ts, bbox, cache)?;

                pattern_pixmap = patt_pix;
                paint.shader = prepare_pattern(&pattern_pixmap, patt_ts, opacity);
            }
        }

        let stroke_cap = match stroke.linecap {
            usvgr::LineCap::Butt => tiny_skia::LineCap::Butt,
            usvgr::LineCap::Round => tiny_skia::LineCap::Round,
            usvgr::LineCap::Square => tiny_skia::LineCap::Square,
        };
        props.line_cap = stroke_cap;

        let stroke_join = match stroke.linejoin {
            usvgr::LineJoin::Miter => tiny_skia::LineJoin::Miter,
            usvgr::LineJoin::Round => tiny_skia::LineJoin::Round,
            usvgr::LineJoin::Bevel => tiny_skia::LineJoin::Bevel,
        };
        props.line_join = stroke_join;

        props.miter_limit = stroke.miterlimit.get() as f32;
        props.width = stroke.width.get() as f32;

        if let Some(ref list) = stroke.dasharray {
            let list: Vec<_> = list.iter().map(|n| *n as f32).collect();
            props.dash = tiny_skia::StrokeDash::new(list, stroke.dashoffset);
        }
    }

    paint.anti_alias = anti_alias;
    paint.blend_mode = blend_mode;

    canvas
        .pixmap
        .stroke_path(path, &paint, &props, canvas.transform, canvas.clip.as_ref());

    Some(())
}

fn prepare_linear(
    g: &usvgr::LinearGradient,
    opacity: usvgr::Opacity,
    bbox: usvgr::PathBbox,
    paint: &mut tiny_skia::Paint,
) -> Option<()> {
    let mode = match g.spread_method {
        usvgr::SpreadMethod::Pad => tiny_skia::SpreadMode::Pad,
        usvgr::SpreadMethod::Reflect => tiny_skia::SpreadMode::Reflect,
        usvgr::SpreadMethod::Repeat => tiny_skia::SpreadMode::Repeat,
    };

    let transform = {
        if g.units == usvgr::Units::ObjectBoundingBox {
            let bbox = bbox
                .to_rect()
                .log_none(|| log::warn!("Gradient on zero-sized shapes is not allowed."))?;

            let mut ts = usvgr::Transform::from_bbox(bbox);
            ts.append(&g.transform);
            ts.to_native()
        } else {
            g.transform.to_native()
        }
    };

    let mut points = Vec::with_capacity(g.stops.len());
    for stop in &g.stops {
        let alpha = stop.opacity * opacity;
        let color = tiny_skia::Color::from_rgba8(
            stop.color.red,
            stop.color.green,
            stop.color.blue,
            alpha.to_u8(),
        );
        points.push(tiny_skia::GradientStop::new(
            stop.offset.get() as f32,
            color,
        ))
    }

    let gradient = tiny_skia::LinearGradient::new(
        (g.x1 as f32, g.y1 as f32).into(),
        (g.x2 as f32, g.y2 as f32).into(),
        points,
        mode,
        transform,
    );

    if let Some(gradient) = gradient {
        paint.shader = gradient;
    }

    Some(())
}

fn prepare_radial(
    g: &usvgr::RadialGradient,
    opacity: usvgr::Opacity,
    bbox: usvgr::PathBbox,
    paint: &mut tiny_skia::Paint,
) -> Option<()> {
    let mode = match g.spread_method {
        usvgr::SpreadMethod::Pad => tiny_skia::SpreadMode::Pad,
        usvgr::SpreadMethod::Reflect => tiny_skia::SpreadMode::Reflect,
        usvgr::SpreadMethod::Repeat => tiny_skia::SpreadMode::Repeat,
    };

    let transform = {
        if g.units == usvgr::Units::ObjectBoundingBox {
            let bbox = bbox
                .to_rect()
                .log_none(|| log::warn!("Gradient on zero-sized shapes is not allowed."))?;

            let mut ts = usvgr::Transform::from_bbox(bbox);
            ts.append(&g.transform);
            ts.to_native()
        } else {
            g.transform.to_native()
        }
    };

    let mut points = Vec::with_capacity(g.stops.len());
    for stop in &g.stops {
        let alpha = stop.opacity * opacity;
        let color = tiny_skia::Color::from_rgba8(
            stop.color.red,
            stop.color.green,
            stop.color.blue,
            alpha.to_u8(),
        );
        points.push(tiny_skia::GradientStop::new(
            stop.offset.get() as f32,
            color,
        ))
    }

    let gradient = tiny_skia::RadialGradient::new(
        (g.fx as f32, g.fy as f32).into(),
        (g.cx as f32, g.cy as f32).into(),
        g.r.get() as f32,
        points,
        mode,
        transform,
    );

    if let Some(gradient) = gradient {
        paint.shader = gradient;
    }

    Some(())
}

fn prepare_pattern_pixmap(
    tree: &usvgr::Tree,
    pattern: &usvgr::Pattern,
    global_ts: &usvgr::Transform,
    bbox: usvgr::PathBbox,
    cache: &mut SvgrCache,
) -> Option<(tiny_skia::Pixmap, usvgr::Transform)> {
    let r = if pattern.units == usvgr::Units::ObjectBoundingBox {
        let bbox = bbox
            .to_rect()
            .log_none(|| log::warn!("Pattern on zero-sized shapes is not allowed."))?;

        pattern.rect.bbox_transform(bbox)
    } else {
        pattern.rect
    };

    let mut ts2 = global_ts.clone();
    ts2.append(&pattern.transform);
    let (sx, sy) = ts2.get_scale();

    let img_size = usvgr::Size::new(r.width() * sx, r.height() * sy)?.to_screen_size();
    let mut pixmap = tiny_skia::Pixmap::new(img_size.width(), img_size.height())?;
    let mut canvas = Canvas::from(pixmap.as_mut());

    canvas.scale(sx as f32, sy as f32);
    if let Some(vbox) = pattern.view_box {
        let ts = usvgr::utils::view_box_to_transform(vbox.rect, vbox.aspect, r.size());
        canvas.apply_transform(ts.to_native());
    } else if pattern.content_units == usvgr::Units::ObjectBoundingBox {
        // 'Note that this attribute has no effect if attribute `viewBox` is specified.'

        // We don't use Transform::from_bbox(bbox) because `x` and `y` should be
        // ignored for some reasons...
        canvas.scale(bbox.width() as f32, bbox.height() as f32);
    }

    crate::render::render_group(
        tree,
        &pattern.root,
        &mut RenderState::Ok,
        &mut canvas,
        cache,
    );

    let mut ts = usvgr::Transform::default();
    ts.append(&pattern.transform);
    ts.translate(r.x(), r.y());
    ts.scale(1.0 / sx, 1.0 / sy);

    Some((pixmap, ts))
}

fn prepare_pattern(
    pixmap: &tiny_skia::Pixmap,
    ts: usvgr::Transform,
    opacity: usvgr::Opacity,
) -> tiny_skia::Shader {
    tiny_skia::Pattern::new(
        pixmap.as_ref(),
        tiny_skia::SpreadMode::Repeat,
        tiny_skia::FilterQuality::Bicubic,
        opacity.get() as f32,
        ts.to_native(),
    )
}
