use super::ext::ImageDownloaderExt;
use crate::Error;
use article_scraper::images::ImageDownloader;
use article_scraper::{DownloadProgress, ImageDownloadError};
use async_trait::async_trait;
use futures::channel::mpsc::Sender;
use gtk4::gdk::Texture;
use gtk4::glib::Bytes;
use reqwest::Client;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::io::AsyncReadExt;
use tokio::runtime::Runtime;
use tokio::sync::RwLock;
use tokio_util::sync::CancellationToken;

#[derive(Debug, Clone)]
pub struct DefaultImageDownloader {
    destination: PathBuf,
    client: Client,
    runtime: Arc<Runtime>,
    cancellation_token: Arc<RwLock<CancellationToken>>,
}

impl DefaultImageDownloader {
    pub fn new(destination: &Path, client: &Client, runtime: &Arc<Runtime>) -> Result<Self, Error> {
        std::fs::DirBuilder::new()
            .recursive(true)
            .create(destination)?;
        Ok(Self {
            destination: destination.to_path_buf(),
            client: client.clone(),
            runtime: runtime.clone(),
            cancellation_token: Arc::new(RwLock::new(CancellationToken::new())),
        })
    }

    async fn download_impl(
        destination: PathBuf,
        content_id: String,
        url: String,
        client: Client,
        progress: Option<Sender<DownloadProgress>>,
    ) -> Result<Texture, ImageDownloadError> {
        let file_name = sanitize_filename::sanitize(format!("{content_id}_{url}"));
        tracing::debug!(file_name, "download image");
        let path = destination.join(file_name);

        if path.exists() {
            let mut file = tokio::fs::File::open(&path).await.map_err(|error| {
                tracing::error!(%error, "Failed to open image file");
                ImageDownloadError::IO
            })?;
            let mut contents = vec![];
            file.read_to_end(&mut contents).await.map_err(|error| {
                tracing::error!(%error, "Failed to read image file");
                ImageDownloadError::IO
            })?;

            let bytes = Bytes::from_owned(contents);
            let loader = glycin::Loader::new_bytes(bytes);
            let image = loader.load().await.map_err(|error| {
                tracing::error!(%error, "Failed to load image data");
                ImageDownloadError::IO
            })?;
            let frame = image.next_frame().await.map_err(|error| {
                tracing::error!(%error, "Failed to decode image frame");
                ImageDownloadError::IO
            })?;
            return Ok(frame.texture());
        }

        let url = url.to_string();
        let client = client.clone();
        let data = ImageDownloader::single_from_url(&url, &client, progress).await?;
        tracing::debug!(?path, "writing image");

        tokio::fs::write(&path, &data).await.map_err(|error| {
            tracing::error!(%error, "Failed to write image");
            ImageDownloadError::IO
        })?;

        let bytes = Bytes::from_owned(data);
        let loader = glycin::Loader::new_bytes(bytes);
        let image = loader.load().await.map_err(|error| {
            tracing::error!(%error, "Failed to load image data");
            ImageDownloadError::IO
        })?;
        let frame = image.next_frame().await.map_err(|error| {
            tracing::error!(%error, "Failed to decode image frame");
            ImageDownloadError::IO
        })?;

        Ok(frame.texture())
    }
}

#[async_trait]
impl ImageDownloaderExt for DefaultImageDownloader {
    async fn download(
        &self,
        content_id: &str,
        url: &str,
        progress: Option<Sender<DownloadProgress>>,
    ) -> Result<Texture, ImageDownloadError> {
        let future = Self::download_impl(
            self.destination.clone(),
            content_id.to_string(),
            url.to_string(),
            self.client.clone(),
            progress,
        );

        let cancellation_token = self.cancellation_token.clone();
        let data = self
            .runtime
            .spawn(async move {
                let cancellation_token = cancellation_token.read().await.clone();

                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();
    }
}
