use std::io;

use super::{convert_to_kibibytes, FileWrapper, ProcResult};

#[cfg(feature = "serde1")]
use serde::{Deserialize, Serialize};

/// This  struct  reports  statistics about memory usage on the system, based on
/// the `/proc/meminfo` file.
///
/// It is used by `free(1)` to report the amount of free and used memory (both
/// physical  and  swap)  on  the  system  as well as the shared memory and
/// buffers used by the kernel.  Each struct member is generally reported in
/// bytes, but a few are unitless values.
///
/// Except as noted below, all of the fields have been present since at least
/// Linux 2.6.0.  Some fields are optional and are present only if the kernel
/// was configured with various options; those dependencies are noted in the list.
///
/// **Notes**
///
/// While the file shows kilobytes (kB; 1 kB equals 1000 B),
/// it is actually kibibytes (KiB; 1 KiB equals 1024 B).
///
/// This imprecision in /proc/meminfo is known,
/// but is not corrected due to legacy concerns -
/// programs rely on /proc/meminfo to specify size with the "kB" string.
///
/// New fields to this struct may be added at any time (even without a major or minor semver bump).
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[allow(non_snake_case)]
#[non_exhaustive]
pub struct Meminfo {
    /// Total usable RAM (i.e., physical RAM minus a few reserved bits and the kernel binary code).
    pub mem_total: u64,
    /// The sum of [LowFree](#structfield.low_free) + [HighFree](#structfield.high_free).
    pub mem_free: u64,
    /// An estimate of how much memory is available for starting new applications, without swapping.
    ///
    /// (since Linux 3.14)
    pub mem_available: Option<u64>,
    /// Relatively temporary storage for raw disk blocks that shouldn't get tremendously large (20MB or so).
    pub buffers: u64,
    /// In-memory cache for files read from the disk (the page cache).  Doesn't include SwapCached.
    pub cached: u64,
    /// Memory  that  once  was  swapped  out, is swapped back in but still also is in the swap
    /// file.
    ///
    /// (If memory pressure is high, these pages don't need to be swapped out again
    /// because they are already in the swap file.  This saves I/O.)
    pub swap_cached: u64,
    /// Memory that has been used more recently and usually not reclaimed unless absolutely
    /// necessary.
    pub active: u64,
    /// Memory which has been less recently used.  It is more eligible to be reclaimed for other
    /// purposes.
    pub inactive: u64,
    /// [To be documented.]
    ///
    /// (since Linux 2.6.28)
    pub active_anon: Option<u64>,
    /// [To be documented.]
    ///
    /// (since Linux 2.6.28)
    pub inactive_anon: Option<u64>,
    /// [To be documented.]
    ///
    /// (since Linux 2.6.28)
    pub active_file: Option<u64>,
    /// [To be documented.]
    ///
    /// (since Linux 2.6.28)
    pub inactive_file: Option<u64>,
    /// [To be documented.]
    ///
    /// (From Linux 2.6.28 to 2.6.30, CONFIG_UNEVICTABLE_LRU was required.)
    pub unevictable: Option<u64>,
    /// [To be documented.]
    ///
    /// (From Linux 2.6.28 to 2.6.30, CONFIG_UNEVICTABLE_LRU was required.)
    pub mlocked: Option<u64>,
    /// Total amount of highmem.
    ///
    /// Highmem is all memory above ~860MB of physical  memory.  Highmem areas are for use by
    /// user-space programs, or for the page cache.  The kernel must use tricks to access this
    /// memory, making it slower to access than lowmem.
    ///
    /// (Starting with Linux 2.6.19, CONFIG_HIGHMEM is required.)
    pub high_total: Option<u64>,
    /// Amount of free highmem.
    ///
    /// (Starting with Linux 2.6.19, CONFIG_HIGHMEM is required.)
    pub high_free: Option<u64>,
    /// Total amount of lowmem.
    ///
    /// Lowmem is memory which can be used for every thing  that highmem can be used for,
    /// but it is also available for the kernel's use for its own data structures.
    /// Among many other things, it is where everything from Slab is allocated.
    /// Bad things happen when you're out of lowmem.
    ///
    /// (Starting with Linux 2.6.19, CONFIG_HIGHMEM is required.)
    pub low_total: Option<u64>,
    /// Amount of free lowmem.
    ///
    /// (Starting with Linux 2.6.19, CONFIG_HIGHMEM is required.)
    pub low_free: Option<u64>,
    /// [To be documented.]
    ///
    /// (since Linux 2.6.29.  CONFIG_MMU is required.)
    pub mmap_copy: Option<u64>,
    /// Total amount of swap space available.
    pub swap_total: u64,
    /// Amount of swap space that is currently unused.
    pub swap_free: u64,
    /// Memory which is waiting to get written back to the disk.
    pub dirty: u64,
    /// Memory which is actively being written back to the disk.
    pub writeback: u64,
    /// Non-file backed pages mapped into user-space page tables.
    ///
    /// (since Linux 2.6.18)
    pub anon_pages: Option<u64>,
    /// Files which have been mapped into memory (with mmap(2)), such as libraries.
    pub mapped: u64,
    /// Amount of memory consumed in tmpfs(5) filesystems.
    ///
    /// (since Linux 2.6.32)
    pub shmem: Option<u64>,
    /// In-kernel data structures cache.
    pub slab: u64,
    /// Part of Slab, that cannot be reclaimed on memory pressure.
    ///
    /// (since Linux 2.6.19)
    pub s_reclaimable: Option<u64>,
    /// Part of Slab, that cannot be reclaimed on memory pressure.
    ///
    /// (since Linux 2.6.19)
    pub s_unreclaim: Option<u64>,
    /// Amount of memory allocated to kernel stacks.
    ///
    /// (since Linux 2.6.32)
    pub kernel_stack: Option<u64>,
    /// Amount of memory dedicated to the lowest level of page tables.
    ///
    /// (since Linux 2.6.18)
    pub page_tables: Option<u64>,
    /// [To be documented.]
    ///
    /// (CONFIG_QUICKLIST is required.  Since Linux 2.6.27)
    pub quicklists: Option<u64>,
    /// NFS pages sent to the server, but not yet committed to stable storage.
    ///
    /// (since Linux 2.6.18)
    pub nfs_unstable: Option<u64>,
    /// Memory used for block device "bounce buffers".
    ///
    /// (since Linux 2.6.18)
    pub bounce: Option<u64>,
    /// Memory used by FUSE for temporary writeback buffers.
    ///
    /// (since Linux 2.6.26)
    pub writeback_tmp: Option<u64>,
    /// This is the total amount of memory currently available to be allocated on the system,
    /// expressed  in bytes.
    ///
    /// This  limit  is adhered  to  only if strict overcommit
    /// accounting is enabled (mode 2 in /proc/sys/vm/overcommit_memory).  The limit is calculated
    /// according to the formula described under /proc/sys/vm/overcommit_memory.  For further
    /// details, see the kernel source  file
    /// [Documentation/vm/overcommit-accounting](https://www.kernel.org/doc/Documentation/vm/overcommit-accounting).
    ///
    /// (since Linux 2.6.10)
    pub commit_limit: Option<u64>,
    /// The  amount of memory presently allocated on the system.
    ///
    /// The committed memory is a sum of all of the memory which has been allocated
    /// by processes, even if it has not been "used" by them as of yet.  A process which allocates 1GB of memory  (using  malloc(3)
    /// or  similar),  but  touches only 300MB of that memory will show up as using only 300MB of memory even if it has the address space
    /// allocated for the entire 1GB.
    ///
    /// This 1GB is memory which has been "committed" to by the VM and can be used at any  time  by  the  allocating  application.   With
    /// strict  overcommit  enabled  on  the  system  (mode 2 in /proc/sys/vm/overcommit_memory), allocations which would exceed the Committed_AS
    /// mitLimit will not be permitted.  This is useful if one needs to guarantee that processes will not fail due to lack of memory once
    /// that memory has been successfully allocated.
    pub committed_as: u64,
    /// Total size of vmalloc memory area.
    pub vmalloc_total: u64,
    /// Amount of vmalloc area which is used.
    pub vmalloc_used: u64,
    /// Largest contiguous block of vmalloc area which is free.
    pub vmalloc_chunk: u64,
    /// [To be documented.]
    ///
    /// (CONFIG_MEMORY_FAILURE is required.  Since Linux 2.6.32)
    pub hardware_corrupted: Option<u64>,
    /// Non-file backed huge pages mapped into user-space page tables.
    ///
    /// (CONFIG_TRANSPARENT_HUGEPAGE is required.  Since Linux 2.6.38)
    pub anon_hugepages: Option<u64>,
    /// Memory used by shared memory (shmem) and tmpfs(5) allocated with huge pages
    ///
    /// (CONFIG_TRANSPARENT_HUGEPAGE is required.  Since Linux 4.8)
    pub shmem_hugepages: Option<u64>,
    /// Shared memory mapped into user space with huge pages.
    ///
    /// (CONFIG_TRANSPARENT_HUGEPAGE is required.  Since Linux 4.8)
    pub shmem_pmd_mapped: Option<u64>,
    /// Total CMA (Contiguous Memory Allocator) pages.
    ///
    /// (CONFIG_CMA is required.  Since Linux 3.1)
    pub cma_total: Option<u64>,
    /// Free CMA (Contiguous Memory Allocator) pages.
    ///
    /// (CONFIG_CMA is required.  Since Linux 3.1)
    pub cma_free: Option<u64>,
    /// The size of the pool of huge pages.
    ///
    /// CONFIG_HUGETLB_PAGE is required.)
    pub hugepages_total: Option<u64>,
    /// The number of huge pages in the pool that are not yet allocated.
    ///
    /// (CONFIG_HUGETLB_PAGE is required.)
    pub hugepages_free: Option<u64>,
    /// This is the number of huge pages for which a commitment to allocate from the pool has been
    /// made, but no allocation has yet been made.
    ///
    /// These reserved huge pages guarantee that an application will be able  to  allocate  a
    /// huge page from the pool of huge pages at fault time.
    ///
    /// (CONFIG_HUGETLB_PAGE  is  required.  Since Linux 2.6.17)
    pub hugepages_rsvd: Option<u64>,
    /// This is the number of huge pages in the pool above the value in /proc/sys/vm/nr_hugepages.
    ///
    /// The maximum number of surplus huge pages is controlled by /proc/sys/vm/nr_overcommit_hugepages.
    ///
    /// (CONFIG_HUGETLB_PAGE  is  required.  Since Linux 2.6.24)
    pub hugepages_surp: Option<u64>,
    /// The size of huge pages.
    ///
    /// (CONFIG_HUGETLB_PAGE is required.)
    pub hugepagesize: Option<u64>,
    /// Number of bytes of RAM linearly mapped by kernel in 4kB pages.  (x86.)
    ///
    /// (since Linux 2.6.27)
    pub direct_map_4k: Option<u64>,
    /// Number of bytes of RAM linearly mapped by kernel in 4MB pages.
    ///
    /// (x86 with CONFIG_X86_64 or CONFIG_X86_PAE enabled.  Since Linux 2.6.27)
    pub direct_map_4M: Option<u64>,
    /// Number of bytes of RAM linearly mapped by kernel in 2MB pages.
    ///
    /// (x86 with neither CONFIG_X86_64 nor CONFIG_X86_PAE enabled.  Since Linux 2.6.27)
    pub direct_map_2M: Option<u64>,
    /// (x86 with CONFIG_X86_64 and CONFIG_X86_DIRECT_GBPAGES enabled.  Since Linux 2.6.27)
    pub direct_map_1G: Option<u64>,

    /// needs documentation
    pub hugetlb: Option<u64>,

    /// Memory allocated to the per-cpu alloctor used to back per-cpu allocations.
    ///
    /// This stat excludes the cost of metadata.
    pub per_cpu: Option<u64>,

    /// Kernel allocations that the kernel will attempt to reclaim under memory pressure.
    ///
    /// Includes s_reclaimable, and other direct allocations with a shrinker.
    pub k_reclaimable: Option<u64>,

    /// Undocumented field
    ///
    /// (CONFIG_TRANSPARENT_HUGEPAGE is requried.  Since Linux 5.4)
    pub file_pmd_mapped: Option<u64>,

    /// Undocumented field
    ///
    /// (CONFIG_TRANSPARENT_HUGEPAGE is required.  Since Linux 5.4)
    pub file_huge_pages: Option<u64>,
}

impl Meminfo {
    /// Reads and parses the `/proc/meminfo`, returning an error if there are problems.
    pub fn new() -> ProcResult<Meminfo> {
        let f = FileWrapper::open("/proc/meminfo")?;

        Meminfo::from_reader(f)
    }

    /// Get Meminfo from a custom Read instead of the default `/proc/meminfo`.
    pub fn from_reader<R: io::Read>(r: R) -> ProcResult<Meminfo> {
        use std::collections::HashMap;
        use std::io::{BufRead, BufReader};

        let reader = BufReader::new(r);
        let mut map = HashMap::new();

        for line in reader.lines() {
            let line = expect!(line);
            if line.is_empty() {
                continue;
            }
            let mut s = line.split_whitespace();
            let field = expect!(s.next(), "no field");
            let value = expect!(s.next(), "no value");
            let unit = s.next(); // optional

            let value = from_str!(u64, value);

            let value = if let Some(unit) = unit {
                convert_to_kibibytes(value, unit)?
            } else {
                value
            };

            map.insert(field[..field.len() - 1].to_string(), value);
        }

        // use 'remove' to move the value out of the hashmap
        // if there's anything still left in the map at the end, that
        // means we probably have a bug/typo, or are out-of-date
        let meminfo = Meminfo {
            mem_total: expect!(map.remove("MemTotal")),
            mem_free: expect!(map.remove("MemFree")),
            mem_available: map.remove("MemAvailable"),
            buffers: expect!(map.remove("Buffers")),
            cached: expect!(map.remove("Cached")),
            swap_cached: expect!(map.remove("SwapCached")),
            active: expect!(map.remove("Active")),
            inactive: expect!(map.remove("Inactive")),
            active_anon: map.remove("Active(anon)"),
            inactive_anon: map.remove("Inactive(anon)"),
            active_file: map.remove("Active(file)"),
            inactive_file: map.remove("Inactive(file)"),
            unevictable: map.remove("Unevictable"),
            mlocked: map.remove("Mlocked"),
            high_total: map.remove("HighTotal"),
            high_free: map.remove("HighFree"),
            low_total: map.remove("LowTotal"),
            low_free: map.remove("LowFree"),
            mmap_copy: map.remove("MmapCopy"),
            swap_total: expect!(map.remove("SwapTotal")),
            swap_free: expect!(map.remove("SwapFree")),
            dirty: expect!(map.remove("Dirty")),
            writeback: expect!(map.remove("Writeback")),
            anon_pages: map.remove("AnonPages"),
            mapped: expect!(map.remove("Mapped")),
            shmem: map.remove("Shmem"),
            slab: expect!(map.remove("Slab")),
            s_reclaimable: map.remove("SReclaimable"),
            s_unreclaim: map.remove("SUnreclaim"),
            kernel_stack: map.remove("KernelStack"),
            page_tables: map.remove("PageTables"),
            quicklists: map.remove("Quicklists"),
            nfs_unstable: map.remove("NFS_Unstable"),
            bounce: map.remove("Bounce"),
            writeback_tmp: map.remove("WritebackTmp"),
            commit_limit: map.remove("CommitLimit"),
            committed_as: expect!(map.remove("Committed_AS")),
            vmalloc_total: expect!(map.remove("VmallocTotal")),
            vmalloc_used: expect!(map.remove("VmallocUsed")),
            vmalloc_chunk: expect!(map.remove("VmallocChunk")),
            hardware_corrupted: map.remove("HardwareCorrupted"),
            anon_hugepages: map.remove("AnonHugePages"),
            shmem_hugepages: map.remove("ShmemHugePages"),
            shmem_pmd_mapped: map.remove("ShmemPmdMapped"),
            cma_total: map.remove("CmaTotal"),
            cma_free: map.remove("CmaFree"),
            hugepages_total: map.remove("HugePages_Total"),
            hugepages_free: map.remove("HugePages_Free"),
            hugepages_rsvd: map.remove("HugePages_Rsvd"),
            hugepages_surp: map.remove("HugePages_Surp"),
            hugepagesize: map.remove("Hugepagesize"),
            direct_map_4k: map.remove("DirectMap4k"),
            direct_map_4M: map.remove("DirectMap4M"),
            direct_map_2M: map.remove("DirectMap2M"),
            direct_map_1G: map.remove("DirectMap1G"),
            k_reclaimable: map.remove("KReclaimable"),
            per_cpu: map.remove("Percpu"),
            hugetlb: map.remove("Hugetlb"),
            file_pmd_mapped: map.remove("FilePmdMapped"),
            file_huge_pages: map.remove("FileHugePages"),
        };

        if cfg!(test) {
            assert!(map.is_empty(), "meminfo map is not empty: {:#?}", map);
        }

        Ok(meminfo)
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::{kernel_config, KernelVersion};

    #[allow(clippy::cognitive_complexity)]
    #[allow(clippy::blocks_in_if_conditions)]
    #[test]
    fn test_meminfo() {
        // TRAVIS
        // we don't have access to the kernel_config on travis, so skip that test there
        match ::std::env::var("TRAVIS") {
            Ok(ref s) if s == "true" => return,
            _ => {}
        }

        let kernel = KernelVersion::current().unwrap();
        let config = kernel_config().ok();

        let meminfo = Meminfo::new().unwrap();
        println!("{:#?}", meminfo);

        // for the fields that are only present in some kernel versions, make sure our
        // actual kernel agrees

        if kernel >= KernelVersion::new(3, 14, 0) {
            assert!(meminfo.mem_available.is_some());
        }

        if kernel >= KernelVersion::new(2, 6, 28) {
            assert!(meminfo.active_anon.is_some());
            assert!(meminfo.inactive_anon.is_some());
            assert!(meminfo.active_file.is_some());
            assert!(meminfo.inactive_file.is_some());
        } else {
            assert!(meminfo.active_anon.is_none());
            assert!(meminfo.inactive_anon.is_none());
            assert!(meminfo.active_file.is_none());
            assert!(meminfo.inactive_file.is_none());
        }

        if kernel >= KernelVersion::new(2, 6, 28)
            && kernel <= KernelVersion::new(2, 6, 30)
            && meminfo.unevictable.is_some()
        {
            if let Some(ref config) = config {
                assert!(config.get("CONFIG_UNEVICTABLE_LRU").is_some());
            }
        }

        if kernel >= KernelVersion::new(2, 6, 19)
            && config.as_ref().map_or(false, |cfg| cfg.contains_key("CONFIG_HIGHMEM"))
        {
            assert!(meminfo.high_total.is_some());
            assert!(meminfo.high_free.is_some());
            assert!(meminfo.low_total.is_some());
            assert!(meminfo.low_free.is_some());
        } else {
            assert!(meminfo.high_total.is_none());
            assert!(meminfo.high_free.is_none());
            assert!(meminfo.low_total.is_none());
            assert!(meminfo.low_free.is_none());
        }

        // Possible bug in procfs documentation:
        // The man page says that MmapCopy requires CONFIG_MMU, but if you look at the
        // source, MmapCopy is only included if CONFIG_MMU is *missing*:
        // https://github.com/torvalds/linux/blob/v4.17/fs/proc/meminfo.c#L80
        //if kernel >= KernelVersion::new(2, 6, 29) && config.contains_key("CONFIG_MMU") {
        //    assert!(meminfo.mmap_copy.is_some());
        //} else {
        //    assert!(meminfo.mmap_copy.is_none());
        //}

        if kernel >= KernelVersion::new(2, 6, 18) {
            assert!(meminfo.anon_pages.is_some());
            assert!(meminfo.page_tables.is_some());
            assert!(meminfo.nfs_unstable.is_some());
            assert!(meminfo.bounce.is_some());
        } else {
            assert!(meminfo.anon_pages.is_none());
            assert!(meminfo.page_tables.is_none());
            assert!(meminfo.nfs_unstable.is_none());
            assert!(meminfo.bounce.is_none());
        }

        if kernel >= KernelVersion::new(2, 6, 32) {
            assert!(meminfo.shmem.is_some());
            assert!(meminfo.kernel_stack.is_some());
        } else {
            assert!(meminfo.shmem.is_none());
            assert!(meminfo.kernel_stack.is_none());
        }

        if kernel >= KernelVersion::new(2, 6, 19) {
            assert!(meminfo.s_reclaimable.is_some());
            assert!(meminfo.s_unreclaim.is_some());
        } else {
            assert!(meminfo.s_reclaimable.is_none());
            assert!(meminfo.s_unreclaim.is_none());
        }

        if kernel >= KernelVersion::new(2, 6, 27)
            && config
                .as_ref()
                .map_or(false, |cfg| cfg.contains_key("CONFIG_QUICKLIST"))
        {
            assert!(meminfo.quicklists.is_some());
        } else {
            assert!(meminfo.quicklists.is_none());
        }

        if kernel >= KernelVersion::new(2, 6, 26) {
            assert!(meminfo.writeback_tmp.is_some());
        } else {
            assert!(meminfo.writeback_tmp.is_none());
        }

        if kernel >= KernelVersion::new(2, 6, 10) {
            assert!(meminfo.commit_limit.is_some());
        } else {
            assert!(meminfo.commit_limit.is_none());
        }

        if kernel >= KernelVersion::new(2, 6, 32)
            && config
                .as_ref()
                .map_or(std::path::Path::new("/proc/kpagecgroup").exists(), |cfg| {
                    cfg.contains_key("CONFIG_MEMORY_FAILURE")
                })
        {
            assert!(meminfo.hardware_corrupted.is_some());
        } else {
            assert!(meminfo.hardware_corrupted.is_none());
        }

        if kernel >= KernelVersion::new(2, 6, 38)
            && config
                .as_ref()
                .map_or(false, |cfg| cfg.contains_key("CONFIG_TRANSPARENT_HUGEPAGE"))
        {
            assert!(meminfo.anon_hugepages.is_some());
        } else {
            // SOme distributions may backport this option into older kernels
            // assert!(meminfo.anon_hugepages.is_none());
        }

        if kernel >= KernelVersion::new(4, 8, 0)
            && config
                .as_ref()
                .map_or(true, |cfg| cfg.contains_key("CONFIG_TRANSPARENT_HUGEPAGE"))
        {
            assert!(meminfo.shmem_hugepages.is_some());
            assert!(meminfo.shmem_pmd_mapped.is_some());
        } else {
            assert!(meminfo.shmem_hugepages.is_none());
            assert!(meminfo.shmem_pmd_mapped.is_none());
        }

        if kernel >= KernelVersion::new(3, 1, 0) && config.as_ref().map_or(true, |cfg| cfg.contains_key("CONFIG_CMA")) {
            assert!(meminfo.cma_total.is_some());
            assert!(meminfo.cma_free.is_some());
        } else {
            assert!(meminfo.cma_total.is_none());
            assert!(meminfo.cma_free.is_none());
        }

        if config
            .as_ref()
            .map_or(true, |cfg| cfg.contains_key("CONFIG_HUGETLB_PAGE"))
        {
            assert!(meminfo.hugepages_total.is_some());
            assert!(meminfo.hugepages_free.is_some());
            assert!(meminfo.hugepagesize.is_some());
        } else {
            assert!(meminfo.hugepages_total.is_none());
            assert!(meminfo.hugepages_free.is_none());
            assert!(meminfo.hugepagesize.is_none());
        }

        if kernel >= KernelVersion::new(2, 6, 17)
            && config
                .as_ref()
                .map_or(true, |cfg| cfg.contains_key("CONFIG_HUGETLB_PAGE"))
        {
            assert!(meminfo.hugepages_rsvd.is_some());
        } else {
            assert!(meminfo.hugepages_rsvd.is_none());
        }

        if kernel >= KernelVersion::new(2, 6, 24)
            && config
                .as_ref()
                .map_or(true, |cfg| cfg.contains_key("CONFIG_HUGETLB_PAGE"))
        {
            assert!(meminfo.hugepages_surp.is_some());
        } else {
            assert!(meminfo.hugepages_surp.is_none());
        }
    }
}
