use cairo::{self, MatrixTrait};
use std::cell::Cell;

use attributes::Attribute;
use coord_units::CoordUnits;
use drawing_ctx::DrawingCtx;
use error::RenderingError;
use length::{Length, LengthDir};
use node::{NodeResult, NodeTrait, RsvgNode};
use parsers::{Parse, ParseValue};
use property_bag::PropertyBag;
use rect::IRect;
use state::Opacity;
use surface_utils::{
    iterators::Pixels,
    shared_surface::SharedImageSurface,
    shared_surface::SurfaceType,
    ImageSurfaceDataExt,
};
use unit_interval::UnitInterval;

coord_units!(MaskUnits, CoordUnits::ObjectBoundingBox);
coord_units!(MaskContentUnits, CoordUnits::UserSpaceOnUse);

pub struct NodeMask {
    x: Cell<Length>,
    y: Cell<Length>,
    width: Cell<Length>,
    height: Cell<Length>,

    units: Cell<MaskUnits>,
    content_units: Cell<MaskContentUnits>,
}

impl NodeMask {
    pub fn new() -> NodeMask {
        NodeMask {
            x: Cell::new(NodeMask::get_default_pos(LengthDir::Horizontal)),
            y: Cell::new(NodeMask::get_default_pos(LengthDir::Vertical)),

            width: Cell::new(NodeMask::get_default_size(LengthDir::Horizontal)),
            height: Cell::new(NodeMask::get_default_size(LengthDir::Vertical)),

            units: Cell::new(MaskUnits::default()),
            content_units: Cell::new(MaskContentUnits::default()),
        }
    }

    fn get_default_pos(dir: LengthDir) -> Length {
        Length::parse_str("-10%", dir).unwrap()
    }

    fn get_default_size(dir: LengthDir) -> Length {
        Length::parse_str("120%", dir).unwrap()
    }

    pub fn generate_cairo_mask(
        &self,
        node: &RsvgNode,
        affine_before_mask: &cairo::Matrix,
        draw_ctx: &mut DrawingCtx,
    ) -> Result<(), RenderingError> {
        let cascaded = node.get_cascaded_values();
        let values = cascaded.get();

        let width = draw_ctx.get_width() as i32;
        let height = draw_ctx.get_height() as i32;

        let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, width, height)?;

        let mask_units = CoordUnits::from(self.units.get());
        let content_units = CoordUnits::from(self.content_units.get());

        let (x, y, w, h) = {
            let params = if mask_units == CoordUnits::ObjectBoundingBox {
                draw_ctx.push_view_box(1.0, 1.0)
            } else {
                draw_ctx.get_view_params()
            };

            let x = self.x.get().normalize(&values, &params);
            let y = self.y.get().normalize(&values, &params);
            let w = self.width.get().normalize(&values, &params);
            let h = self.height.get().normalize(&values, &params);

            (x, y, w, h)
        };

        // Use a scope because mask_cr needs to release the
        // reference to the surface before we access the pixels
        {
            let bbox_rect = {
                if let Some(ref rect) = draw_ctx.get_bbox().rect {
                    *rect
                } else {
                    // The node being masked is empty / doesn't have a
                    // bounding box, so there's nothing to mask!
                    return Ok(());
                }
            };

            let save_cr = draw_ctx.get_cairo_context();

            let mask_cr = cairo::Context::new(&surface);
            mask_cr.set_matrix(*affine_before_mask);
            mask_cr.transform(node.get_transform());

            draw_ctx.set_cairo_context(&mask_cr);

            if mask_units == CoordUnits::ObjectBoundingBox {
                draw_ctx.clip(
                    x * bbox_rect.width + bbox_rect.x,
                    y * bbox_rect.height + bbox_rect.y,
                    w * bbox_rect.width,
                    h * bbox_rect.height,
                );
            } else {
                draw_ctx.clip(x, y, w, h);
            }

            {
                let _params = if content_units == CoordUnits::ObjectBoundingBox {
                    let bbtransform = cairo::Matrix::new(
                        bbox_rect.width,
                        0.0,
                        0.0,
                        bbox_rect.height,
                        bbox_rect.x,
                        bbox_rect.y,
                    );

                    mask_cr.transform(bbtransform);

                    draw_ctx.push_view_box(1.0, 1.0)
                } else {
                    draw_ctx.get_view_params()
                };

                let res = draw_ctx.with_discrete_layer(node, values, false, &mut |dc| {
                    node.draw_children(&cascaded, dc, false)
                });

                draw_ctx.set_cairo_context(&save_cr);

                res
            }
        }?;

        let Opacity(opacity) = values.opacity;
        let mask_surface = compute_luminance_to_alpha(surface, opacity)?;
        draw_ctx.mask_surface(&mask_surface);

        Ok(())
    }
}

// Returns a surface whose alpha channel for each pixel is equal to the
// luminance of that pixel's unpremultiplied RGB values.  The resulting
// surface's RGB values are not meanignful; only the alpha channel has
// useful luminance data.
//
// This is to get a mask suitable for use with cairo_mask_surface().
fn compute_luminance_to_alpha(
    surface: cairo::ImageSurface,
    opacity: UnitInterval,
) -> Result<cairo::ImageSurface, cairo::Status> {
    let surface = SharedImageSurface::new(surface, SurfaceType::SRgb)?;

    let width = surface.width();
    let height = surface.height();

    let bounds = IRect {
        x0: 0,
        y0: 0,
        x1: width,
        y1: height,
    };

    let opacity = u8::from(opacity);
    let mut output = cairo::ImageSurface::create(cairo::Format::ARgb32, width, height)?;
    let output_stride = output.get_stride() as usize;

    {
        let mut output_data = output.get_data().unwrap();

        for (x, y, pixel) in Pixels::new(&surface, bounds) {
            output_data.set_pixel(output_stride, pixel.to_mask(opacity), x, y);
        }
    }

    Ok(output)
}

impl NodeTrait for NodeMask {
    fn set_atts(&self, _: &RsvgNode, pbag: &PropertyBag<'_>) -> NodeResult {
        for (attr, value) in pbag.iter() {
            match attr {
                Attribute::X => self.x.set(attr.parse(value, LengthDir::Horizontal)?),
                Attribute::Y => self.y.set(attr.parse(value, LengthDir::Vertical)?),
                Attribute::Width => self.width.set(attr.parse_and_validate(
                    value,
                    LengthDir::Horizontal,
                    Length::check_nonnegative,
                )?),
                Attribute::Height => self.height.set(attr.parse_and_validate(
                    value,
                    LengthDir::Vertical,
                    Length::check_nonnegative,
                )?),
                Attribute::MaskUnits => self.units.set(attr.parse(value, ())?),
                Attribute::MaskContentUnits => self.content_units.set(attr.parse(value, ())?),
                _ => (),
            }
        }

        Ok(())
    }
}
