use std::ops::Deref;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;

use crate::Result;

/// A directory, which is possibly self-destructing.
///
/// References a directory.  The directory may be a permanent
/// directory, or a temporary directory, which is automatically
/// cleaned up when the data structure is dropped.
#[derive(Clone, Debug)]
pub enum Directory {
    Directory(PathBuf),
    TempDir(Arc<tempfile::TempDir>),
}

impl Deref for Directory {
    type Target = Path;

    fn deref(&self) -> &Path {
        match self {
            Directory::Directory(d) => d.as_path(),
            Directory::TempDir(d) => d.path(),
        }
    }
}

impl From<PathBuf> for Directory {
    fn from(pathbuf: PathBuf) -> Directory {
        Directory::Directory(pathbuf)
    }
}

impl From<&Path> for Directory {
    fn from(path: &Path) -> Directory {
        Directory::Directory(path.to_path_buf())
    }
}

impl From<tempfile::TempDir> for Directory {
    fn from(tempdir: tempfile::TempDir) -> Directory {
        Directory::TempDir(Arc::new(tempdir))
    }
}

impl AsRef<Path> for Directory {
    fn as_ref(&self) -> &Path {
        self.deref()
    }
}

impl Directory {
    /// Returns an ephemeral home directory.
    ///
    /// An ephmeral home directory is a temporary directory that is
    /// removed when `self` goes out of scope.
    pub fn ephemeral() -> Result<Self> {
        Ok(Directory::from(
            tempfile::Builder::new().prefix("sq-keystore").tempdir()?))
    }

    /// Returns the home directory in a `PathBuf`.
    pub fn to_path_buf(&self) -> PathBuf {
        self.as_ref().to_path_buf()
    }
}

/// Maps a panic of a worker thread to an error.
///
/// Unfortunately, there is nothing useful to do with the error, but
/// returning a generic error is better than panicking.
fn map_panic(_: Box<dyn std::any::Any + std::marker::Send>) -> anyhow::Error
{
    anyhow::anyhow!("worker thread panicked")
}

/// A helper function to run an asynchronous task in a sync context.
///
/// If we are in a sync context, we can't use any async functions
/// without creating a new async runtime.  Although it is possible to
/// get a handle to an existing runtime using
/// [`tokio::runtime::Handle::try_current`](https://docs.rs/tokio/latest/tokio/runtime/struct.Handle.html#method.try_current),
/// we can't actually use it to run a sync function.  Specifically, if
/// we do:
///
/// ```text
/// if let Ok(rt) = tokio::runtime::Handle::try_current() {
///     Ok(rt.block_on(f))
/// }
/// ```
///
/// then Tokio will panic:
///
/// ```text
/// thread 'tests::verify' panicked at 'Cannot start a runtime from within a runtime. This happens because a function (like `block_on`) attempted to block the current thread while the thread is being used to drive asynchronous tasks.', backend/src/utils.rs:97:15
/// ```
///
/// Instead, we check if there is an async runtime, if *not*, then we
/// create an async runtime using Tokio, run the async function, and
/// return.  If there is an async runtime, we start a new thread,
/// create a new runtime there, and execute the function using that
/// runtime.
pub fn run_async<F: std::future::Future + Send>(f: F) -> Result<F::Output>
where F::Output: Send
{
    use tokio::runtime::{Handle, Runtime};

    // See if the current thread is managed by a tokio
    // runtime.
    if Handle::try_current().is_err() {
        // Doesn't seem to be the case, so it is safe
        // to create a new runtime and block.
        let rt = Runtime::new()?;
        Ok(rt.block_on(f))
    } else {
        // It is!  We must not create a second runtime
        // on this thread, but instead we will
        // delegate this to a new thread.
        let r: Result<F::Output> = std::thread::scope(|s| {
            s.spawn(move || -> Result<F::Output> {
                let rt = Runtime::new()?;
                Ok(rt.block_on(f))
            }).join()
        }).map_err(map_panic)?;
        r
    }
}
