use std::fmt;
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};

use image::ImageReader;
use ruin_runtime::channel::oneshot;

use crate::UiSize;

static NEXT_IMAGE_RESOURCE_ID: AtomicU64 = AtomicU64::new(1);

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ImageFit {
    Contain,
    Cover,
    Fill,
}

#[derive(Clone)]
pub struct ImageResource {
    inner: Arc<ImageResourceInner>,
}

struct ImageResourceInner {
    id: u64,
    width: u32,
    height: u32,
    pixels: Arc<[u8]>,
}

#[derive(Debug)]
pub enum ImageResourceError {
    Io(std::io::Error),
    Decode(image::ImageError),
    DecodeTaskClosed,
    InvalidRgbaPixels { width: u32, height: u32, len: usize },
}

impl ImageResource {
    pub async fn load_path(path: impl AsRef<Path>) -> Result<Self, ImageResourceError> {
        let bytes = ruin_runtime::fs::read(path.as_ref())
            .await
            .map_err(ImageResourceError::Io)?;
        Self::decode_encoded_bytes(bytes).await
    }

    pub async fn decode_encoded_bytes(
        bytes: impl Into<Vec<u8>>,
    ) -> Result<Self, ImageResourceError> {
        let bytes = bytes.into();
        let (tx, mut rx) = oneshot::channel();
        std::thread::Builder::new()
            .name("ruin-ui-image-decode".to_owned())
            .spawn(move || {
                let result = Self::decode_encoded_bytes_sync(&bytes);
                let _ = tx.send(result);
            })
            .map_err(ImageResourceError::Io)?;
        rx.recv()
            .await
            .map_err(|_| ImageResourceError::DecodeTaskClosed)?
    }

    pub fn from_rgba8(
        width: u32,
        height: u32,
        pixels: impl Into<Vec<u8>>,
    ) -> Result<Self, ImageResourceError> {
        let pixels = pixels.into();
        let expected_len = width as usize * height as usize * 4;
        if pixels.len() != expected_len {
            return Err(ImageResourceError::InvalidRgbaPixels {
                width,
                height,
                len: pixels.len(),
            });
        }
        Ok(Self {
            inner: Arc::new(ImageResourceInner {
                id: NEXT_IMAGE_RESOURCE_ID.fetch_add(1, Ordering::Relaxed),
                width,
                height,
                pixels: pixels.into(),
            }),
        })
    }

    fn decode_encoded_bytes_sync(bytes: &[u8]) -> Result<Self, ImageResourceError> {
        let image = ImageReader::new(std::io::Cursor::new(bytes))
            .with_guessed_format()
            .map_err(ImageResourceError::Io)?
            .decode()
            .map_err(ImageResourceError::Decode)?;
        let rgba = image.to_rgba8();
        Self::from_rgba8(rgba.width(), rgba.height(), rgba.into_raw())
    }

    pub fn id(&self) -> u64 {
        self.inner.id
    }

    pub fn width(&self) -> u32 {
        self.inner.width
    }

    pub fn height(&self) -> u32 {
        self.inner.height
    }

    pub fn size(&self) -> UiSize {
        UiSize::new(self.inner.width as f32, self.inner.height as f32)
    }

    pub fn pixels(&self) -> &[u8] {
        &self.inner.pixels
    }
}

impl fmt::Debug for ImageResource {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("ImageResource")
            .field("id", &self.inner.id)
            .field("width", &self.inner.width)
            .field("height", &self.inner.height)
            .finish()
    }
}

impl PartialEq for ImageResource {
    fn eq(&self, other: &Self) -> bool {
        self.inner.id == other.inner.id
    }
}

impl Eq for ImageResource {}

impl std::hash::Hash for ImageResource {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.inner.id.hash(state);
    }
}

impl fmt::Display for ImageResourceError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Io(error) => write!(f, "{error}"),
            Self::Decode(error) => write!(f, "{error}"),
            Self::DecodeTaskClosed => {
                f.write_str("image decode task closed before producing a result")
            }
            Self::InvalidRgbaPixels { width, height, len } => write!(
                f,
                "expected {} RGBA bytes for {width}x{height} image, got {len}",
                *width as usize * *height as usize * 4
            ),
        }
    }
}

impl std::error::Error for ImageResourceError {}
