use super::*;

use crate::fs::FileMode;
use crate::process::{do_getegid, do_geteuid, gid_t, uid_t, ThreadRef};
use crate::time::{do_gettimeofday, time_t};
use crate::vm::{
    ChunkRef, MunmapChunkFlag, VMInitializer, VMMapOptionsBuilder, VMPerms, VMRange,
    USER_SPACE_VM_MANAGER,
};
use std::collections::{HashMap, HashSet};

#[allow(non_camel_case_types)]
pub type key_t = u32;
pub type ShmId = u32;
pub type CmdId = u32;

// min shared seg size (bytes)
const SHMMIN: usize = 1;
// max shared seg size (bytes)
const SHMMAX: usize = (usize::MAX - (1_usize << 24));
// max num of segs system wide,
// also indicates the max shmid - 1 in Occlum
const SHMMNI: ShmId = 4096;

const IPC_PRIVATE: key_t = 0;

// For cmd in shmctl()
const IPC_RMID: CmdId = 0;
const IPC_SET: CmdId = 1;
const IPC_STAT: CmdId = 2;
const IPC_INFO: CmdId = 3;
const SHM_LOCK: CmdId = 11;
const SHM_UNLOCK: CmdId = 12;
const SHM_STAT: CmdId = 13;
const SHM_INFO: CmdId = 14;
const SHM_STAT_ANY: CmdId = 15;

#[allow(non_camel_case_types)]
#[derive(Debug)]
#[repr(C)]
struct ipc_perm_t {
    key: key_t,
    uid: uid_t,
    gid: gid_t,
    cuid: uid_t,
    cgid: gid_t,
    mode: u16,
    pad1: u16,
    seq: u16,
    pad2: u16,
    unused1: u64,
    unused2: u64,
}

#[allow(non_camel_case_types)]
#[derive(Debug)]
#[repr(C)]
pub struct shmids_t {
    shm_perm: ipc_perm_t,
    shm_segsz: size_t,
    shm_atime: time_t,
    shm_dtime: time_t,
    shm_ctime: time_t,
    shm_cpid: pid_t,
    shm_lpid: pid_t,
    shm_nattach: u64,
    unused1: u64,
    unused2: u64,
}

// shared memory segment status
bitflags! {
    struct ShmStatus : u16 {
        // segment will be destroyed on last detach
        const SHM_DEST = 0o1000;
        // segment will not be swapped, unused now
        const SHM_LOCKED = 0o2000;
    }
}

bitflags! {
    pub struct ShmFlags: u32 {
        const IPC_CREAT = 0o1000;
        const IPC_EXCL = 0o2000;

        /// read by owner
        const S_IRUSR = FileMode::S_IRUSR.bits() as u32;
        /// write by owner
        const S_IWUSR = FileMode::S_IWUSR.bits() as u32;
        /// execute/search by owner
        const S_IXUSR = FileMode::S_IXUSR.bits() as u32;
        /// read by group
        const S_IRGRP = FileMode::S_IRGRP.bits() as u32;
        /// write by group
        const S_IWGRP = FileMode::S_IWGRP.bits() as u32;
        /// execute/search by group
        const S_IXGRP = FileMode::S_IXGRP.bits() as u32;
        /// read by others
        const S_IROTH = FileMode::S_IROTH.bits() as u32;
        /// write by others
        const S_IWOTH = FileMode::S_IWOTH.bits() as u32;
        /// execute/search by others
        const S_IXOTH = FileMode::S_IXOTH.bits() as u32;
    }
}

impl ShmFlags {
    fn to_file_mode(&self) -> FileMode {
        let mut shmflgs = *self;
        shmflgs.remove(ShmFlags::IPC_CREAT);
        shmflgs.remove(ShmFlags::IPC_EXCL);
        let file_mode = FileMode::from_bits(shmflgs.bits as u16 & FileMode::all().bits()).unwrap();
        file_mode
    }
}

#[derive(Debug)]
struct ShmSegment {
    shmid: ShmId,
    key: key_t,

    uid: uid_t,
    gid: gid_t,
    cuid: uid_t,
    cgid: gid_t,
    mode: FileMode,
    status: ShmStatus,

    shm_atime: time_t,
    shm_dtime: time_t,
    shm_ctime: time_t,

    shm_cpid: pid_t,
    shm_lpid: pid_t,

    shm_nattach: u64,
    chunk: ChunkRef,
    process_set: HashSet<pid_t>,
}

impl ShmSegment {
    fn new(shmid: ShmId, key: key_t, size: size_t, mode: FileMode) -> Result<Self> {
        let vm_option = VMMapOptionsBuilder::default()
            .size(size)
            // Currently, Occlum only support shared memory segment with rw permission
            .perms(VMPerms::READ | VMPerms::WRITE)
            .build()?;
        let chunk = USER_SPACE_VM_MANAGER.internal().mmap_chunk(&vm_option)?;

        Ok(ShmSegment {
            shmid: shmid,
            key: key,
            uid: do_geteuid().unwrap() as u32,
            cuid: do_geteuid().unwrap() as u32,
            gid: do_getegid().unwrap() as u32,
            cgid: do_getegid().unwrap() as u32,
            mode: mode,
            status: ShmStatus::empty(),
            shm_atime: 0,
            shm_dtime: 0,
            shm_ctime: ShmManager::current_time(),
            shm_cpid: current!().process().pid(),
            shm_lpid: 0,
            shm_nattach: 0,
            chunk: chunk,
            process_set: HashSet::new(),
        })
    }

    fn check_perm(&self) -> Result<()> {
        // TODO: Add permission control
        Ok(())
    }

    fn set_destruction(&mut self) {
        self.status.insert(ShmStatus::SHM_DEST)
    }

    fn is_destruction(&self) -> bool {
        self.status.contains(ShmStatus::SHM_DEST)
    }

    fn shm_start(&self) -> usize {
        self.chunk.range().start()
    }

    fn shm_size(&self) -> usize {
        self.chunk.range().size()
    }

    fn shm_add_pid(&mut self, pid: &pid_t) -> Result<()> {
        if self.process_set.contains(pid) {
            return_errno!(EINVAL, "this pid has been attached to the shm");
        }
        self.process_set.insert(*pid);
        Ok(())
    }

    fn shm_remove_pid(&mut self, pid: &pid_t) -> Result<()> {
        if !self.process_set.contains(pid) {
            return_errno!(EINVAL, " this pid has not been attached to the shm");
        }
        self.process_set.remove(&pid);
        Ok(())
    }

    fn chunk(&self) -> &ChunkRef {
        &self.chunk
    }

    fn process_set(&self) -> &HashSet<pid_t> {
        &self.process_set
    }
}

impl Drop for ShmSegment {
    fn drop(&mut self) {
        debug!("drop shm: {:?}", self);
        assert!(self.shm_nattach == 0);
        assert!(self.process_set.len() == 0);
        USER_SPACE_VM_MANAGER
            .internal()
            .munmap_chunk(&self.chunk, None, MunmapChunkFlag::Default);
    }
}

#[derive(Debug)]
struct ShmIdManager {
    used_id: HashSet<ShmId>,
    free_num: u32,
    last_alloc_id: ShmId,
}

impl ShmIdManager {
    fn new() -> Self {
        let used_id = HashSet::new();
        let free_num = SHMMNI as u32;
        let last_alloc_id = SHMMNI - 1;
        ShmIdManager {
            used_id,
            free_num,
            last_alloc_id,
        }
    }

    // Always return next free id for shmid
    fn get_new_shmid(&mut self) -> Result<ShmId> {
        if self.free_num == 0 {
            return_errno!(ENOSPC, "all possible shared memory IDs have been taken");
        } else {
            self.free_num -= 1;
        }
        let mut id = self.last_alloc_id + 1;
        loop {
            if id == SHMMNI {
                id = 0;
            }
            if !self.used_id.contains(&id) {
                break;
            }
            id += 1;
        }
        self.last_alloc_id = id;
        Ok(id)
    }

    fn free_shmid(&mut self, shmid: &ShmId) -> Result<()> {
        self.free_num += 1;
        self.used_id.remove(shmid);
        Ok(())
    }
}

lazy_static! {
    pub static ref SYSTEM_V_SHM_MANAGER: ShmManager = ShmManager::new();
}

#[derive(Debug)]
pub struct ShmManager {
    shm_segments: RwLock<HashMap<ShmId, ShmSegment>>,
    shmid_manager: RwLock<ShmIdManager>,
}

impl ShmManager {
    fn new() -> Self {
        ShmManager {
            shm_segments: RwLock::new(HashMap::new()),
            shmid_manager: RwLock::new(ShmIdManager::new()),
        }
    }

    pub fn get_shm_chunk_containing_addr(&self, addr: usize, pid: pid_t) -> Option<ChunkRef> {
        let shm_segments = self.shm_segments.read().unwrap();
        shm_segments
            .iter()
            .find(|(_, shm_segment)| {
                shm_segment.chunk().range().contains(addr) && shm_segment.process_set.contains(&pid)
            })
            .map(|(_, shm_segment)| shm_segment.chunk().clone())
    }

    fn current_time() -> time_t {
        do_gettimeofday().sec()
    }

    fn get_new_shmid(&self) -> Result<ShmId> {
        let mut shmid_manager = self.shmid_manager.write().unwrap();
        shmid_manager.get_new_shmid()
    }

    fn free_shmid(&self, shmid: &ShmId) -> Result<()> {
        let mut shmid_manager = self.shmid_manager.write().unwrap();
        shmid_manager.free_shmid(&shmid)
    }

    fn shmctl_rmshm(&self, shmid: ShmId) -> Result<()> {
        let mut shm_segments = self.shm_segments.write().unwrap();
        let shm = shm_segments.get_mut(&shmid);

        if let Some(shm) = shm {
            shm.shm_ctime = ShmManager::current_time();
            shm.set_destruction();
            if shm.shm_nattach == 0 {
                let shmid = shm.shmid;
                self.free_shmid(&shmid)?;
                shm_segments.remove(&shmid);
            }
        } else {
            return_errno!(EINVAL, "cannot find shm by shmid");
        }
        Ok(())
    }

    fn shmctl_ipcstat(&self, shmid: ShmId, buf: Option<&mut shmids_t>) -> Result<()> {
        let shm_segments = self.shm_segments.read().unwrap();
        let shm = shm_segments.get(&shmid);
        if let Some(shm) = shm {
            shm.check_perm()?;
            let mut buf = match buf {
                Some(buf) => buf,
                None => return_errno!(EFAULT, "buf is empty"),
            };
            let shm_perm = ipc_perm_t {
                key: shm.key,
                uid: shm.uid,
                gid: shm.uid,
                cuid: shm.cuid,
                cgid: shm.cgid,
                mode: shm.mode.bits() | shm.status.bits(),
                pad1: 0,
                seq: 0,
                pad2: 0,
                unused1: 0,
                unused2: 0,
            };
            let shmids = shmids_t {
                shm_perm: shm_perm,
                shm_segsz: shm.shm_size(),
                shm_atime: shm.shm_atime,
                shm_dtime: shm.shm_dtime,
                shm_ctime: shm.shm_ctime,
                shm_cpid: shm.shm_cpid,
                shm_lpid: shm.shm_lpid,
                shm_nattach: shm.shm_nattach,
                unused1: 0,
                unused2: 0,
            };
            *buf = shmids;
        } else {
            return_errno!(EINVAL, "cannot find shm by shmid");
        }
        Ok(())
    }

    pub fn do_shmget(&self, key: key_t, size: usize, shmflg: ShmFlags) -> Result<ShmId> {
        debug!(
            "do_shmget: key: {:?}, size: {:?}, shmflg: {:?}",
            key, size, shmflg
        );

        // Check the size from user for shm creation
        if shmflg.contains(ShmFlags::IPC_CREAT) && (size < SHMMIN || size > SHMMAX) {
            return_errno!(EINVAL, "invalid size");
        }

        let mut mode = shmflg.to_file_mode();
        // The creator and user must have the read and write permission to the segment
        let read_per = mode.contains(FileMode::S_IRUSR)
            || mode.contains(FileMode::S_IRGRP)
            || mode.contains(FileMode::S_IROTH);
        let write_per = mode.contains(FileMode::S_IWUSR)
            || mode.contains(FileMode::S_IWGRP)
            || mode.contains(FileMode::S_IWOTH);
        if !(read_per && write_per) {
            warn!("shared memory segement in occlum should have rw permission");
        }

        let mut shm_segments = self.shm_segments.write().unwrap();
        let shmid = if key == IPC_PRIVATE {
            let shmid = self.get_new_shmid()?;
            let shm = ShmSegment::new(shmid, key, size, mode)?;
            shm_segments.insert(shm.shmid, shm);
            shmid
        } else {
            // Get the shm from key if the segment is not marked to be destroyed
            let shm = shm_segments
                .values()
                .find(|&shm| !shm.is_destruction() && shm.key == key);
            let shmid = if let Some(shm) = shm {
                if shmflg.contains(ShmFlags::IPC_CREAT) && shmflg.contains(ShmFlags::IPC_EXCL) {
                    return_errno!(
                        EEXIST,
                        "the shared memory segment already exists for given key"
                    );
                }
                // The size from user must be less than the actual size
                if size > shm.shm_size() {
                    return_errno!(EINVAL, "the size from user is too large");
                }
                // Check the permission
                shm.check_perm()?;
                shm.shmid
            } else {
                if !shmflg.contains(ShmFlags::IPC_CREAT) {
                    return_errno!(ENOENT, "no segment exists for given key");
                }
                let shmid = self.get_new_shmid()?;
                let shm = ShmSegment::new(shmid, key, size, mode)?;
                shm_segments.insert(shm.shmid, shm);
                shmid
            };
            shmid
        };
        Ok(shmid)
    }

    pub fn do_shmat(&self, shmid: ShmId, addr: usize, shmflg: ShmFlags) -> Result<usize> {
        debug!(
            "do_shmat: shmid: {:?}, addr: {:?}, shmflg: {:?}",
            shmid, addr, shmflg
        );
        let pid = current!().process().pid();
        let mut shm_segments = self.shm_segments.write().unwrap();
        let shm = shm_segments.get_mut(&shmid);
        let addr = if let Some(shm) = shm {
            if addr != 0 && addr != shm.shm_start() {
                return_errno!(EINVAL, "invalid addr");
            }
            // Check the permission
            shm.check_perm()?;

            shm.shm_nattach += 1;
            shm.shm_atime = ShmManager::current_time();
            shm.shm_add_pid(&pid)?;
            shm.shm_lpid = pid;
            shm.shm_start()
        } else {
            return_errno!(EINVAL, "cannot find shm by shmid");
        };
        Ok(addr)
    }

    pub fn do_shmdt(&self, addr: usize) -> Result<()> {
        debug!("do_shmdt: addr: {:?}", addr);
        let pid = current!().process().pid();
        let mut shm_segments = self.shm_segments.write().unwrap();
        let shm = shm_segments
            .values_mut()
            .find(|shm| shm.shm_start() == addr);

        if let Some(shm) = shm {
            shm.shm_dtime = ShmManager::current_time();
            shm.shm_lpid = pid;
            shm.shm_remove_pid(&pid)?;
            shm.shm_nattach -= 1;
            if shm.is_destruction() && shm.shm_nattach == 0 {
                let shmid = shm.shmid;
                self.free_shmid(&shmid);
                shm_segments.remove(&shmid);
            }
        } else {
            return_errno!(EINVAL, "cannot find shm by addr");
        }
        Ok(())
    }

    pub fn do_shmctl(&self, shmid: ShmId, cmd: CmdId, buf: Option<&mut shmids_t>) -> Result<()> {
        debug!(
            "do_shmctl: shmid: {:?}, cmd: {:?}, buf: {:?}",
            shmid, cmd, buf
        );
        match cmd {
            IPC_RMID => self.shmctl_rmshm(shmid),
            IPC_STAT => self.shmctl_ipcstat(shmid, buf),
            _ => return_errno!(EINVAL, "unimplemented cmd"),
        }
    }

    pub fn detach_shm_when_process_exit(&self, thread: &ThreadRef) {
        let pid = &thread.process().pid();
        let mut shm_segments = self.shm_segments.write().unwrap();
        shm_segments.drain_filter(|shmid, shm| match shm.shm_remove_pid(&pid) {
            // There exists a shm that has been attached to the current process
            Ok(_) => {
                shm.shm_nattach -= 1;
                if shm.is_destruction() && shm.shm_nattach == 0 {
                    self.free_shmid(&shmid);
                    true
                } else {
                    false
                }
            }
            Err(_) => false,
        });
    }

    pub fn clean_when_libos_exit(&self) {
        let mut shm_segments = self.shm_segments.write().unwrap();
        for (_, shm) in shm_segments.drain() {
            debug!("clean shm: {:?}", shm);
            self.free_shmid(&shm.shmid);
        }
    }
}
