use std::{borrow::Cow, ops::RangeBounds};

use piet::{
    kurbo::{Affine, Insets, PathEl, Point, Rect, Shape, Size},
    HitTestPosition, TextAttribute, TextStorage,
};
use piet::{
    Color, Error, FixedGradient, FontFamily, HitTestPoint, ImageFormat, InterpolationMode,
    IntoBrush, LineMetric, RenderContext, StrokeStyle, Text, TextLayout, TextLayoutBuilder,
};

use piet_gpu_types::encoder::{Encode, Encoder};
use piet_gpu_types::scene::{
    Clip, CubicSeg, Element, FillColor, LineSeg, QuadSeg, SetFillMode, SetLineWidth, Transform,
};

use crate::text::Font;
pub use crate::text::{PathEncoder, PietGpuText, PietGpuTextLayout, PietGpuTextLayoutBuilder};

pub struct PietGpuImage;

pub struct PietGpuRenderContext {
    encoder: Encoder,
    elements: Vec<Element>,
    // Will probably need direct accesss to hal Device to create images etc.
    inner_text: PietGpuText,
    stroke_width: f32,
    fill_mode: FillMode,
    // We're tallying these cpu-side for expedience, but will probably
    // move this to some kind of readback from element processing.
    /// The count of elements that make it through to coarse rasterization.
    path_count: usize,
    /// The count of path segment elements.
    pathseg_count: usize,
    /// The count of transform elements.
    trans_count: usize,

    cur_transform: Affine,
    state_stack: Vec<State>,
    clip_stack: Vec<ClipElement>,
}

#[derive(Clone)]
pub enum PietGpuBrush {
    Solid(u32),
    Gradient,
}

#[derive(Default)]
struct State {
    /// The transform relative to the parent state.
    rel_transform: Affine,
    /// The transform at the parent state.
    ///
    /// This invariant should hold: transform * rel_transform = cur_transform
    transform: Affine,
    n_clip: usize,
}

struct ClipElement {
    /// Index of BeginClip element in element vec, for bbox fixup.
    begin_ix: usize,
    bbox: Option<Rect>,
}

#[derive(Clone, Copy, PartialEq)]
pub(crate) enum FillMode {
    // Fill path according to the non-zero winding rule.
    Nonzero = 0,
    // Fill stroked path.
    Stroke = 1,
}

const TOLERANCE: f64 = 0.25;

impl PietGpuRenderContext {
    pub fn new() -> PietGpuRenderContext {
        let encoder = Encoder::new();
        let elements = Vec::new();
        let font = Font::new();
        let inner_text = PietGpuText::new(font);
        let stroke_width = 0.0;
        PietGpuRenderContext {
            encoder,
            elements,
            inner_text,
            stroke_width,
            fill_mode: FillMode::Nonzero,
            path_count: 0,
            pathseg_count: 0,
            trans_count: 0,
            cur_transform: Affine::default(),
            state_stack: Vec::new(),
            clip_stack: Vec::new(),
        }
    }

    pub fn get_scene_buf(&mut self) -> &[u8] {
        self.elements.encode(&mut self.encoder);
        self.encoder.buf()
    }

    pub fn path_count(&self) -> usize {
        self.path_count
    }

    pub fn pathseg_count(&self) -> usize {
        self.pathseg_count
    }

    pub fn trans_count(&self) -> usize {
        self.trans_count
    }

    pub(crate) fn set_fill_mode(&mut self, fill_mode: FillMode) {
        if self.fill_mode != fill_mode {
            self.elements.push(Element::SetFillMode(SetFillMode {
                fill_mode: fill_mode as u32,
            }));
            self.fill_mode = fill_mode;
        }
    }
}

impl RenderContext for PietGpuRenderContext {
    type Brush = PietGpuBrush;
    type Image = PietGpuImage;
    type Text = PietGpuText;
    type TextLayout = PietGpuTextLayout;

    fn status(&mut self) -> Result<(), Error> {
        Ok(())
    }

    fn solid_brush(&mut self, color: Color) -> Self::Brush {
        // kernel4 expects colors encoded in alpha-premultiplied sRGB:
        //
        // [α,sRGB(α⋅R),sRGB(α⋅G),sRGB(α⋅B)]
        //
        // See also http://ssp.impulsetrain.com/gamma-premult.html.
        let (r, g, b, a) = color.as_rgba();
        let premul = Color::rgba(
            to_srgb(from_srgb(r) * a),
            to_srgb(from_srgb(g) * a),
            to_srgb(from_srgb(b) * a),
            a,
        );
        PietGpuBrush::Solid(premul.as_rgba_u32())
    }

    fn gradient(&mut self, _gradient: impl Into<FixedGradient>) -> Result<Self::Brush, Error> {
        Ok(Self::Brush::Gradient)
    }

    fn clear(&mut self, _color: Color) {}

    fn stroke(&mut self, shape: impl Shape, brush: &impl IntoBrush<Self>, width: f64) {
        let width_f32 = width as f32;
        if self.stroke_width != width_f32 {
            self.elements
                .push(Element::SetLineWidth(SetLineWidth { width: width_f32 }));
            self.stroke_width = width_f32;
        }
        self.set_fill_mode(FillMode::Stroke);
        let brush = brush.make_brush(self, || shape.bounding_box()).into_owned();
        match brush {
            PietGpuBrush::Solid(rgba_color) => {
                // Note: the bbox contribution of stroke becomes more complicated with miter joins.
                self.accumulate_bbox(|| shape.bounding_box() + Insets::uniform(width * 0.5));
                let path = shape.path_elements(TOLERANCE);
                self.encode_path(path, false);
                let stroke = FillColor { rgba_color };
                self.elements.push(Element::FillColor(stroke));
                self.path_count += 1;
            }
            _ => (),
        }
    }

    fn stroke_styled(
        &mut self,
        _shape: impl Shape,
        _brush: &impl IntoBrush<Self>,
        _width: f64,
        _style: &StrokeStyle,
    ) {
    }

    fn fill(&mut self, shape: impl Shape, brush: &impl IntoBrush<Self>) {
        let brush = brush.make_brush(self, || shape.bounding_box()).into_owned();
        if let PietGpuBrush::Solid(rgba_color) = brush {
            // Note: we might get a good speedup from using an approximate bounding box.
            // Perhaps that should be added to kurbo.
            self.accumulate_bbox(|| shape.bounding_box());
            let path = shape.path_elements(TOLERANCE);
            self.set_fill_mode(FillMode::Nonzero);
            self.encode_path(path, true);
            let fill = FillColor { rgba_color };
            self.elements.push(Element::FillColor(fill));
            self.path_count += 1;
        }
    }

    fn fill_even_odd(&mut self, _shape: impl Shape, _brush: &impl IntoBrush<Self>) {}

    fn clip(&mut self, shape: impl Shape) {
        self.set_fill_mode(FillMode::Nonzero);
        let path = shape.path_elements(TOLERANCE);
        self.encode_path(path, true);
        let begin_ix = self.elements.len();
        self.elements.push(Element::BeginClip(Clip {
            bbox: Default::default(),
        }));
        self.clip_stack.push(ClipElement {
            bbox: None,
            begin_ix,
        });
        self.path_count += 1;
        if let Some(tos) = self.state_stack.last_mut() {
            tos.n_clip += 1;
        }
    }

    fn text(&mut self) -> &mut Self::Text {
        &mut self.inner_text
    }

    fn draw_text(&mut self, layout: &Self::TextLayout, pos: impl Into<Point>) {
        layout.draw_text(self, pos.into());
    }

    fn save(&mut self) -> Result<(), Error> {
        self.state_stack.push(State {
            rel_transform: Affine::default(),
            transform: self.cur_transform,
            n_clip: 0,
        });
        Ok(())
    }

    fn restore(&mut self) -> Result<(), Error> {
        if let Some(state) = self.state_stack.pop() {
            if state.rel_transform != Affine::default() {
                let a_inv = state.rel_transform.inverse();
                self.encode_transform(to_scene_transform(a_inv));
            }
            self.cur_transform = state.transform;
            for _ in 0..state.n_clip {
                self.pop_clip();
            }
            Ok(())
        } else {
            Err(Error::StackUnbalance)
        }
    }

    fn finish(&mut self) -> Result<(), Error> {
        for _ in 0..self.clip_stack.len() {
            self.pop_clip();
        }
        Ok(())
    }

    fn transform(&mut self, transform: Affine) {
        self.encode_transform(to_scene_transform(transform));
        if let Some(tos) = self.state_stack.last_mut() {
            tos.rel_transform *= transform;
        }
        self.cur_transform *= transform;
    }

    fn make_image(
        &mut self,
        _width: usize,
        _height: usize,
        _buf: &[u8],
        _format: ImageFormat,
    ) -> Result<Self::Image, Error> {
        Ok(PietGpuImage)
    }

    fn draw_image(
        &mut self,
        _image: &Self::Image,
        _rect: impl Into<Rect>,
        _interp: InterpolationMode,
    ) {
    }

    fn draw_image_area(
        &mut self,
        _image: &Self::Image,
        _src_rect: impl Into<Rect>,
        _dst_rect: impl Into<Rect>,
        _interp: InterpolationMode,
    ) {
    }

    fn blurred_rect(&mut self, _rect: Rect, _blur_radius: f64, _brush: &impl IntoBrush<Self>) {}

    fn current_transform(&self) -> Affine {
        self.cur_transform
    }

    fn with_save(&mut self, f: impl FnOnce(&mut Self) -> Result<(), Error>) -> Result<(), Error> {
        self.save()?;
        // Always try to restore the stack, even if `f` errored.
        f(self).and(self.restore())
    }
}

impl PietGpuRenderContext {
    fn encode_line_seg(&mut self, seg: LineSeg) {
        self.elements.push(Element::Line(seg));
        self.pathseg_count += 1;
    }

    fn encode_quad_seg(&mut self, seg: QuadSeg) {
        self.elements.push(Element::Quad(seg));
        self.pathseg_count += 1;
    }

    fn encode_cubic_seg(&mut self, seg: CubicSeg) {
        self.elements.push(Element::Cubic(seg));
        self.pathseg_count += 1;
    }

    fn encode_path(&mut self, path: impl Iterator<Item = PathEl>, is_fill: bool) {
        if is_fill {
            self.encode_path_inner(
                path.flat_map(|el| {
                    match el {
                        PathEl::MoveTo(..) => Some(PathEl::ClosePath),
                        _ => None,
                    }
                    .into_iter()
                    .chain(Some(el))
                })
                .chain(Some(PathEl::ClosePath)),
            )
        } else {
            self.encode_path_inner(path)
        }
    }

    fn encode_path_inner(&mut self, path: impl Iterator<Item = PathEl>) {
        let flatten = false;
        if flatten {
            let mut start_pt = None;
            let mut last_pt = None;
            piet::kurbo::flatten(path, TOLERANCE, |el| {
                match el {
                    PathEl::MoveTo(p) => {
                        let scene_pt = to_f32_2(p);
                        start_pt = Some(scene_pt);
                        last_pt = Some(scene_pt);
                    }
                    PathEl::LineTo(p) => {
                        let scene_pt = to_f32_2(p);
                        let seg = LineSeg {
                            p0: last_pt.unwrap(),
                            p1: scene_pt,
                        };
                        self.encode_line_seg(seg);
                        last_pt = Some(scene_pt);
                    }
                    PathEl::ClosePath => {
                        if let (Some(start), Some(last)) = (start_pt.take(), last_pt.take()) {
                            if last != start {
                                let seg = LineSeg {
                                    p0: last,
                                    p1: start,
                                };
                                self.encode_line_seg(seg);
                            }
                        }
                    }
                    _ => (),
                }
                //println!("{:?}", el);
            });
        } else {
            let mut start_pt = None;
            let mut last_pt = None;
            for el in path {
                match el {
                    PathEl::MoveTo(p) => {
                        let scene_pt = to_f32_2(p);
                        start_pt = Some(scene_pt);
                        last_pt = Some(scene_pt);
                    }
                    PathEl::LineTo(p) => {
                        let scene_pt = to_f32_2(p);
                        let seg = LineSeg {
                            p0: last_pt.unwrap(),
                            p1: scene_pt,
                        };
                        self.encode_line_seg(seg);
                        last_pt = Some(scene_pt);
                    }
                    PathEl::QuadTo(p1, p2) => {
                        let scene_p1 = to_f32_2(p1);
                        let scene_p2 = to_f32_2(p2);
                        let seg = QuadSeg {
                            p0: last_pt.unwrap(),
                            p1: scene_p1,
                            p2: scene_p2,
                        };
                        self.encode_quad_seg(seg);
                        last_pt = Some(scene_p2);
                    }
                    PathEl::CurveTo(p1, p2, p3) => {
                        let scene_p1 = to_f32_2(p1);
                        let scene_p2 = to_f32_2(p2);
                        let scene_p3 = to_f32_2(p3);
                        let seg = CubicSeg {
                            p0: last_pt.unwrap(),
                            p1: scene_p1,
                            p2: scene_p2,
                            p3: scene_p3,
                        };
                        self.encode_cubic_seg(seg);
                        last_pt = Some(scene_p3);
                    }
                    PathEl::ClosePath => {
                        if let (Some(start), Some(last)) = (start_pt.take(), last_pt.take()) {
                            if last != start {
                                let seg = LineSeg {
                                    p0: last,
                                    p1: start,
                                };
                                self.encode_line_seg(seg);
                            }
                        }
                    }
                }
                //println!("{:?}", el);
            }
        }
    }

    fn pop_clip(&mut self) {
        let tos = self.clip_stack.pop().unwrap();
        let bbox = tos.bbox.unwrap_or_default();
        let bbox_f32_4 = rect_to_f32_4(bbox);
        self.elements
            .push(Element::EndClip(Clip { bbox: bbox_f32_4 }));
        self.path_count += 1;
        if let Element::BeginClip(begin_clip) = &mut self.elements[tos.begin_ix] {
            begin_clip.bbox = bbox_f32_4;
        } else {
            unreachable!("expected BeginClip, not found");
        }
        if let Some(bbox) = tos.bbox {
            self.union_bbox(bbox);
        }
    }

    /// Accumulate a bbox.
    ///
    /// The bbox is given lazily as a closure, relative to the current transform.
    /// It's lazy because we don't need to compute it unless we're inside a clip.
    fn accumulate_bbox(&mut self, f: impl FnOnce() -> Rect) {
        if !self.clip_stack.is_empty() {
            let bbox = f();
            let bbox = self.cur_transform.transform_rect_bbox(bbox);
            self.union_bbox(bbox);
        }
    }

    /// Accumulate an absolute bbox.
    ///
    /// The bbox is given already transformed into surface coordinates.
    fn union_bbox(&mut self, bbox: Rect) {
        if let Some(tos) = self.clip_stack.last_mut() {
            tos.bbox = if let Some(old_bbox) = tos.bbox {
                Some(old_bbox.union(bbox))
            } else {
                Some(bbox)
            };
        }
    }

    pub(crate) fn append_path_encoder(&mut self, path: &PathEncoder) {
        let elements = path.elements();
        self.elements.extend(elements.iter().cloned());
        self.pathseg_count += elements.len();
    }

    pub(crate) fn fill_glyph(&mut self, rgba_color: u32) {
        let fill = FillColor { rgba_color };
        self.elements.push(Element::FillColor(fill));
        self.path_count += 1;
    }

    pub(crate) fn encode_transform(&mut self, transform: Transform) {
        self.elements.push(Element::Transform(transform));
        self.trans_count += 1;
    }
}

impl IntoBrush<PietGpuRenderContext> for PietGpuBrush {
    fn make_brush<'b>(
        &'b self,
        _piet: &mut PietGpuRenderContext,
        _bbox: impl FnOnce() -> Rect,
    ) -> std::borrow::Cow<'b, PietGpuBrush> {
        Cow::Borrowed(self)
    }
}

pub(crate) fn to_f32_2(point: Point) -> [f32; 2] {
    [point.x as f32, point.y as f32]
}

fn rect_to_f32_4(rect: Rect) -> [f32; 4] {
    [
        rect.x0 as f32,
        rect.y0 as f32,
        rect.x1 as f32,
        rect.y1 as f32,
    ]
}

fn to_scene_transform(transform: Affine) -> Transform {
    let c = transform.as_coeffs();
    Transform {
        mat: [c[0] as f32, c[1] as f32, c[2] as f32, c[3] as f32],
        translate: [c[4] as f32, c[5] as f32],
    }
}

fn to_srgb(f: f64) -> f64 {
    if f <= 0.0031308 {
        f * 12.92
    } else {
        let a = 0.055;
        (1. + a) * f64::powf(f, f64::recip(2.4)) - a
    }
}

fn from_srgb(f: f64) -> f64 {
    if f <= 0.04045 {
        f / 12.92
    } else {
        let a = 0.055;
        f64::powf((f + a) * f64::recip(1. + a), 2.4)
    }
}
