//! Online SQLite backup API.
//!
//! To create a [`Backup`], you must have two distinct [`Connection`]s - one
//! for the source (which can be used while the backup is running) and one for
//! the destination (which cannot).  A [`Backup`] handle exposes three methods:
//! [`step`](Backup::step) will attempt to back up a specified number of pages,
//! [`progress`](Backup::progress) gets the current progress of the backup as of
//! the last call to [`step`](Backup::step), and
//! [`run_to_completion`](Backup::run_to_completion) will attempt to back up the
//! entire source database, allowing you to specify how many pages are backed up
//! at a time and how long the thread should sleep between chunks of pages.
//!
//! The following example is equivalent to "Example 2: Online Backup of a
//! Running Database" from [SQLite's Online Backup API
//! documentation](https://www.sqlite.org/backup.html).
//!
//! ```rust,no_run
//! # use rusqlite::{backup, Connection, Result};
//! # use std::path::Path;
//! # use std::time;
//!
//! fn backup_db<P: AsRef<Path>>(
//!     src: &Connection,
//!     dst: P,
//!     progress: fn(backup::Progress),
//! ) -> Result<()> {
//!     let mut dst = Connection::open(dst)?;
//!     let backup = backup::Backup::new(src, &mut dst)?;
//!     backup.run_to_completion(5, time::Duration::from_millis(250), Some(progress))
//! }
//! ```

use std::marker::PhantomData;
use std::path::Path;
use std::ptr;

use std::os::raw::c_int;
use std::thread;
use std::time::Duration;

use crate::ffi;

use crate::error::error_from_handle;
use crate::{Connection, DatabaseName, Result};

impl Connection {
    /// Back up the `name` database to the given
    /// destination path.
    ///
    /// If `progress` is not `None`, it will be called periodically
    /// until the backup completes.
    ///
    /// For more fine-grained control over the backup process (e.g.,
    /// to sleep periodically during the backup or to back up to an
    /// already-open database connection), see the `backup` module.
    ///
    /// # Failure
    ///
    /// Will return `Err` if the destination path cannot be opened
    /// or if the backup fails.
    pub fn backup<P: AsRef<Path>>(
        &self,
        name: DatabaseName<'_>,
        dst_path: P,
        progress: Option<fn(Progress)>,
    ) -> Result<()> {
        use self::StepResult::{Busy, Done, Locked, More};
        let mut dst = Connection::open(dst_path)?;
        let backup = Backup::new_with_names(self, name, &mut dst, DatabaseName::Main)?;

        let mut r = More;
        while r == More {
            r = backup.step(100)?;
            if let Some(f) = progress {
                f(backup.progress());
            }
        }

        match r {
            Done => Ok(()),
            Busy => Err(unsafe { error_from_handle(ptr::null_mut(), ffi::SQLITE_BUSY) }),
            Locked => Err(unsafe { error_from_handle(ptr::null_mut(), ffi::SQLITE_LOCKED) }),
            More => unreachable!(),
        }
    }

    /// Restore the given source path into the
    /// `name` database. If `progress` is not `None`, it will be
    /// called periodically until the restore completes.
    ///
    /// For more fine-grained control over the restore process (e.g.,
    /// to sleep periodically during the restore or to restore from an
    /// already-open database connection), see the `backup` module.
    ///
    /// # Failure
    ///
    /// Will return `Err` if the destination path cannot be opened
    /// or if the restore fails.
    pub fn restore<P: AsRef<Path>, F: Fn(Progress)>(
        &mut self,
        name: DatabaseName<'_>,
        src_path: P,
        progress: Option<F>,
    ) -> Result<()> {
        use self::StepResult::{Busy, Done, Locked, More};
        let src = Connection::open(src_path)?;
        let restore = Backup::new_with_names(&src, DatabaseName::Main, self, name)?;

        let mut r = More;
        let mut busy_count = 0_i32;
        'restore_loop: while r == More || r == Busy {
            r = restore.step(100)?;
            if let Some(ref f) = progress {
                f(restore.progress());
            }
            if r == Busy {
                busy_count += 1;
                if busy_count >= 3 {
                    break 'restore_loop;
                }
                thread::sleep(Duration::from_millis(100));
            }
        }

        match r {
            Done => Ok(()),
            Busy => Err(unsafe { error_from_handle(ptr::null_mut(), ffi::SQLITE_BUSY) }),
            Locked => Err(unsafe { error_from_handle(ptr::null_mut(), ffi::SQLITE_LOCKED) }),
            More => unreachable!(),
        }
    }
}

/// Possible successful results of calling
/// [`Backup::step`].
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum StepResult {
    /// The backup is complete.
    Done,

    /// The step was successful but there are still more pages that need to be
    /// backed up.
    More,

    /// The step failed because appropriate locks could not be acquired. This is
    /// not a fatal error - the step can be retried.
    Busy,

    /// The step failed because the source connection was writing to the
    /// database. This is not a fatal error - the step can be retried.
    Locked,
}

/// Struct specifying the progress of a backup. The
/// percentage completion can be calculated as `(pagecount - remaining) /
/// pagecount`. The progress of a backup is as of the last call to
/// [`step`](Backup::step) - if the source database is modified after a call to
/// [`step`](Backup::step), the progress value will become outdated and
/// potentially incorrect.
#[derive(Copy, Clone, Debug)]
pub struct Progress {
    /// Number of pages in the source database that still need to be backed up.
    pub remaining: c_int,
    /// Total number of pages in the source database.
    pub pagecount: c_int,
}

/// A handle to an online backup.
pub struct Backup<'a, 'b> {
    phantom_from: PhantomData<&'a Connection>,
    to: &'b Connection,
    b: *mut ffi::sqlite3_backup,
}

impl Backup<'_, '_> {
    /// Attempt to create a new handle that will allow backups from `from` to
    /// `to`. Note that `to` is a `&mut` - this is because SQLite forbids any
    /// API calls on the destination of a backup while the backup is taking
    /// place.
    ///
    /// # Failure
    ///
    /// Will return `Err` if the underlying `sqlite3_backup_init` call returns
    /// `NULL`.
    #[inline]
    pub fn new<'a, 'b>(from: &'a Connection, to: &'b mut Connection) -> Result<Backup<'a, 'b>> {
        Backup::new_with_names(from, DatabaseName::Main, to, DatabaseName::Main)
    }

    /// Attempt to create a new handle that will allow backups from the
    /// `from_name` database of `from` to the `to_name` database of `to`. Note
    /// that `to` is a `&mut` - this is because SQLite forbids any API calls on
    /// the destination of a backup while the backup is taking place.
    ///
    /// # Failure
    ///
    /// Will return `Err` if the underlying `sqlite3_backup_init` call returns
    /// `NULL`.
    pub fn new_with_names<'a, 'b>(
        from: &'a Connection,
        from_name: DatabaseName<'_>,
        to: &'b mut Connection,
        to_name: DatabaseName<'_>,
    ) -> Result<Backup<'a, 'b>> {
        let to_name = to_name.as_cstring()?;
        let from_name = from_name.as_cstring()?;

        let to_db = to.db.borrow_mut().db;

        let b = unsafe {
            let b = ffi::sqlite3_backup_init(
                to_db,
                to_name.as_ptr(),
                from.db.borrow_mut().db,
                from_name.as_ptr(),
            );
            if b.is_null() {
                return Err(error_from_handle(to_db, ffi::sqlite3_errcode(to_db)));
            }
            b
        };

        Ok(Backup {
            phantom_from: PhantomData,
            to,
            b,
        })
    }

    /// Gets the progress of the backup as of the last call to
    /// [`step`](Backup::step).
    #[inline]
    #[must_use]
    pub fn progress(&self) -> Progress {
        unsafe {
            Progress {
                remaining: ffi::sqlite3_backup_remaining(self.b),
                pagecount: ffi::sqlite3_backup_pagecount(self.b),
            }
        }
    }

    /// Attempts to back up the given number of pages. If `num_pages` is
    /// negative, will attempt to back up all remaining pages. This will hold a
    /// lock on the source database for the duration, so it is probably not
    /// what you want for databases that are currently active (see
    /// [`run_to_completion`](Backup::run_to_completion) for a better
    /// alternative).
    ///
    /// # Failure
    ///
    /// Will return `Err` if the underlying `sqlite3_backup_step` call returns
    /// an error code other than `DONE`, `OK`, `BUSY`, or `LOCKED`. `BUSY` and
    /// `LOCKED` are transient errors and are therefore returned as possible
    /// `Ok` values.
    #[inline]
    pub fn step(&self, num_pages: c_int) -> Result<StepResult> {
        use self::StepResult::{Busy, Done, Locked, More};

        let rc = unsafe { ffi::sqlite3_backup_step(self.b, num_pages) };
        match rc {
            ffi::SQLITE_DONE => Ok(Done),
            ffi::SQLITE_OK => Ok(More),
            ffi::SQLITE_BUSY => Ok(Busy),
            ffi::SQLITE_LOCKED => Ok(Locked),
            _ => self.to.decode_result(rc).map(|_| More),
        }
    }

    /// Attempts to run the entire backup. Will call
    /// [`step(pages_per_step)`](Backup::step) as many times as necessary,
    /// sleeping for `pause_between_pages` between each call to give the
    /// source database time to process any pending queries. This is a
    /// direct implementation of "Example 2: Online Backup of a Running
    /// Database" from [SQLite's Online Backup API documentation](https://www.sqlite.org/backup.html).
    ///
    /// If `progress` is not `None`, it will be called after each step with the
    /// current progress of the backup. Note that is possible the progress may
    /// not change if the step returns `Busy` or `Locked` even though the
    /// backup is still running.
    ///
    /// # Failure
    ///
    /// Will return `Err` if any of the calls to [`step`](Backup::step) return
    /// `Err`.
    pub fn run_to_completion(
        &self,
        pages_per_step: c_int,
        pause_between_pages: Duration,
        progress: Option<fn(Progress)>,
    ) -> Result<()> {
        use self::StepResult::{Busy, Done, Locked, More};

        assert!(pages_per_step > 0, "pages_per_step must be positive");

        loop {
            let r = self.step(pages_per_step)?;
            if let Some(progress) = progress {
                progress(self.progress());
            }
            match r {
                More | Busy | Locked => thread::sleep(pause_between_pages),
                Done => return Ok(()),
            }
        }
    }
}

impl Drop for Backup<'_, '_> {
    #[inline]
    fn drop(&mut self) {
        unsafe { ffi::sqlite3_backup_finish(self.b) };
    }
}

#[cfg(test)]
mod test {
    use super::{Backup, Progress};
    use crate::{Connection, DatabaseName, Result};
    use std::time::Duration;

    #[test]
    fn backup_to_path() -> Result<()> {
        let src = Connection::open_in_memory()?;
        src.execute_batch("CREATE TABLE foo AS SELECT 42 AS x")?;

        let temp_dir = tempfile::tempdir().unwrap();
        let path = temp_dir.path().join("test.db3");

        fn progress(_: Progress) {}

        src.backup(DatabaseName::Main, path.as_path(), Some(progress))?;

        let mut dst = Connection::open_in_memory()?;
        dst.restore(DatabaseName::Main, path, Some(progress))?;

        Ok(())
    }

    #[test]
    fn test_backup() -> Result<()> {
        let src = Connection::open_in_memory()?;
        let sql = "BEGIN;
                   CREATE TABLE foo(x INTEGER);
                   INSERT INTO foo VALUES(42);
                   END;";
        src.execute_batch(sql)?;

        let mut dst = Connection::open_in_memory()?;

        {
            let backup = Backup::new(&src, &mut dst)?;
            backup.step(-1)?;
        }

        let the_answer: i64 = dst.one_column("SELECT x FROM foo")?;
        assert_eq!(42, the_answer);

        src.execute_batch("INSERT INTO foo VALUES(43)")?;

        {
            let backup = Backup::new(&src, &mut dst)?;
            backup.run_to_completion(5, Duration::from_millis(250), None)?;
        }

        let the_answer: i64 = dst.one_column("SELECT SUM(x) FROM foo")?;
        assert_eq!(42 + 43, the_answer);
        Ok(())
    }

    #[test]
    fn test_backup_temp() -> Result<()> {
        let src = Connection::open_in_memory()?;
        let sql = "BEGIN;
                   CREATE TEMPORARY TABLE foo(x INTEGER);
                   INSERT INTO foo VALUES(42);
                   END;";
        src.execute_batch(sql)?;

        let mut dst = Connection::open_in_memory()?;

        {
            let backup =
                Backup::new_with_names(&src, DatabaseName::Temp, &mut dst, DatabaseName::Main)?;
            backup.step(-1)?;
        }

        let the_answer: i64 = dst.one_column("SELECT x FROM foo")?;
        assert_eq!(42, the_answer);

        src.execute_batch("INSERT INTO foo VALUES(43)")?;

        {
            let backup =
                Backup::new_with_names(&src, DatabaseName::Temp, &mut dst, DatabaseName::Main)?;
            backup.run_to_completion(5, Duration::from_millis(250), None)?;
        }

        let the_answer: i64 = dst.one_column("SELECT SUM(x) FROM foo")?;
        assert_eq!(42 + 43, the_answer);
        Ok(())
    }

    #[test]
    fn test_backup_attached() -> Result<()> {
        let src = Connection::open_in_memory()?;
        let sql = "ATTACH DATABASE ':memory:' AS my_attached;
                   BEGIN;
                   CREATE TABLE my_attached.foo(x INTEGER);
                   INSERT INTO my_attached.foo VALUES(42);
                   END;";
        src.execute_batch(sql)?;

        let mut dst = Connection::open_in_memory()?;

        {
            let backup = Backup::new_with_names(
                &src,
                DatabaseName::Attached("my_attached"),
                &mut dst,
                DatabaseName::Main,
            )?;
            backup.step(-1)?;
        }

        let the_answer: i64 = dst.one_column("SELECT x FROM foo")?;
        assert_eq!(42, the_answer);

        src.execute_batch("INSERT INTO foo VALUES(43)")?;

        {
            let backup = Backup::new_with_names(
                &src,
                DatabaseName::Attached("my_attached"),
                &mut dst,
                DatabaseName::Main,
            )?;
            backup.run_to_completion(5, Duration::from_millis(250), None)?;
        }

        let the_answer: i64 = dst.one_column("SELECT SUM(x) FROM foo")?;
        assert_eq!(42 + 43, the_answer);
        Ok(())
    }
}
