use std::ffi::OsStr;
use std::path::Path;

use typst::geom::Smart;
use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat};
use typst::util::option_eq;

use crate::compute::Readable;
use crate::meta::{Figurable, LocalName};
use crate::prelude::*;
use crate::text::families;

/// A raster or vector graphic.
///
/// Supported formats are PNG, JPEG, GIF and SVG.
///
/// _Note:_ Work on SVG export is ongoing and there might be visual inaccuracies
/// in the resulting PDF. Make sure to double-check embedded SVG images. If you
/// have an issue, also feel free to report it on [GitHub][gh-svg].
///
/// # Example
/// ```example
/// #figure(
///   image("molecular.jpg", width: 80%),
///   caption: [
///     A step in the molecular testing
///     pipeline of our lab.
///   ],
/// )
/// ```
///
/// [gh-svg]: https://github.com/typst/typst/issues?q=is%3Aopen+is%3Aissue+label%3Asvg
#[elem(scope, Layout, LocalName, Figurable)]
pub struct ImageElem {
    /// Path to an image file.
    #[required]
    #[parse(
        let Spanned { v: path, span } =
            args.expect::<Spanned<EcoString>>("path to image file")?;
        let id = vm.resolve_path(&path).at(span)?;
        let data = vm.world().file(id).at(span)?;
        path
    )]
    pub path: EcoString,

    /// The raw file data.
    #[internal]
    #[required]
    #[parse(Readable::Bytes(data))]
    pub data: Readable,

    /// The image's format. Detected automatically by default.
    pub format: Smart<ImageFormat>,

    /// The width of the image.
    pub width: Smart<Rel<Length>>,

    /// The height of the image.
    pub height: Smart<Rel<Length>>,

    /// A text describing the image.
    pub alt: Option<EcoString>,

    /// How the image should adjust itself to a given area.
    #[default(ImageFit::Cover)]
    pub fit: ImageFit,
}

#[scope]
impl ImageElem {
    /// Decode a raster or vector graphic from bytes or a string.
    ///
    /// ```example
    /// #let original = read("diagram.svg")
    /// #let changed = original.replace(
    ///   "#2B80FF", // blue
    ///   green.to-hex(),
    /// )
    ///
    /// #image.decode(original)
    /// #image.decode(changed)
    /// ```
    #[func(title = "Decode Image")]
    pub fn decode(
        /// The data to decode as an image. Can be a string for SVGs.
        data: Readable,
        /// The image's format. Detected automatically by default.
        #[named]
        format: Option<Smart<ImageFormat>>,
        /// The width of the image.
        #[named]
        width: Option<Smart<Rel<Length>>>,
        /// The height of the image.
        #[named]
        height: Option<Smart<Rel<Length>>>,
        /// A text describing the image.
        #[named]
        alt: Option<Option<EcoString>>,
        /// How the image should adjust itself to a given area.
        #[named]
        fit: Option<ImageFit>,
    ) -> StrResult<Content> {
        let mut elem = ImageElem::new(EcoString::new(), data);
        if let Some(format) = format {
            elem.push_format(format);
        }
        if let Some(width) = width {
            elem.push_width(width);
        }
        if let Some(height) = height {
            elem.push_height(height);
        }
        if let Some(alt) = alt {
            elem.push_alt(alt);
        }
        if let Some(fit) = fit {
            elem.push_fit(fit);
        }
        Ok(elem.pack())
    }
}

impl Layout for ImageElem {
    #[tracing::instrument(name = "ImageElem::layout", skip_all)]
    fn layout(
        &self,
        vt: &mut Vt,
        styles: StyleChain,
        regions: Regions,
    ) -> SourceResult<Fragment> {
        // Take the format that was explicitly defined, or parse the extention,
        // or try to detect the format.
        let data = self.data();
        let format = match self.format(styles) {
            Smart::Custom(v) => v,
            Smart::Auto => {
                let ext = Path::new(self.path().as_str())
                    .extension()
                    .and_then(OsStr::to_str)
                    .unwrap_or_default()
                    .to_lowercase();

                match ext.as_str() {
                    "png" => ImageFormat::Raster(RasterFormat::Png),
                    "jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg),
                    "gif" => ImageFormat::Raster(RasterFormat::Gif),
                    "svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg),
                    _ => match &data {
                        Readable::Str(_) => ImageFormat::Vector(VectorFormat::Svg),
                        Readable::Bytes(bytes) => match RasterFormat::detect(bytes) {
                            Some(f) => ImageFormat::Raster(f),
                            None => bail!(self.span(), "unknown image format"),
                        },
                    },
                }
            }
        };

        let image = Image::with_fonts(
            data.into(),
            format,
            self.alt(styles),
            vt.world,
            &families(styles).map(|s| s.as_str().into()).collect::<Vec<_>>(),
        )
        .at(self.span())?;

        let sizing = Axes::new(self.width(styles), self.height(styles));
        let region = sizing
            .zip_map(regions.base(), |s, r| s.map(|v| v.resolve(styles).relative_to(r)))
            .unwrap_or(regions.base());

        let expand = sizing.as_ref().map(Smart::is_custom) | regions.expand;
        let region_ratio = region.x / region.y;

        // Find out whether the image is wider or taller than the target size.
        let pxw = image.width() as f64;
        let pxh = image.height() as f64;
        let px_ratio = pxw / pxh;
        let wide = px_ratio > region_ratio;

        // The space into which the image will be placed according to its fit.
        let target = if expand.x && expand.y {
            region
        } else if expand.x || (!expand.y && wide && region.x.is_finite()) {
            Size::new(region.x, region.y.min(region.x.safe_div(px_ratio)))
        } else if region.y.is_finite() {
            Size::new(region.x.min(region.y * px_ratio), region.y)
        } else {
            Size::new(Abs::pt(pxw), Abs::pt(pxh))
        };

        // Compute the actual size of the fitted image.
        let fit = self.fit(styles);
        let fitted = match fit {
            ImageFit::Cover | ImageFit::Contain => {
                if wide == (fit == ImageFit::Contain) {
                    Size::new(target.x, target.x / px_ratio)
                } else {
                    Size::new(target.y * px_ratio, target.y)
                }
            }
            ImageFit::Stretch => target,
        };

        // First, place the image in a frame of exactly its size and then resize
        // the frame to the target size, center aligning the image in the
        // process.
        let mut frame = Frame::soft(fitted);
        frame.push(Point::zero(), FrameItem::Image(image, fitted, self.span()));
        frame.resize(target, Axes::splat(FixedAlign::Center));

        // Create a clipping group if only part of the image should be visible.
        if fit == ImageFit::Cover && !target.fits(fitted) {
            frame.clip();
        }

        // Apply metadata.
        frame.meta(styles, false);

        Ok(Fragment::frame(frame))
    }
}

impl LocalName for ImageElem {
    fn local_name(&self, lang: Lang, region: Option<Region>) -> &'static str {
        match lang {
            Lang::ALBANIAN => "Figurë",
            Lang::ARABIC => "شكل",
            Lang::BOKMÅL => "Figur",
            Lang::CHINESE if option_eq(region, "TW") => "圖",
            Lang::CHINESE => "图",
            Lang::CZECH => "Obrázek",
            Lang::DANISH => "Figur",
            Lang::DUTCH => "Figuur",
            Lang::FILIPINO => "Pigura",
            Lang::FINNISH => "Kuva",
            Lang::FRENCH => "Figure",
            Lang::GERMAN => "Abbildung",
            Lang::HUNGARIAN => "Ábra",
            Lang::ITALIAN => "Figura",
            Lang::NYNORSK => "Figur",
            Lang::POLISH => "Rysunek",
            Lang::PORTUGUESE => "Figura",
            Lang::ROMANIAN => "Figura",
            Lang::RUSSIAN => "Рисунок",
            Lang::SLOVENIAN => "Slika",
            Lang::SPANISH => "Figura",
            Lang::SWEDISH => "Figur",
            Lang::TURKISH => "Şekil",
            Lang::UKRAINIAN => "Рисунок",
            Lang::VIETNAMESE => "Hình",
            Lang::JAPANESE => "図",
            Lang::ENGLISH | _ => "Figure",
        }
    }
}

impl Figurable for ImageElem {}

/// How an image should adjust itself to a given area.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum ImageFit {
    /// The image should completely cover the area. This is the default.
    Cover,
    /// The image should be fully contained in the area.
    Contain,
    /// The image should be stretched so that it exactly fills the area, even if
    /// this means that the image will be distorted.
    Stretch,
}
