use crate::core::text::LineHeight;
use crate::core::{Pixels, Point, Rectangle, Size, Transformation, Vector};
use crate::graphics::geometry::fill::{self, Fill};
use crate::graphics::geometry::stroke::{self, Stroke};
use crate::graphics::geometry::{Path, Style, Text};
use crate::graphics::Gradient;
use crate::primitive::{self, Primitive};

pub struct Frame {
    size: Size,
    transform: tiny_skia::Transform,
    stack: Vec<tiny_skia::Transform>,
    primitives: Vec<Primitive>,
}

impl Frame {
    pub fn new(size: Size) -> Self {
        Self {
            size,
            transform: tiny_skia::Transform::identity(),
            stack: Vec::new(),
            primitives: Vec::new(),
        }
    }

    pub fn width(&self) -> f32 {
        self.size.width
    }

    pub fn height(&self) -> f32 {
        self.size.height
    }

    pub fn size(&self) -> Size {
        self.size
    }

    pub fn center(&self) -> Point {
        Point::new(self.size.width / 2.0, self.size.height / 2.0)
    }

    pub fn fill(&mut self, path: &Path, fill: impl Into<Fill>) {
        let Some(path) =
            convert_path(path).and_then(|path| path.transform(self.transform))
        else {
            return;
        };

        let fill = fill.into();

        let mut paint = into_paint(fill.style);
        paint.shader.transform(self.transform);

        self.primitives
            .push(Primitive::Custom(primitive::Custom::Fill {
                path,
                paint,
                rule: into_fill_rule(fill.rule),
            }));
    }

    pub fn fill_rectangle(
        &mut self,
        top_left: Point,
        size: Size,
        fill: impl Into<Fill>,
    ) {
        let Some(path) = convert_path(&Path::rectangle(top_left, size))
            .and_then(|path| path.transform(self.transform))
        else {
            return;
        };

        let fill = fill.into();

        let mut paint = tiny_skia::Paint {
            anti_alias: false,
            ..into_paint(fill.style)
        };
        paint.shader.transform(self.transform);

        self.primitives
            .push(Primitive::Custom(primitive::Custom::Fill {
                path,
                paint,
                rule: into_fill_rule(fill.rule),
            }));
    }

    pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) {
        let Some(path) =
            convert_path(path).and_then(|path| path.transform(self.transform))
        else {
            return;
        };

        let stroke = stroke.into();
        let skia_stroke = into_stroke(&stroke);

        let mut paint = into_paint(stroke.style);
        paint.shader.transform(self.transform);

        self.primitives
            .push(Primitive::Custom(primitive::Custom::Stroke {
                path,
                paint,
                stroke: skia_stroke,
            }));
    }

    pub fn fill_text(&mut self, text: impl Into<Text>) {
        let text = text.into();

        let (scale_x, scale_y) = self.transform.get_scale();

        if self.transform.is_scale_translate()
            && scale_x == scale_y
            && scale_x > 0.0
            && scale_y > 0.0
        {
            let (position, size, line_height) = if self.transform.is_identity()
            {
                (text.position, text.size, text.line_height)
            } else {
                let mut position = [tiny_skia::Point {
                    x: text.position.x,
                    y: text.position.y,
                }];

                self.transform.map_points(&mut position);

                let size = text.size.0 * scale_y;

                let line_height = match text.line_height {
                    LineHeight::Absolute(size) => {
                        LineHeight::Absolute(Pixels(size.0 * scale_y))
                    }
                    LineHeight::Relative(factor) => {
                        LineHeight::Relative(factor)
                    }
                };

                (
                    Point::new(position[0].x, position[0].y),
                    size.into(),
                    line_height,
                )
            };

            let bounds = Rectangle {
                x: position.x,
                y: position.y,
                width: f32::INFINITY,
                height: f32::INFINITY,
            };

            // TODO: Honor layering!
            self.primitives.push(Primitive::Text {
                content: text.content,
                bounds,
                color: text.color,
                size,
                line_height,
                font: text.font,
                horizontal_alignment: text.horizontal_alignment,
                vertical_alignment: text.vertical_alignment,
                shaping: text.shaping,
                clip_bounds: Rectangle::with_size(Size::INFINITY),
            });
        } else {
            text.draw_with(|path, color| self.fill(&path, color));
        }
    }

    pub fn push_transform(&mut self) {
        self.stack.push(self.transform);
    }

    pub fn pop_transform(&mut self) {
        self.transform = self.stack.pop().expect("Pop transform");
    }

    pub fn clip(&mut self, frame: Self, at: Point) {
        self.primitives.push(Primitive::Transform {
            transformation: Transformation::translate(at.x, at.y),
            content: Box::new(frame.into_primitive()),
        });
    }

    pub fn translate(&mut self, translation: Vector) {
        self.transform =
            self.transform.pre_translate(translation.x, translation.y);
    }

    pub fn rotate(&mut self, angle: f32) {
        self.transform = self
            .transform
            .pre_concat(tiny_skia::Transform::from_rotate(angle.to_degrees()));
    }

    pub fn scale(&mut self, scale: impl Into<f32>) {
        let scale = scale.into();

        self.scale_nonuniform(Vector { x: scale, y: scale });
    }

    pub fn scale_nonuniform(&mut self, scale: impl Into<Vector>) {
        let scale = scale.into();

        self.transform = self.transform.pre_scale(scale.x, scale.y);
    }

    pub fn into_primitive(self) -> Primitive {
        Primitive::Clip {
            bounds: Rectangle::new(Point::ORIGIN, self.size),
            content: Box::new(Primitive::Group {
                primitives: self.primitives,
            }),
        }
    }
}

fn convert_path(path: &Path) -> Option<tiny_skia::Path> {
    use iced_graphics::geometry::path::lyon_path;

    let mut builder = tiny_skia::PathBuilder::new();
    let mut last_point = lyon_path::math::Point::default();

    for event in path.raw() {
        match event {
            lyon_path::Event::Begin { at } => {
                builder.move_to(at.x, at.y);

                last_point = at;
            }
            lyon_path::Event::Line { from, to } => {
                if last_point != from {
                    builder.move_to(from.x, from.y);
                }

                builder.line_to(to.x, to.y);

                last_point = to;
            }
            lyon_path::Event::Quadratic { from, ctrl, to } => {
                if last_point != from {
                    builder.move_to(from.x, from.y);
                }

                builder.quad_to(ctrl.x, ctrl.y, to.x, to.y);

                last_point = to;
            }
            lyon_path::Event::Cubic {
                from,
                ctrl1,
                ctrl2,
                to,
            } => {
                if last_point != from {
                    builder.move_to(from.x, from.y);
                }

                builder
                    .cubic_to(ctrl1.x, ctrl1.y, ctrl2.x, ctrl2.y, to.x, to.y);

                last_point = to;
            }
            lyon_path::Event::End { close, .. } => {
                if close {
                    builder.close();
                }
            }
        }
    }

    let result = builder.finish();

    #[cfg(debug_assertions)]
    if result.is_none() {
        log::warn!("Invalid path: {:?}", path.raw());
    }

    result
}

pub fn into_paint(style: Style) -> tiny_skia::Paint<'static> {
    tiny_skia::Paint {
        shader: match style {
            Style::Solid(color) => tiny_skia::Shader::SolidColor(
                tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a)
                    .expect("Create color"),
            ),
            Style::Gradient(gradient) => match gradient {
                Gradient::Linear(linear) => {
                    let stops: Vec<tiny_skia::GradientStop> = linear
                        .stops
                        .into_iter()
                        .flatten()
                        .map(|stop| {
                            tiny_skia::GradientStop::new(
                                stop.offset,
                                tiny_skia::Color::from_rgba(
                                    stop.color.b,
                                    stop.color.g,
                                    stop.color.r,
                                    stop.color.a,
                                )
                                .expect("Create color"),
                            )
                        })
                        .collect();

                    tiny_skia::LinearGradient::new(
                        tiny_skia::Point {
                            x: linear.start.x,
                            y: linear.start.y,
                        },
                        tiny_skia::Point {
                            x: linear.end.x,
                            y: linear.end.y,
                        },
                        if stops.is_empty() {
                            vec![tiny_skia::GradientStop::new(
                                0.0,
                                tiny_skia::Color::BLACK,
                            )]
                        } else {
                            stops
                        },
                        tiny_skia::SpreadMode::Pad,
                        tiny_skia::Transform::identity(),
                    )
                    .expect("Create linear gradient")
                }
            },
        },
        anti_alias: true,
        ..Default::default()
    }
}

pub fn into_fill_rule(rule: fill::Rule) -> tiny_skia::FillRule {
    match rule {
        fill::Rule::EvenOdd => tiny_skia::FillRule::EvenOdd,
        fill::Rule::NonZero => tiny_skia::FillRule::Winding,
    }
}

pub fn into_stroke(stroke: &Stroke<'_>) -> tiny_skia::Stroke {
    tiny_skia::Stroke {
        width: stroke.width,
        line_cap: match stroke.line_cap {
            stroke::LineCap::Butt => tiny_skia::LineCap::Butt,
            stroke::LineCap::Square => tiny_skia::LineCap::Square,
            stroke::LineCap::Round => tiny_skia::LineCap::Round,
        },
        line_join: match stroke.line_join {
            stroke::LineJoin::Miter => tiny_skia::LineJoin::Miter,
            stroke::LineJoin::Round => tiny_skia::LineJoin::Round,
            stroke::LineJoin::Bevel => tiny_skia::LineJoin::Bevel,
        },
        dash: if stroke.line_dash.segments.is_empty() {
            None
        } else {
            tiny_skia::StrokeDash::new(
                stroke.line_dash.segments.into(),
                stroke.line_dash.offset as f32,
            )
        },
        ..Default::default()
    }
}
