//! Environment and database flags.

use bitflags::bitflags;
use ffi::*;
use std::str::FromStr;

/// MDBX sync mode
#[derive(PartialEq, Eq, Clone, Copy, Debug, Default)]
pub enum SyncMode {
    /// Default robust and durable sync mode.
    /// Metadata is written and flushed to disk after a data is written and flushed, which
    /// guarantees the integrity of the database in the event of a crash at any time.
    #[default]
    Durable,

    /// Don't sync the meta-page after commit.
    ///
    /// Flush system buffers to disk only once per transaction commit, omit the metadata flush.
    /// Defer that until the system flushes files to disk, or next non-read-only commit or
    /// [`Environment::sync()`](crate::Environment::sync). Depending on the platform and
    /// hardware, with [`SyncMode::NoMetaSync`] you may get a doubling of write performance.
    ///
    /// This trade-off maintains database integrity, but a system crash may undo the last committed
    /// transaction. I.e. it preserves the ACPI (atomicity, consistency, isolation) but not D
    /// (durability) database property.
    NoMetaSync,

    /// Don't sync anything but keep previous steady commits.
    ///
    /// [`SyncMode::UtterlyNoSync`] the [`SyncMode::SafeNoSync`] flag disable similarly flush
    /// system buffers to disk when committing a transaction. But there is a huge difference in
    /// how are recycled the MVCC snapshots corresponding to previous "steady" transactions
    /// (see below).
    ///
    /// With [`crate::sys::EnvironmentKind::WriteMap`] the [`SyncMode::SafeNoSync`] instructs MDBX to
    /// use asynchronous mmap-flushes to disk. Asynchronous mmap-flushes means that actually
    /// all writes will scheduled and performed by operation system on it own manner, i.e.
    /// unordered. MDBX itself just notify operating system that it would be nice to write data
    /// to disk, but no more.
    ///
    /// Depending on the platform and hardware, with [`SyncMode::SafeNoSync`] you may get a
    /// multiple increase of write performance, even 10 times or more.
    ///
    /// In contrast to [`SyncMode::UtterlyNoSync`] mode, with [`SyncMode::SafeNoSync`] flag MDBX
    /// will keeps untouched pages within B-tree of the last transaction "steady" which was
    /// synced to disk completely. This has big implications for both data durability and
    /// (unfortunately) performance:
    /// - A system crash can't corrupt the database, but you will lose the last transactions;
    ///   because MDBX will rollback to last steady commit since it kept explicitly.
    /// - The last steady transaction makes an effect similar to "long-lived" read transaction
    ///   since prevents reuse of pages freed by newer write transactions, thus the any data
    ///   changes will be placed in newly allocated pages.
    /// - To avoid rapid database growth, the system will sync data and issue a steady commit-point
    ///   to resume reuse pages, each time there is insufficient space and before increasing the
    ///   size of the file on disk.
    ///
    /// In other words, with
    /// [`SyncMode::SafeNoSync`] flag MDBX protects you from the whole database corruption, at the
    /// cost increasing database size and/or number of disk IOPs. So, [`SyncMode::SafeNoSync`]
    /// flag could be used with [`Environment::sync()`](crate::Environment::sync) as alternatively
    /// for batch committing or nested transaction (in some cases).
    ///
    /// The number and volume of disk IOPs with [`SyncMode::SafeNoSync`] flag will exactly the
    /// as without any no-sync flags. However, you should expect a larger process's work set
    /// and significantly worse a locality of reference, due to the more intensive allocation
    /// of previously unused pages and increase the size of the database.
    SafeNoSync,

    /// Don't sync anything and wipe previous steady commits.
    ///
    /// Don't flush system buffers to disk when committing a transaction.
    /// This optimization means a system crash can corrupt the database, if buffers are not yet
    /// flushed to disk. Depending on the platform and hardware, with [`SyncMode::UtterlyNoSync`]
    /// you may get a multiple increase of write performance, even 100 times or more.
    ///
    /// If the filesystem preserves write order (which is rare and never provided unless explicitly
    /// noted) and the [`WriteMap`](crate::sys::EnvironmentKind::WriteMap) and
    /// [`EnvironmentFlags::liforeclaim`] flags are not used, then a system crash can't corrupt
    /// the database, but you can lose the last transactions, if at least one buffer is not yet
    /// flushed to disk. The risk is governed by how often the system flushes dirty buffers to
    /// disk and how often [`Environment::sync()`](crate::Environment::sync) is called. So,
    /// transactions exhibit ACPI (atomicity, consistency, isolation) properties and only lose D
    /// (durability). I.e. database integrity is maintained, but a system crash may undo the
    /// final transactions.
    ///
    /// Otherwise, if the filesystem not preserves write order (which is typically) or
    /// [`WriteMap`](crate::sys::EnvironmentKind::WriteMap) or [`EnvironmentFlags::liforeclaim`] flags
    /// are used, you should expect the corrupted database after a system crash.
    ///
    /// So, most important thing about [`SyncMode::UtterlyNoSync`]:
    /// - A system crash immediately after commit the write transaction high likely lead to
    ///   database corruption.
    /// - Successful completion of [`Environment::sync(force=true`)](crate::Environment::sync)
    ///   after one or more committed transactions guarantees consistency and durability.
    /// - BUT by committing two or more transactions you back database into a weak state, in which
    ///   a system crash may lead to database corruption! In case single transaction after
    ///   [`Environment::sync()`](crate::Environment::sync), you may lose transaction itself, but
    ///   not a whole database.
    ///
    /// Nevertheless, [`SyncMode::UtterlyNoSync`] provides "weak" durability in
    /// case of an application crash (but no durability on system failure), and therefore may
    /// be very useful in scenarios where data durability is not required over a system failure
    /// (e.g for short-lived data), or if you can take such risk.
    UtterlyNoSync,
}

/// Environment mode (read-only or read-write).
#[derive(Clone, Copy, Debug)]
pub enum Mode {
    /// Read-only mode.
    ReadOnly,
    /// Read-write mode with specified sync mode.
    ReadWrite {
        /// Sync mode for write transactions.
        sync_mode: SyncMode,
    },
}

impl Default for Mode {
    fn default() -> Self {
        Self::ReadWrite { sync_mode: SyncMode::default() }
    }
}

impl From<Mode> for EnvironmentFlags {
    fn from(mode: Mode) -> Self {
        Self { mode, ..Default::default() }
    }
}

impl FromStr for SyncMode {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let val = s.trim().to_ascii_lowercase();
        match val.as_str() {
            "durable" => Ok(Self::Durable),
            "safe-no-sync" | "safenosync" | "safe_no_sync" => Ok(Self::SafeNoSync),
            _ => Err(format!(
                "invalid value '{s}' for sync mode. valid values: durable, safe-no-sync"
            )),
        }
    }
}

/// Environment opening flags.
#[derive(Clone, Copy, Debug, Default)]
pub struct EnvironmentFlags {
    /// Do not create a subdirectory for the environment.
    pub no_sub_dir: bool,
    /// Open the environment in exclusive mode.
    pub exclusive: bool,
    /// Flag is intended to open an existing sub-database which was created with unknown flags
    /// In such cases, instead of returning the `MDBX_INCOMPATIBLE` error, the sub-database will be
    /// opened with flags which it was created, and then an application could determine the actual
    /// flags.
    pub accede: bool,
    /// Environment mode (RO/RW).
    pub mode: Mode,
    /// Disable read-ahead.
    pub no_rdahead: bool,
    /// Do not initialize memory before writing to it.
    pub no_meminit: bool,
    /// Enable page coalescing.
    pub coalesce: bool,
    /// Enable LIFO reclamation of pages.
    pub liforeclaim: bool,
}

impl EnvironmentFlags {
    /// Configures the mdbx flags to use when opening the environment.
    pub(crate) const fn make_flags(&self) -> ffi::MDBX_env_flags_t {
        let mut flags = 0;

        if self.no_sub_dir {
            flags |= ffi::MDBX_NOSUBDIR;
        }

        if self.exclusive {
            flags |= ffi::MDBX_EXCLUSIVE;
        }

        if self.accede {
            flags |= ffi::MDBX_ACCEDE;
        }

        match self.mode {
            Mode::ReadOnly => {
                flags |= ffi::MDBX_RDONLY;
            }
            Mode::ReadWrite { sync_mode } => {
                flags |= match sync_mode {
                    SyncMode::Durable => ffi::MDBX_SYNC_DURABLE,
                    SyncMode::NoMetaSync => ffi::MDBX_NOMETASYNC,
                    SyncMode::SafeNoSync => ffi::MDBX_SAFE_NOSYNC,
                    SyncMode::UtterlyNoSync => ffi::MDBX_UTTERLY_NOSYNC,
                };
            }
        }

        if self.no_rdahead {
            flags |= ffi::MDBX_NORDAHEAD;
        }

        if self.no_meminit {
            flags |= ffi::MDBX_NOMEMINIT;
        }

        if self.coalesce {
            flags |= ffi::MDBX_COALESCE;
        }

        if self.liforeclaim {
            flags |= ffi::MDBX_LIFORECLAIM;
        }

        flags |= ffi::MDBX_NOTLS;

        flags
    }
}

bitflags! {
    #[doc="Database options."]
    #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
    #[repr(transparent)]
    pub struct DatabaseFlags: MDBX_env_flags_t {
        /// Use reverse string comparison for keys.
        const REVERSE_KEY = MDBX_REVERSEKEY;
        /// Use sorted duplicates, i.e. allow multi-values for a keys.
        const DUP_SORT = MDBX_DUPSORT;
        /// Numeric keys in native byte order either uint32_t or uint64_t (must
        /// be one of uint32_t or uint64_t, other integer types, for example,
        /// signed integer or uint16_t will not work). The keys must all be of
        /// the same size and must be aligned while passing as arguments.
        const INTEGER_KEY = MDBX_INTEGERKEY;
        /// With MDBX_DUPSORT; sorted dup items have fixed size. The data
        /// values must all be of the same size.
        const DUP_FIXED = MDBX_DUPFIXED;
        /// With MDBX_DUPSORT and with MDBX_DUPFIXED; dups are fixed size like
        /// MDBX_INTEGERKEY -style integers. The data values must all be of the
        /// same size and must be aligned while passing as arguments.
        const INTEGER_DUP = MDBX_INTEGERDUP;
        /// With MDBX_DUPSORT; use reverse string comparison for data values.
        const REVERSE_DUP = MDBX_REVERSEDUP;
        /// Create DB if not already existing.
        const CREATE = MDBX_CREATE;
        /// Open an existing table which may be open by another process
        /// with unknown mode/flags. In such cases, instead of returning the
        /// `MDBX_INCOMPATIBLE` error, the database will be opened with flags
        /// which it was created, and then an application could determine the
        /// actual flags.
        const ACCEDE = MDBX_DB_ACCEDE;
    }
}

bitflags! {
    #[doc="Write options."]
    #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
    #[repr(transparent)]
    pub struct WriteFlags: MDBX_env_flags_t {
        /// Upsertion by default (without any other flags)
        const UPSERT = MDBX_UPSERT;
        /// For insertion: Don't write if the key already exists.
        const NO_OVERWRITE = MDBX_NOOVERWRITE;
        /// Has effect only for MDBX_DUPSORT tables. For upsertion: don't write
        /// if the key-value pair already exist.
        const NO_DUP_DATA = MDBX_NODUPDATA;
        /// For upsertion: overwrite the current key/data pair. MDBX allows
        /// this flag for mdbx_put() for explicit overwrite/update without
        /// insertion. For deletion: remove only single entry at the current
        /// cursor position.
        const CURRENT = MDBX_CURRENT;
        /// Has effect only for MDBX_DUPSORT tables. For deletion: remove all
        /// multi-values (aka duplicates) for given key. For upsertion: replace
        /// all multi-values for given key with a new one.
        const ALLDUPS = MDBX_ALLDUPS;
        /// For upsertion: Just reserve space for data, don't copy it. Return a
        /// pointer to the reserved space.
        const RESERVE = MDBX_RESERVE;
        /// Data is being appended. Don't split full pages, continue on a new
        /// instead.
        const APPEND = MDBX_APPEND;
        /// Has effect only for MDBX_DUPSORT tables. Duplicate data is being
        /// appended. Don't split full pages, continue on a new instead.
        const APPEND_DUP = MDBX_APPENDDUP;
        /// Only for MDBX_DUPFIXED. Store multiple data items in one call.
        const MULTIPLE = MDBX_MULTIPLE;
    }
}
