use crate::urls::{UrlCache, UrlMimes};
use crate::Result;
use egui::TextureHandle;
use image::{Delay, Frame};
use poll_promise::Promise;

use egui::ColorImage;

use std::collections::HashMap;
use std::fs::{create_dir_all, File};
use std::sync::mpsc::Receiver;
use std::time::{Duration, Instant, SystemTime};

use hex::ToHex;
use sha2::Digest;
use std::path::PathBuf;
use std::path::{self, Path};
use tracing::warn;

#[derive(Default)]
pub struct TexturesCache {
    cache: hashbrown::HashMap<String, TextureStateInternal>,
}

impl TexturesCache {
    pub fn handle_and_get_or_insert_loadable(
        &mut self,
        url: &str,
        closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
    ) -> LoadableTextureState {
        let internal = self.handle_and_get_state_internal(url, true, closure);

        internal.into()
    }

    pub fn handle_and_get_or_insert(
        &mut self,
        url: &str,
        closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
    ) -> TextureState {
        let internal = self.handle_and_get_state_internal(url, false, closure);

        internal.into()
    }

    fn handle_and_get_state_internal(
        &mut self,
        url: &str,
        use_loading: bool,
        closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
    ) -> &mut TextureStateInternal {
        let state = match self.cache.raw_entry_mut().from_key(url) {
            hashbrown::hash_map::RawEntryMut::Occupied(entry) => {
                let state = entry.into_mut();
                handle_occupied(state, use_loading);

                state
            }
            hashbrown::hash_map::RawEntryMut::Vacant(entry) => {
                let res = closure();
                let (_, state) = entry.insert(url.to_owned(), TextureStateInternal::Pending(res));

                state
            }
        };

        state
    }

    pub fn insert_pending(&mut self, url: &str, promise: Promise<Option<Result<TexturedImage>>>) {
        self.cache
            .insert(url.to_owned(), TextureStateInternal::Pending(promise));
    }

    pub fn move_to_loaded(&mut self, url: &str) {
        let hashbrown::hash_map::RawEntryMut::Occupied(entry) =
            self.cache.raw_entry_mut().from_key(url)
        else {
            return;
        };

        entry.replace_entry_with(|_, v| {
            let TextureStateInternal::Loading(textured) = v else {
                return None;
            };

            Some(TextureStateInternal::Loaded(textured))
        });
    }

    pub fn get_and_handle(&mut self, url: &str) -> Option<LoadableTextureState> {
        self.cache.get_mut(url).map(|state| {
            handle_occupied(state, true);
            state.into()
        })
    }
}

fn handle_occupied(state: &mut TextureStateInternal, use_loading: bool) {
    let TextureStateInternal::Pending(promise) = state else {
        return;
    };

    let Some(res) = promise.ready_mut() else {
        return;
    };

    let Some(res) = res.take() else {
        tracing::error!("Failed to take the promise");
        *state =
            TextureStateInternal::Error(crate::Error::Generic("Promise already taken".to_owned()));
        return;
    };

    match res {
        Ok(textured) => {
            *state = if use_loading {
                TextureStateInternal::Loading(textured)
            } else {
                TextureStateInternal::Loaded(textured)
            }
        }
        Err(e) => *state = TextureStateInternal::Error(e),
    }
}

pub enum LoadableTextureState<'a> {
    Pending,
    Error(&'a crate::Error),
    Loading {
        actual_image_tex: &'a mut TexturedImage,
    }, // the texture is in the loading state, for transitioning between the pending and loaded states
    Loaded(&'a mut TexturedImage),
}

pub enum TextureState<'a> {
    Pending,
    Error(&'a crate::Error),
    Loaded(&'a mut TexturedImage),
}

impl<'a> From<&'a mut TextureStateInternal> for TextureState<'a> {
    fn from(value: &'a mut TextureStateInternal) -> Self {
        match value {
            TextureStateInternal::Pending(_) => TextureState::Pending,
            TextureStateInternal::Error(error) => TextureState::Error(error),
            TextureStateInternal::Loading(textured_image) => TextureState::Loaded(textured_image),
            TextureStateInternal::Loaded(textured_image) => TextureState::Loaded(textured_image),
        }
    }
}

pub enum TextureStateInternal {
    Pending(Promise<Option<Result<TexturedImage>>>),
    Error(crate::Error),
    Loading(TexturedImage), // the image is in the loading state, for transitioning between blur and image
    Loaded(TexturedImage),
}

impl<'a> From<&'a mut TextureStateInternal> for LoadableTextureState<'a> {
    fn from(value: &'a mut TextureStateInternal) -> Self {
        match value {
            TextureStateInternal::Pending(_) => LoadableTextureState::Pending,
            TextureStateInternal::Error(error) => LoadableTextureState::Error(error),
            TextureStateInternal::Loading(textured_image) => LoadableTextureState::Loading {
                actual_image_tex: textured_image,
            },
            TextureStateInternal::Loaded(textured_image) => {
                LoadableTextureState::Loaded(textured_image)
            }
        }
    }
}

pub enum TexturedImage {
    Static(TextureHandle),
    Animated(Animation),
}

impl TexturedImage {
    pub fn get_first_texture(&self) -> &TextureHandle {
        match self {
            TexturedImage::Static(texture_handle) => texture_handle,
            TexturedImage::Animated(animation) => &animation.first_frame.texture,
        }
    }
}

pub struct Animation {
    pub first_frame: TextureFrame,
    pub other_frames: Vec<TextureFrame>,
    pub receiver: Option<Receiver<TextureFrame>>,
}

impl Animation {
    pub fn get_frame(&self, index: usize) -> Option<&TextureFrame> {
        if index == 0 {
            Some(&self.first_frame)
        } else {
            self.other_frames.get(index - 1)
        }
    }

    pub fn num_frames(&self) -> usize {
        self.other_frames.len() + 1
    }
}

pub struct TextureFrame {
    pub delay: Duration,
    pub texture: TextureHandle,
}

pub struct ImageFrame {
    pub delay: Duration,
    pub image: ColorImage,
}

pub struct MediaCache {
    pub cache_dir: path::PathBuf,
    pub textures_cache: TexturesCache,
    pub cache_type: MediaCacheType,
}

#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub enum MediaCacheType {
    Image,
    Gif,
}

impl MediaCache {
    pub fn new(parent_dir: &Path, cache_type: MediaCacheType) -> Self {
        let cache_dir = parent_dir.join(Self::rel_dir(cache_type));
        Self {
            cache_dir,
            textures_cache: TexturesCache::default(),
            cache_type,
        }
    }

    pub fn rel_dir(cache_type: MediaCacheType) -> &'static str {
        match cache_type {
            MediaCacheType::Image => "img",
            MediaCacheType::Gif => "gif",
        }
    }

    pub fn write(cache_dir: &path::Path, url: &str, data: ColorImage) -> Result<()> {
        let file = Self::create_file(cache_dir, url)?;
        let encoder = image::codecs::webp::WebPEncoder::new_lossless(file);

        encoder.encode(
            data.as_raw(),
            data.size[0] as u32,
            data.size[1] as u32,
            image::ColorType::Rgba8.into(),
        )?;

        Ok(())
    }

    fn create_file(cache_dir: &path::Path, url: &str) -> Result<File> {
        let file_path = cache_dir.join(Self::key(url));
        if let Some(p) = file_path.parent() {
            create_dir_all(p)?;
        }
        Ok(File::options()
            .write(true)
            .create(true)
            .truncate(true)
            .open(file_path)?)
    }

    pub fn write_gif(cache_dir: &path::Path, url: &str, data: Vec<ImageFrame>) -> Result<()> {
        let file = Self::create_file(cache_dir, url)?;

        let mut encoder = image::codecs::gif::GifEncoder::new(file);
        for img in data {
            let buf = color_image_to_rgba(img.image);
            let frame = Frame::from_parts(buf, 0, 0, Delay::from_saturating_duration(img.delay));
            if let Err(e) = encoder.encode_frame(frame) {
                tracing::error!("problem encoding frame: {e}");
            }
        }

        Ok(())
    }

    pub fn key(url: &str) -> String {
        let k: String = sha2::Sha256::digest(url.as_bytes()).encode_hex();
        PathBuf::from(&k[0..2])
            .join(&k[2..4])
            .join(k)
            .to_string_lossy()
            .to_string()
    }

    /// Migrate from base32 encoded url to sha256 url + sub-dir structure
    pub fn migrate_v0(&self) -> Result<()> {
        for file in std::fs::read_dir(&self.cache_dir)? {
            let file = if let Ok(f) = file {
                f
            } else {
                // not sure how this could fail, skip entry
                continue;
            };
            if !file.path().is_file() {
                continue;
            }
            let old_filename = file.file_name().to_string_lossy().to_string();
            let old_url = if let Some(u) =
                base32::decode(base32::Alphabet::Crockford, &old_filename)
                    .and_then(|s| String::from_utf8(s).ok())
            {
                u
            } else {
                warn!("Invalid base32 filename: {}", &old_filename);
                continue;
            };
            let new_path = self.cache_dir.join(Self::key(&old_url));
            if let Some(p) = new_path.parent() {
                create_dir_all(p)?;
            }

            if let Err(e) = std::fs::rename(file.path(), &new_path) {
                warn!(
                    "Failed to migrate file from {} to {}: {:?}",
                    file.path().display(),
                    new_path.display(),
                    e
                );
            }
        }
        Ok(())
    }
}

fn color_image_to_rgba(color_image: ColorImage) -> image::RgbaImage {
    let width = color_image.width() as u32;
    let height = color_image.height() as u32;

    let rgba_pixels: Vec<u8> = color_image
        .pixels
        .iter()
        .flat_map(|color| color.to_array()) // Convert Color32 to `[u8; 4]`
        .collect();

    image::RgbaImage::from_raw(width, height, rgba_pixels)
        .expect("Failed to create RgbaImage from ColorImage")
}

pub struct Images {
    pub static_imgs: MediaCache,
    pub gifs: MediaCache,
    pub urls: UrlMimes,
    pub gif_states: GifStateMap,
}

impl Images {
    /// path to directory to place [`MediaCache`]s
    pub fn new(path: path::PathBuf) -> Self {
        Self {
            static_imgs: MediaCache::new(&path, MediaCacheType::Image),
            gifs: MediaCache::new(&path, MediaCacheType::Gif),
            urls: UrlMimes::new(UrlCache::new(path.join(UrlCache::rel_dir()))),
            gif_states: Default::default(),
        }
    }

    pub fn migrate_v0(&self) -> Result<()> {
        self.static_imgs.migrate_v0()?;
        self.gifs.migrate_v0()
    }

    pub fn get_cache(&self, cache_type: MediaCacheType) -> &MediaCache {
        match cache_type {
            MediaCacheType::Image => &self.static_imgs,
            MediaCacheType::Gif => &self.gifs,
        }
    }

    pub fn get_cache_mut(&mut self, cache_type: MediaCacheType) -> &mut MediaCache {
        match cache_type {
            MediaCacheType::Image => &mut self.static_imgs,
            MediaCacheType::Gif => &mut self.gifs,
        }
    }
}

pub type GifStateMap = HashMap<String, GifState>;

pub struct GifState {
    pub last_frame_rendered: Instant,
    pub last_frame_duration: Duration,
    pub next_frame_time: Option<SystemTime>,
    pub last_frame_index: usize,
}
