// yt - A fully featured command line YouTube client
//
// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This file is part of Yt.
//
// You should have received a copy of the License along with this program.
// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.

use std::{env::current_exe, mem, usize};

use anyhow::{bail, Result};
use libmpv2::{events::Event, EndFileReason, Mpv};
use log::{debug, info};
use tokio::process::Command;

use crate::{
    app::App,
    comments::get_comments,
    storage::video_database::{
        extractor_hash::ExtractorHash,
        getters::{get_video_by_hash, get_video_mpv_opts, get_videos},
        setters::{set_state_change, set_video_watched},
        VideoStatus,
    },
};

pub struct MpvEventHandler {
    currently_playing_index: Option<usize>,
    current_playlist_position: usize,
    current_playlist: Vec<ExtractorHash>,
}

impl MpvEventHandler {
    pub fn from_playlist(playlist: Vec<ExtractorHash>) -> Self {
        Self {
            currently_playing_index: None,
            current_playlist: playlist,
            current_playlist_position: 0,
        }
    }

    /// Checks, whether new videos are ready to be played
    pub async fn possibly_add_new_videos(&mut self, app: &App, mpv: &Mpv) -> Result<()> {
        let play_things = get_videos(app, &[VideoStatus::Cached], Some(false)).await?;

        // There is nothing to watch
        if play_things.len() == 0 {
            return Ok(());
        }

        let play_things = play_things
            .into_iter()
            .filter(|val| !self.current_playlist.contains(&val.extractor_hash))
            .collect::<Vec<_>>();

        info!(
            "{} videos are cached and will be added to the list to be played",
            play_things.len()
        );

        self.current_playlist.reserve(play_things.len());

        for play_thing in play_things {
            debug!("Adding '{}' to playlist.", play_thing.title);

            let orig_cache_path = play_thing.cache_path.expect("Is cached and thus some");
            let cache_path = orig_cache_path.to_str().expect("Should be vaild utf8");
            let cache_path = format!("\"{}\"", cache_path);

            let args = &[&cache_path, "append-play"];

            mpv.execute("loadfile", args)?;
            self.current_playlist.push(play_thing.extractor_hash);
        }

        Ok(())
    }

    async fn mark_video_watched(&mut self, app: &App, hash: &ExtractorHash) -> Result<()> {
        let video = get_video_by_hash(app, hash).await?;
        set_video_watched(&app, &video).await?;
        Ok(())
    }
    async fn mark_cvideo_watched(&mut self, app: &App) -> Result<()> {
        if let Some(index) = self.currently_playing_index {
            let video_hash = self.current_playlist[(index) as usize].clone();
            self.mark_video_watched(app, &video_hash).await?;
        }
        Ok(())
    }

    async fn mark_cvideo_inactive(&mut self, app: &App) -> Result<()> {
        if let Some(index) = self.currently_playing_index {
            let video_hash = &self.current_playlist[(index) as usize];
            self.currently_playing_index = None;
            set_state_change(&app, video_hash, false).await?;
        }
        Ok(())
    }
    async fn mark_video_active(&mut self, app: &App, playlist_index: usize) -> Result<()> {
        let video_hash = &self.current_playlist[(playlist_index) as usize];
        self.currently_playing_index = Some(playlist_index);
        set_state_change(&app, video_hash, true).await?;
        Ok(())
    }

    /// Apply the options set with e.g. `watch --speed=<speed>`
    async fn apply_options(&self, app: &App, mpv: &Mpv, hash: &ExtractorHash) -> Result<()> {
        let options = get_video_mpv_opts(app, hash).await?;

        mpv.set_property("speed", options.playback_speed)?;

        Ok(())
    }

    /// This will return [`true`], if the event handling should be stopped
    pub async fn handle_mpv_event<'a>(
        &mut self,
        app: &App,
        mpv: &Mpv,
        event: Event<'a>,
    ) -> Result<bool> {
        match event {
            Event::EndFile(r) => match r {
                EndFileReason::Eof => {
                    info!("Mpv reached eof of current video. Marking it inactive.");

                    self.mark_cvideo_inactive(app).await?;
                }
                EndFileReason::Stop => {
                    info!("Mpv stopped current video. Marking it inactive.");

                    // TODO: Should we also mark the video watched? <2024-08-21>

                    self.mark_cvideo_inactive(app).await?;
                }
                EndFileReason::Quit => {
                    info!("Mpv quit. Exiting playback");

                    // draining the playlist is okay, as mpv is done playing
                    let videos = mem::take(&mut self.current_playlist);
                    for video in videos {
                        self.mark_video_watched(app, &video).await?;
                        set_state_change(&app, &video, false).await?;
                    }
                    return Ok(true);
                }
                EndFileReason::Error => {
                    unreachable!("This will be raised as a separate error")
                }
                EndFileReason::Redirect => {
                    todo!("We probably need to handle this somehow");
                }
            },
            Event::StartFile(playlist_index) => {
                self.possibly_add_new_videos(app, &mpv).await?;

                self.mark_video_active(app, (playlist_index - 1) as usize)
                    .await?;
                self.current_playlist_position = (playlist_index - 1) as usize;
                self.apply_options(
                    app,
                    mpv,
                    &self.current_playlist[self.current_playlist_position],
                )
                .await?;
            }
            Event::FileLoaded => {}
            Event::ClientMessage(a) => {
                debug!("Got Client Message event: '{}'", a.join(" "));

                match a.as_slice() {
                    &["yt-comments-external"] => {
                        let binary = current_exe().expect("A current exe should exist");

                        let status = Command::new("riverctl")
                            .args(["focus-output", "next"])
                            .status()
                            .await?;
                        if !status.success() {
                            bail!("focusing the next output failed!");
                        }

                        let status = Command::new("alacritty")
                            .args(&[
                                "--title",
                                "floating please",
                                "--command",
                                binary.to_str().expect("Should be valid unicode"),
                                "comments",
                            ])
                            .status()
                            .await?;
                        if !status.success() {
                            bail!("Falied to start `yt comments`");
                        }

                        let status = Command::new("riverctl")
                            .args(["focus-output", "next"])
                            .status()
                            .await?;
                        if !status.success() {
                            bail!("focusing the next output failed!");
                        }
                    }
                    &["yt-comments-local"] => {
                        let comments: String = get_comments(app)
                            .await?
                            .render(false)
                            .replace("\"", "")
                            .replace("'", "")
                            .chars()
                            .take(app.config.watch.local_comments_length)
                            .collect();

                        mpv.execute("show-text", &[&format!("'{}'", comments), "6000"])?;
                    }
                    &["yt-description"] => {
                        // let description = description(app).await?;
                        mpv.execute("script-message", &["osc-message", "'<YT Description>'"])?;
                    }
                    &["yt-mark-watch-later"] => {
                        self.mark_cvideo_inactive(app).await?;
                        mpv.execute("write-watch-later-config", &[])?;
                        mpv.execute("playlist-remove", &["current"])?;
                    }
                    &["yt-mark-done-and-go-next"] => {
                        self.mark_cvideo_inactive(app).await?;
                        self.mark_cvideo_watched(app).await?;

                        mpv.execute("playlist-remove", &["current"])?;
                    }
                    &["yt-check-new-videos"] => {
                        self.possibly_add_new_videos(app, mpv).await?;
                    }
                    other => {
                        debug!("Unknown message: {}", other.join(" "))
                    }
                }
            }
            _ => {}
        }

        Ok(false)
    }
}