// Copyright 2006 The Android Open Source Project
// Copyright 2020 Yevhenii Reizner
//
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use core::convert::TryFrom;

use tiny_skia_path::{SaturateCast, ScreenIntRect};

use crate::{FillRule, IntRect, LengthU32, Path, Rect};

use crate::blitter::Blitter;
use crate::edge::{Edge, LineEdge};
use crate::edge_builder::{BasicEdgeBuilder, ShiftedIntRect};
use crate::fixed_point::{fdot16, fdot6, FDot16};

#[cfg(all(not(feature = "std"), feature = "no-std-float"))]
use tiny_skia_path::NoStdFloat;

pub fn fill_path(
    path: &Path,
    fill_rule: FillRule,
    clip: &ScreenIntRect,
    blitter: &mut dyn Blitter,
) -> Option<()> {
    let ir = conservative_round_to_int(&path.bounds())?;

    let path_contained_in_clip = if let Some(bounds) = ir.to_screen_int_rect() {
        clip.contains(&bounds)
    } else {
        // If bounds cannot be converted into ScreenIntRect,
        // the path is out of clip.
        false
    };

    // TODO: SkScanClipper

    fill_path_impl(
        path,
        fill_rule,
        clip,
        ir.y(),
        ir.bottom(),
        0,
        path_contained_in_clip,
        blitter,
    )
}

// Conservative rounding function, which effectively nudges the int-rect to be slightly larger
// than Rect::round() might have produced. This is a safety-net for the scan-converter, which
// inspects the returned int-rect, and may disable clipping (for speed) if it thinks all of the
// edges will fit inside the clip's bounds. The scan-converter introduces slight numeric errors
// due to accumulated += of the slope, so this function is used to return a conservatively large
// int-bounds, and thus we will only disable clipping if we're sure the edges will stay in-bounds.
fn conservative_round_to_int(src: &Rect) -> Option<IntRect> {
    // We must use `from_ltrb`, otherwise rounding will be incorrect.
    IntRect::from_ltrb(
        round_down_to_int(src.left()),
        round_down_to_int(src.top()),
        round_up_to_int(src.right()),
        round_up_to_int(src.bottom()),
    )
}

// Bias used for conservative rounding of float rects to int rects, to nudge the irects a little
// larger, so we don't "think" a path's bounds are inside a clip, when (due to numeric drift in
// the scan-converter) we might walk beyond the predicted limits.
//
// This value has been determined trial and error: pick the smallest value (after the 0.5) that
// fixes any problematic cases (e.g. crbug.com/844457)
// NOTE: cubics appear to be the main reason for needing this slop. If we could (perhaps) have a
// more accurate walker for cubics, we may be able to reduce this fudge factor.
const CONSERVATIVE_ROUND_BIAS: f64 = 0.5 + 1.5 / fdot6::ONE as f64;

// Round the value down. This is used to round the top and left of a rectangle,
// and corresponds to the way the scan converter treats the top and left edges.
// It has a slight bias to make the "rounded" int smaller than a normal round, to create a more
// conservative int-bounds (larger) from a float rect.
fn round_down_to_int(x: f32) -> i32 {
    let mut xx = x as f64;
    xx -= CONSERVATIVE_ROUND_BIAS;
    i32::saturate_from(xx.ceil())
}

// Round the value up. This is used to round the right and bottom of a rectangle.
// It has a slight bias to make the "rounded" int smaller than a normal round, to create a more
// conservative int-bounds (larger) from a float rect.
fn round_up_to_int(x: f32) -> i32 {
    let mut xx = x as f64;
    xx += CONSERVATIVE_ROUND_BIAS;
    i32::saturate_from(xx.floor())
}

pub fn fill_path_impl(
    path: &Path,
    fill_rule: FillRule,
    clip_rect: &ScreenIntRect,
    mut start_y: i32,
    mut stop_y: i32,
    shift_edges_up: i32,
    path_contained_in_clip: bool,
    blitter: &mut dyn Blitter,
) -> Option<()> {
    let shifted_clip = ShiftedIntRect::new(clip_rect, shift_edges_up)?;
    let clip = if path_contained_in_clip {
        None
    } else {
        Some(&shifted_clip)
    };
    let mut edges = BasicEdgeBuilder::build_edges(path, clip, shift_edges_up)?;

    edges.sort_by(|a, b| {
        let mut value_a = a.as_line().first_y;
        let mut value_b = b.as_line().first_y;

        if value_a == value_b {
            value_a = a.as_line().x;
            value_b = b.as_line().x;
        }

        value_a.cmp(&value_b)
    });

    for i in 0..edges.len() {
        // 0 will be set later, so start with 1.
        edges[i].prev = Some(i as u32 + 0);
        edges[i].next = Some(i as u32 + 2);
    }

    const EDGE_HEAD_Y: i32 = i32::MIN;
    const EDGE_TAIL_Y: i32 = i32::MAX;

    edges.insert(
        0,
        Edge::Line(LineEdge {
            prev: None,
            next: Some(1),
            x: i32::MIN,
            first_y: EDGE_HEAD_Y,
            ..LineEdge::default()
        }),
    );

    edges.push(Edge::Line(LineEdge {
        prev: Some(edges.len() as u32 - 1),
        next: None,
        first_y: EDGE_TAIL_Y,
        ..LineEdge::default()
    }));

    start_y <<= shift_edges_up;
    stop_y <<= shift_edges_up;

    let top = shifted_clip.shifted().y() as i32;
    if !path_contained_in_clip && start_y < top {
        start_y = top;
    }

    let bottom = shifted_clip.shifted().bottom() as i32;
    if !path_contained_in_clip && stop_y > bottom as i32 {
        stop_y = bottom as i32;
    }

    let start_y = u32::try_from(start_y).ok()?;
    let stop_y = u32::try_from(stop_y).ok()?;

    // TODO: walk_simple_edges

    walk_edges(
        fill_rule,
        start_y,
        stop_y,
        shifted_clip.shifted().right(),
        &mut edges,
        blitter,
    );
    Some(())
}

// TODO: simplify!
fn walk_edges(
    fill_rule: FillRule,
    start_y: u32,
    stop_y: u32,
    right_clip: u32,
    edges: &mut [Edge],
    blitter: &mut dyn Blitter,
) {
    let mut curr_y = start_y;
    let winding_mask = if fill_rule == FillRule::EvenOdd {
        1
    } else {
        -1
    };

    loop {
        let mut w = 0i32;
        let mut left = 0u32;
        let mut prev_x = edges[0].x;

        let mut curr_idx = edges[0].next.unwrap() as usize;
        while edges[curr_idx].first_y <= curr_y as i32 {
            debug_assert!(edges[curr_idx].last_y >= curr_y as i32);

            let x = fdot16::round_to_i32(edges[curr_idx].x) as u32; // TODO: check

            if (w & winding_mask) == 0 {
                // we're starting interval
                left = x;
            }

            w += i32::from(edges[curr_idx].winding);

            if (w & winding_mask) == 0 {
                // we finished an interval
                if let Some(width) = LengthU32::new(x - left) {
                    blitter.blit_h(left, curr_y, width);
                }
            }

            let next_idx = edges[curr_idx].next.unwrap();
            let new_x;

            if edges[curr_idx].last_y == curr_y as i32 {
                // are we done with this edge?
                match &mut edges[curr_idx] {
                    Edge::Line(_) => {
                        remove_edge(curr_idx, edges);
                    }
                    Edge::Quadratic(ref mut quad) => {
                        if quad.curve_count > 0 && quad.update() {
                            new_x = quad.line.x;

                            if new_x < prev_x {
                                // ripple current edge backwards until it is x-sorted
                                backward_insert_edge_based_on_x(curr_idx, edges);
                            } else {
                                prev_x = new_x;
                            }
                        } else {
                            remove_edge(curr_idx, edges);
                        }
                    }
                    Edge::Cubic(ref mut cubic) => {
                        if cubic.curve_count < 0 && cubic.update() {
                            debug_assert!(cubic.line.first_y == curr_y as i32 + 1);

                            new_x = cubic.line.x;

                            if new_x < prev_x {
                                // ripple current edge backwards until it is x-sorted
                                backward_insert_edge_based_on_x(curr_idx, edges);
                            } else {
                                prev_x = new_x;
                            }
                        } else {
                            remove_edge(curr_idx, edges);
                        }
                    }
                }
            } else {
                debug_assert!(edges[curr_idx].last_y > curr_y as i32);
                new_x = edges[curr_idx].x + edges[curr_idx].dx;
                edges[curr_idx].x = new_x;

                if new_x < prev_x {
                    // ripple current edge backwards until it is x-sorted
                    backward_insert_edge_based_on_x(curr_idx, edges);
                } else {
                    prev_x = new_x;
                }
            }

            curr_idx = next_idx as usize;
        }

        if (w & winding_mask) != 0 {
            // was our right-edge culled away?
            if let Some(width) = LengthU32::new(right_clip - left) {
                blitter.blit_h(left, curr_y, width);
            }
        }

        curr_y += 1;
        if curr_y >= stop_y {
            break;
        }

        // now current edge points to the first edge with a Yint larger than curr_y
        insert_new_edges(curr_idx, curr_y as i32, edges);
    }
}

fn remove_edge(curr_idx: usize, edges: &mut [Edge]) {
    let prev = edges[curr_idx].prev.unwrap();
    let next = edges[curr_idx].next.unwrap();

    edges[prev as usize].next = Some(next);
    edges[next as usize].prev = Some(prev);
}

fn backward_insert_edge_based_on_x(curr_idx: usize, edges: &mut [Edge]) {
    let x = edges[curr_idx].x;
    let mut prev_idx = edges[curr_idx].prev.unwrap() as usize;
    while prev_idx != 0 {
        if edges[prev_idx].x > x {
            prev_idx = edges[prev_idx].prev.unwrap() as usize;
        } else {
            break;
        }
    }

    let next_idx = edges[prev_idx].next.unwrap() as usize;
    if next_idx != curr_idx {
        remove_edge(curr_idx, edges);
        insert_edge_after(curr_idx, prev_idx, edges);
    }
}

fn insert_edge_after(curr_idx: usize, after_idx: usize, edges: &mut [Edge]) {
    edges[curr_idx].prev = Some(after_idx as u32);
    edges[curr_idx].next = edges[after_idx].next;

    let after_next_idx = edges[after_idx].next.unwrap() as usize;
    edges[after_next_idx].prev = Some(curr_idx as u32);
    edges[after_idx].next = Some(curr_idx as u32);
}

// Start from the right side, searching backwards for the point to begin the new edge list
// insertion, marching forwards from here. The implementation could have started from the left
// of the prior insertion, and search to the right, or with some additional caching, binary
// search the starting point. More work could be done to determine optimal new edge insertion.
fn backward_insert_start(mut prev_idx: usize, x: FDot16, edges: &mut [Edge]) -> usize {
    while let Some(prev) = edges[prev_idx].prev {
        prev_idx = prev as usize;
        if edges[prev_idx].x <= x {
            break;
        }
    }

    prev_idx
}

fn insert_new_edges(mut new_idx: usize, curr_y: i32, edges: &mut [Edge]) {
    if edges[new_idx].first_y != curr_y {
        return;
    }

    let prev_idx = edges[new_idx].prev.unwrap() as usize;
    if edges[prev_idx].x <= edges[new_idx].x {
        return;
    }

    // find first x pos to insert
    let mut start_idx = backward_insert_start(prev_idx, edges[new_idx].x, edges);
    // insert the lot, fixing up the links as we go
    loop {
        let next_idx = edges[new_idx].next.unwrap() as usize;
        let mut keep_edge = false;
        loop {
            let after_idx = edges[start_idx].next.unwrap() as usize;
            if after_idx == new_idx {
                keep_edge = true;
                break;
            }

            if edges[after_idx].x >= edges[new_idx].x {
                break;
            }

            start_idx = after_idx;
        }

        if !keep_edge {
            remove_edge(new_idx, edges);
            insert_edge_after(new_idx, start_idx, edges);
        }

        start_idx = new_idx;
        new_idx = next_idx;

        if edges[new_idx].first_y != curr_y {
            break;
        }
    }
}
