use crate::ApDocument;
use crate::IMAGE_MEDIA_RE;
use anyhow::{anyhow, Context, Error, Result};
use image::ImageFormat;
#[cfg(not(target_arch = "wasm32"))]
use image::ImageReader;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fmt::{Debug, Display, Formatter};
use url::Url;

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Default)]
pub enum ApImageType {
    #[serde(alias = "image")]
    #[default]
    Image,
}

impl Display for ApImageType {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        Debug::fmt(self, f)
    }
}

#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Ord, PartialOrd, Default)]
#[serde(rename_all = "camelCase")]
pub struct ApImage {
    #[serde(rename = "type")]
    pub kind: ApImageType,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub media_type: Option<String>,
    pub url: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub width: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub height: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub blurhash: Option<String>,

    // filename is used internally to facilitate struct initialization
    #[serde(skip_serializing)]
    pub(crate) filename: Option<String>,

    // local_path is used internally to facilitate struct initialization
    #[serde(skip_serializing)]
    pub(crate) path: Option<String>,
}

#[cfg(not(target_arch = "wasm32"))]
impl ApImage {
    pub fn initialize(path_str: String, filename_str: String, media_type_str: String) -> Self {
        let url_str = format!("{}/media/uploads/{}", *crate::SERVER_URL, filename_str);
        ApImage {
            kind: ApImageType::Image,
            media_type: Some(media_type_str),
            url: url_str,
            name: None,
            width: None,
            height: None,
            blurhash: None,
            filename: Some(filename_str),
            path: Some(path_str),
        }
    }

    pub fn clean(&self) -> Result<()> {
        let full_path = &format!(
            "{}/{}",
            self.path
                .clone()
                .context("path cannot be None for clean operation")?,
            self.filename
                .clone()
                .context("filename cannot be None for clean operation")?
        );
        if let Ok(meta) = rexiv2::Metadata::new_from_path(full_path) {
            let orientation = meta.get_orientation();
            meta.clear();
            meta.set_orientation(orientation);
            meta.save_to_file(full_path).map_err(anyhow::Error::msg)
        } else {
            Err(anyhow!("failed to clean media file: {}", full_path))
        }
    }

    pub fn analyze(&mut self) -> Result<()> {
        if self.filename.is_none() || self.path.is_none() {
            return Err(anyhow!(
                "Unable to analyze ApImage without filename or path"
            ));
        }

        let full_path = &format!(
            "{}/{}",
            self.path.as_ref().unwrap(), // Known to be Some due to check above
            self.filename.as_ref().unwrap()  // Known to be Some due to check above
        );

        let img = ImageReader::open(full_path)
            .map_err(|e| anyhow!("Failed to open image {}: {}", full_path, e))?
            .decode()
            .map_err(|e| anyhow!("Failed to decode image {}: {}", full_path, e))?;

        self.width = Some(img.width() as i32);
        self.height = Some(img.height() as i32);
        self.blurhash =
            blurhash::encode(4, 3, img.width(), img.height(), &img.to_rgba8().into_vec())
                .map_err(|e| anyhow!("Failed to encode blurhash for {}: {}", full_path, e))
                .ok();

        Ok(())
    }
}

impl Debug for ApImage {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "Image[{}{}{}]",
            self.url,
            match (self.width, self.height) {
                (Some(w), Some(h)) => format!(" ({}x{})", w, h),
                _ => String::new(),
            },
            self.media_type
                .as_ref()
                .map(|mt| format!(" {}", mt))
                .unwrap_or_default()
        )
    }
}

impl Display for ApImage {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "Image[{}]", self.url,)
    }
}

fn get_media_type(url: &str) -> Option<String> {
    let url = Url::parse(url).ok()?;
    let ext = url.path().split('.').next_back()?;
    if ["png", "jpg", "jpeg", "gif", "bmp", "ico", "svg"].contains(&ext.to_lowercase().as_str()) {
        ImageFormat::from_extension(ext).map(|x| x.to_mime_type().to_string())
    } else {
        None
    }
}

impl TryFrom<Value> for ApImage {
    type Error = anyhow::Error;

    fn try_from(value: Value) -> Result<Self> {
        serde_json::from_value(value).map_err(anyhow::Error::msg)
    }
}

impl From<String> for ApImage {
    fn from(url: String) -> Self {
        ApImage {
            kind: ApImageType::Image,
            media_type: get_media_type(&url),
            url,
            ..Default::default()
        }
    }
}

impl TryFrom<ApDocument> for ApImage {
    type Error = Error;

    fn try_from(document: ApDocument) -> Result<Self, Self::Error> {
        let url = document.url.ok_or(Self::Error::msg("url is None"))?;
        let media_type = document
            .media_type
            .ok_or(Self::Error::msg("media_type is None"))?;

        IMAGE_MEDIA_RE
            .is_match(&media_type)
            .then_some(ApImage {
                kind: ApImageType::Image,
                media_type: Some(media_type),
                url,
                ..Default::default()
            })
            .ok_or(Self::Error::msg("not cacheable"))
    }
}
