use crate::app::App;
use crate::infrastructure::TokioRuntime;
use async_trait::async_trait;
use futures::channel::mpsc::Sender;
use gdk4::Texture;
use glib::Bytes;
use html2gtk::{DownloadProgress, ImageDownloadError, ImageDownloaderExt};
use news_flash::NewsFlash;
use news_flash::error::NewsFlashError;
use news_flash::models::ArticleID;
use reqwest::Client;
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio_util::sync::CancellationToken;

#[derive(Clone)]
pub struct ImageLoader {
    news_flash: Arc<RwLock<Option<NewsFlash>>>,
    cancellation_token: Arc<RwLock<CancellationToken>>,
}

impl Default for ImageLoader {
    fn default() -> Self {
        Self {
            news_flash: App::news_flash(),
            cancellation_token: Arc::new(RwLock::new(CancellationToken::new())),
        }
    }
}

impl ImageLoader {
    pub fn new() -> Self {
        Self::default()
    }

    fn map_err(error: NewsFlashError) -> ImageDownloadError {
        match error {
            NewsFlashError::ImageDownload(inner) => inner,
            _ => ImageDownloadError::Unknown,
        }
    }

    async fn download_impl(
        news_flash: Arc<RwLock<Option<NewsFlash>>>,
        article_id: ArticleID,
        url: String,
        client: Client,
        progress: Option<Sender<DownloadProgress>>,
    ) -> Result<Texture, ImageDownloadError> {
        let news_flash_guad = news_flash.read().await;
        let news_flash = news_flash_guad.as_ref().ok_or(ImageDownloadError::Unknown)?;
        let image = news_flash
            .get_image(&article_id, &url, &client, progress)
            .await
            .map_err(Self::map_err)?;

        let bytes = Bytes::from_owned(image.data);
        let loader = glycin::Loader::new_bytes(bytes);
        let image = loader.load().await.map_err(|_| ImageDownloadError::IO)?;
        let first_frame = image.next_frame().await.map_err(|_| ImageDownloadError::IO)?;
        let texture = first_frame.texture();
        Ok(texture)
    }
}

#[async_trait]
impl ImageDownloaderExt for ImageLoader {
    async fn download(
        &self,
        content_id: &str,
        url: &str,
        progress: Option<Sender<DownloadProgress>>,
    ) -> Result<Texture, ImageDownloadError> {
        let news_flash = self.news_flash.clone();
        let cancellation_token = self.cancellation_token.clone();
        let article_id = ArticleID::new(content_id);
        let url = url.to_string();

        let data = TokioRuntime::spawn(async move {
            let cancellation_token = cancellation_token.read().await.clone();
            let client = App::client().await;
            let future = Self::download_impl(news_flash, article_id, url, client, progress);

            tokio::select! {
                image = future => {
                    Ok(image)
                }
                _ = cancellation_token.cancelled() => {
                    tracing::debug!("image download was cancelled");
                    Err(ImageDownloadError::Cancelled)
                }
            }
        })
        .await
        .unwrap()??;
        Ok(data)
    }

    async fn cancel_all(&self) {
        let mut mut_guard = self.cancellation_token.write().await;
        mut_guard.cancel();
        *mut_guard = CancellationToken::new();
    }
}
