use crate::{
    async_trait,
    support::task::{Handler, Task},
    traits::{ApplicationTrait, ServiceTrait},
    Result, Schedule,
};
use soph_runtime::support::runtime;

#[async_trait]
impl ServiceTrait for Schedule {
    type Item = Task;

    fn new() -> Self {
        Self::default()
    }

    fn register(mut self, item: Self::Item) -> Self {
        self.tasks.push(item);

        self
    }

    fn init<A: ApplicationTrait>() -> Self {
        (&A::with_schedule() as &dyn std::any::Any)
            .downcast_ref::<Self>()
            .cloned()
            .unwrap_or_default()
    }

    async fn run(self) -> Result<()> {
        let sync_tasks = self
            .tasks()
            .iter()
            .filter(|task| !task.handler().is_async())
            .cloned()
            .collect::<Vec<_>>();

        if !sync_tasks.is_empty() {
            std::thread::spawn(|| foreground(sync_tasks));
        }

        let async_tasks = self
            .tasks()
            .iter()
            .filter(|task| task.handler().is_async())
            .cloned()
            .collect::<Vec<_>>();

        if !async_tasks.is_empty() {
            runtime().spawn(background(async_tasks));
        }

        Self::shutdown().await;

        Ok(())
    }
}

fn foreground(mut tasks: Vec<Task>) {
    loop {
        for task in &mut tasks {
            if !task.available() {
                continue;
            }

            if let Handler::Sync(handler) = task.handler() {
                handler().unwrap_or_else(|err| panic!("{}", err));
                task.set_last_tick(chrono::Local::now());
            }
        }

        std::thread::sleep(delay(&tasks));
    }
}

async fn background(mut tasks: Vec<Task>) {
    loop {
        for task in &mut tasks {
            if !task.available() {
                continue;
            }

            if let Handler::Async(handler) = task.handler() {
                handler().await.unwrap_or_else(|err| panic!("{}", err));
                task.set_last_tick(chrono::Local::now());
            }
        }

        tokio::time::sleep(delay(&tasks)).await;
    }
}

fn delay(tasks: &[Task]) -> std::time::Duration {
    tasks
        .iter()
        .map(|task| task.delay())
        .min()
        .unwrap_or(std::time::Duration::from_millis(500))
}
