use {
    crate::{
        cmm::{
            cmm_description::{ColorDescription, LinearColorDescription},
            cmm_render_intent::RenderIntent,
        },
        gfx_api::{
            AcquireSync, AlphaMode, BufferResv, CopyTexture, FillRect, FramebufferRect, GfxApiOpt,
            GfxTexture, ReleaseSync, RoundedCopyTexture, RoundedFillRect, SampleRect,
        },
        rect::Rect,
        scale::Scale,
        theme::{Color, CornerRadius},
        tree::Transform,
    },
    std::rc::Rc,
};

pub struct RendererBase<'a> {
    pub ops: &'a mut Vec<GfxApiOpt>,
    pub scaled: bool,
    pub scale: Scale,
    pub scalef: f64,
    pub transform: Transform,
    pub fb_width: f32,
    pub fb_height: f32,
}

impl RendererBase<'_> {
    pub fn scale(&self) -> Scale {
        self.scale
    }

    pub fn scale_point(&self, mut x: i32, mut y: i32) -> (i32, i32) {
        if self.scaled {
            [x, y] = self.scale.pixel_size([x, y]);
        }
        (x, y)
    }

    pub fn scale_point_f(&self, mut x: f32, mut y: f32) -> (f32, f32) {
        if self.scaled {
            x = (x as f64 * self.scalef) as _;
            y = (y as f64 * self.scalef) as _;
        }
        (x, y)
    }

    pub fn scale_rect(&self, mut rect: Rect) -> Rect {
        if self.scaled {
            let [x1, y1, x2, y2] =
                self.scale
                    .pixel_size([rect.x1(), rect.y1(), rect.x2(), rect.y2()]);
            rect = Rect::new_saturating(x1, y1, x2, y2);
        }
        rect
    }

    pub fn scale_rect_f(&self, mut rect: (f32, f32, f32, f32)) -> (f32, f32, f32, f32) {
        if self.scaled {
            let x1 = (rect.0 as f64 * self.scalef).round() as _;
            let y1 = (rect.1 as f64 * self.scalef).round() as _;
            let x2 = (rect.2 as f64 * self.scalef).round() as _;
            let y2 = (rect.3 as f64 * self.scalef).round() as _;
            rect = (x1, y1, x2, y2)
        }
        rect
    }

    pub fn fill_scaled_boxes(
        &mut self,
        boxes: &[Rect],
        color: &Color,
        alpha: Option<f32>,
        cd: &Rc<LinearColorDescription>,
        render_intent: RenderIntent,
    ) {
        self.fill_boxes3(boxes, color, alpha, cd, render_intent, 0, 0, true);
    }

    pub fn fill_boxes(
        &mut self,
        boxes: &[Rect],
        color: &Color,
        cd: &Rc<LinearColorDescription>,
        render_intent: RenderIntent,
    ) {
        self.fill_boxes3(boxes, color, None, cd, render_intent, 0, 0, false);
    }

    pub fn fill_boxes2(
        &mut self,
        boxes: &[Rect],
        color: &Color,
        cd: &Rc<LinearColorDescription>,
        render_intent: RenderIntent,
        dx: i32,
        dy: i32,
    ) {
        self.fill_boxes3(boxes, color, None, cd, render_intent, dx, dy, false);
    }

    fn fill_boxes3(
        &mut self,
        boxes: &[Rect],
        color: &Color,
        alpha: Option<f32>,
        cd: &Rc<LinearColorDescription>,
        render_intent: RenderIntent,
        dx: i32,
        dy: i32,
        scaled: bool,
    ) {
        if boxes.is_empty() || *color == Color::TRANSPARENT {
            return;
        }
        for bx in boxes {
            let bx = bx.move_(dx, dy);
            let bx = match scaled {
                false => self.scale_rect(bx),
                true => bx,
            };
            self.ops.push(GfxApiOpt::FillRect(FillRect {
                rect: FramebufferRect::new(
                    bx.x1() as f32,
                    bx.y1() as f32,
                    bx.x2() as f32,
                    bx.y2() as f32,
                    self.transform,
                    self.fb_width,
                    self.fb_height,
                ),
                color: *color,
                alpha,
                render_intent,
                cd: cd.clone(),
            }));
        }
    }

    pub fn fill_boxes_f(
        &mut self,
        boxes: &[(f32, f32, f32, f32)],
        color: &Color,
        cd: &Rc<LinearColorDescription>,
        render_intent: RenderIntent,
    ) {
        self.fill_boxes2_f(boxes, color, cd, render_intent, 0.0, 0.0);
    }

    pub fn fill_boxes2_f(
        &mut self,
        boxes: &[(f32, f32, f32, f32)],
        color: &Color,
        cd: &Rc<LinearColorDescription>,
        render_intent: RenderIntent,
        dx: f32,
        dy: f32,
    ) {
        if boxes.is_empty() || *color == Color::TRANSPARENT {
            return;
        }
        let (dx, dy) = self.scale_point_f(dx, dy);
        for bx in boxes {
            let (x1, y1, x2, y2) = self.scale_rect_f(*bx);
            self.ops.push(GfxApiOpt::FillRect(FillRect {
                rect: FramebufferRect::new(
                    x1 + dx,
                    y1 + dy,
                    x2 + dx,
                    y2 + dy,
                    self.transform,
                    self.fb_width,
                    self.fb_height,
                ),
                color: *color,
                alpha: None,
                render_intent,
                cd: cd.clone(),
            }));
        }
    }

    pub fn render_texture(
        &mut self,
        texture: &Rc<dyn GfxTexture>,
        alpha: Option<f32>,
        x: i32,
        y: i32,
        tpoints: Option<SampleRect>,
        tsize: Option<(i32, i32)>,
        tscale: Scale,
        bounds: Option<&Rect>,
        buffer_resv: Option<Rc<dyn BufferResv>>,
        acquire_sync: AcquireSync,
        release_sync: ReleaseSync,
        opaque: bool,
        cd: &Rc<ColorDescription>,
        render_intent: RenderIntent,
        alpha_mode: AlphaMode,
    ) {
        // log::info!("rendering texture {:?}", std::ptr::from_ref(&**texture) as *const u8);
        // log::info!("{:?}", backtrace::Backtrace::new());

        let mut texcoord = tpoints.unwrap_or_else(SampleRect::identity);

        let (twidth, theight) = if let Some(size) = tsize {
            size
        } else {
            let (mut w, mut h) = texcoord.buffer_transform.maybe_swap(texture.size());
            if tscale != self.scale {
                let tscale = tscale.to_f64();
                w = (w as f64 * self.scalef / tscale).round() as _;
                h = (h as f64 * self.scalef / tscale).round() as _;
            }
            (w, h)
        };

        let mut target_x = [x, x + twidth];
        let mut target_y = [y, y + theight];

        if let Some(bounds) = bounds
            && bound_target(&mut target_x, &mut target_y, &mut texcoord, bounds)
        {
            return;
        }

        let target = FramebufferRect::new(
            target_x[0] as f32,
            target_y[0] as f32,
            target_x[1] as f32,
            target_y[1] as f32,
            self.transform,
            self.fb_width,
            self.fb_height,
        );

        self.ops.push(GfxApiOpt::CopyTexture(CopyTexture {
            tex: texture.clone(),
            source: texcoord,
            target,
            alpha,
            buffer_resv,
            acquire_sync,
            release_sync,
            opaque,
            render_intent,
            cd: cd.clone(),
            alpha_mode,
        }));
    }

    pub fn fill_rounded_rect(
        &mut self,
        rect: Rect,
        color: &Color,
        alpha: Option<f32>,
        cd: &Rc<LinearColorDescription>,
        render_intent: RenderIntent,
        corner_radius: CornerRadius,
        border_width: f32,
    ) {
        self.fill_rounded_rect_z(
            rect,
            color,
            alpha,
            cd,
            render_intent,
            corner_radius,
            border_width,
            0,
        )
    }

    pub fn fill_rounded_rect_z(
        &mut self,
        rect: Rect,
        color: &Color,
        alpha: Option<f32>,
        cd: &Rc<LinearColorDescription>,
        render_intent: RenderIntent,
        corner_radius: CornerRadius,
        border_width: f32,
        z_order: u32,
    ) {
        if *color == Color::TRANSPARENT {
            return;
        }
        let rect = self.scale_rect(rect);
        let width = (rect.x2() - rect.x1()) as f32;
        let height = (rect.y2() - rect.y1()) as f32;
        let scale = self.scalef as f32;
        let fitted = corner_radius.fit_to(width, height);
        let cr: [f32; 4] = fitted.into();
        self.ops.push(GfxApiOpt::RoundedFillRect(RoundedFillRect {
            rect: FramebufferRect::new(
                rect.x1() as f32,
                rect.y1() as f32,
                rect.x2() as f32,
                rect.y2() as f32,
                self.transform,
                self.fb_width,
                self.fb_height,
            ),
            color: *color,
            alpha,
            render_intent,
            cd: cd.clone(),
            size: [width, height],
            corner_radius: cr,
            border_width,
            scale,
            z_order,
        }));
    }

    pub fn render_rounded_texture(
        &mut self,
        texture: &Rc<dyn GfxTexture>,
        alpha: Option<f32>,
        x: i32,
        y: i32,
        tpoints: Option<SampleRect>,
        tsize: Option<(i32, i32)>,
        tscale: Scale,
        bounds: Option<&Rect>,
        buffer_resv: Option<Rc<dyn BufferResv>>,
        acquire_sync: AcquireSync,
        release_sync: ReleaseSync,
        cd: &Rc<ColorDescription>,
        render_intent: RenderIntent,
        alpha_mode: AlphaMode,
        corner_radius: CornerRadius,
    ) {
        let mut texcoord = tpoints.unwrap_or_else(SampleRect::identity);

        let (twidth, theight) = if let Some(size) = tsize {
            size
        } else {
            let (mut w, mut h) = texcoord.buffer_transform.maybe_swap(texture.size());
            if tscale != self.scale {
                let tscale = tscale.to_f64();
                w = (w as f64 * self.scalef / tscale).round() as _;
                h = (h as f64 * self.scalef / tscale).round() as _;
            }
            (w, h)
        };

        let mut target_x = [x, x + twidth];
        let mut target_y = [y, y + theight];

        if let Some(bounds) = bounds
            && bound_target(&mut target_x, &mut target_y, &mut texcoord, bounds)
        {
            return;
        }

        let target = FramebufferRect::new(
            target_x[0] as f32,
            target_y[0] as f32,
            target_x[1] as f32,
            target_y[1] as f32,
            self.transform,
            self.fb_width,
            self.fb_height,
        );

        let width = (target_x[1] - target_x[0]) as f32;
        let height = (target_y[1] - target_y[0]) as f32;
        let scale = self.scalef as f32;
        let fitted = corner_radius.fit_to(width, height);
        let cr: [f32; 4] = fitted.into();

        self.ops
            .push(GfxApiOpt::RoundedCopyTexture(RoundedCopyTexture {
                tex: texture.clone(),
                source: texcoord,
                target,
                alpha,
                buffer_resv,
                acquire_sync,
                release_sync,
                opaque: false,
                render_intent,
                cd: cd.clone(),
                alpha_mode,
                size: [width, height],
                corner_radius: cr,
                scale,
            }));
    }

    pub fn sync(&mut self) {
        self.ops.push(GfxApiOpt::Sync);
    }
}

#[inline]
fn bound_target(
    target_x: &mut [i32; 2],
    target_y: &mut [i32; 2],
    texcoord: &mut SampleRect,
    bounds: &Rect,
) -> bool {
    let bounds_x = [bounds.x1(), bounds.x2()];
    let bounds_y = [bounds.y1(), bounds.y2()];

    if target_x[0] >= bounds_x[0]
        && target_x[1] <= bounds_x[1]
        && target_y[0] >= bounds_y[0]
        && target_y[1] <= bounds_y[1]
    {
        return false;
    }

    #[cold]
    fn cold() {}
    cold();

    let SampleRect {
        x1: t_x1,
        x2: t_x2,
        y1: t_y1,
        y2: t_y2,
        ..
    } = texcoord;

    macro_rules! clamp {
        ($desired:ident, $bounds:ident, $test_idx:expr, $test_cmp:ident, $test_cmp_eq:ident, $modify:ident, $keep:ident) => {{
            let desired_test = $desired[$test_idx];
            let desired_other = $desired[1 - $test_idx];
            let bound = $bounds[$test_idx];
            if desired_test.$test_cmp(&bound) {
                cold();
                if desired_other.$test_cmp_eq(&bound) {
                    return true;
                }
                let max = (desired_other - bound) as f32;
                let desired = ($desired[1] - $desired[0]) as f32;
                let factor = max.abs() / desired;
                *$modify = *$keep + (*$modify - *$keep) * factor;
                $desired[$test_idx] = bound;
            }
        }};
    }

    clamp!(target_x, bounds_x, 0, lt, le, t_x1, t_x2);
    clamp!(target_x, bounds_x, 1, gt, ge, t_x2, t_x1);
    clamp!(target_y, bounds_y, 0, lt, le, t_y1, t_y2);
    clamp!(target_y, bounds_y, 1, gt, ge, t_y2, t_y1);

    false
}
