//! This file supports specifying and applying redirections.

use crate::io::IoChain;
use crate::prelude::*;
use crate::wutil::fish_wcstoi;
use nix::fcntl::OFlag;
use std::os::fd::RawFd;

#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum RedirectionMode {
    Overwrite, // normal redirection: > file.txt
    Append,    // appending redirection: >> file.txt
    Input,     // input redirection: < file.txt
    TryInput,  // try-input redirection: <? file.txt
    Fd,        // fd redirection: 2>&1
    NoClob,    // noclobber redirection: >? file.txt
}

/// A type that represents the action dup2(src, target).
/// If target is negative, this represents close(src).
/// Note none of the fds here are considered 'owned'.
#[derive(Clone, Copy)]
pub struct Dup2Action {
    pub src: i32,
    pub target: i32,
}

/// A class representing a sequence of basic redirections.
#[derive(Default)]
pub struct Dup2List {
    /// The list of actions.
    pub actions: Vec<Dup2Action>,
}

impl RedirectionMode {
    /// The open flags for this redirection mode.
    pub fn oflags(self) -> Option<OFlag> {
        match self {
            RedirectionMode::Append => Some(OFlag::O_CREAT | OFlag::O_APPEND | OFlag::O_WRONLY),
            RedirectionMode::Overwrite => Some(OFlag::O_CREAT | OFlag::O_WRONLY | OFlag::O_TRUNC),
            RedirectionMode::NoClob => Some(OFlag::O_CREAT | OFlag::O_EXCL | OFlag::O_WRONLY),
            RedirectionMode::Input | RedirectionMode::TryInput => Some(OFlag::O_RDONLY),
            _ => None,
        }
    }
}

/// A struct which represents a redirection specification from the user.
/// Here the file descriptors don't represent open files - it's purely textual.
#[derive(Clone)]
pub struct RedirectionSpec {
    /// The redirected fd, or -1 on overflow.
    /// In the common case of a pipe, this is 1 (STDOUT_FILENO).
    /// For example, in the case of "3>&1" this will be 3.
    pub fd: RawFd,

    /// The redirection mode.
    pub mode: RedirectionMode,

    /// The target of the redirection.
    /// For example in "3>&1", this will be "1".
    /// In "< file.txt" this will be "file.txt".
    pub target: WString,
}

impl RedirectionSpec {
    pub fn new(fd: RawFd, mode: RedirectionMode, target: WString) -> Self {
        Self { fd, mode, target }
    }
    /// Return if this is a close-type redirection.
    pub fn is_close(&self) -> bool {
        self.mode == RedirectionMode::Fd && self.target == "-"
    }

    /// Attempt to parse target as an fd.
    pub fn get_target_as_fd(&self) -> Option<RawFd> {
        fish_wcstoi(&self.target).ok()
    }

    /// Return the open flags for this redirection.
    pub fn oflags(&self) -> OFlag {
        match self.mode.oflags() {
            Some(flags) => flags,
            None => panic!("Not a file redirection"),
        }
    }
}

pub type RedirectionSpecList = Vec<RedirectionSpec>;

/// Produce a dup_fd_list_t from an io_chain. This may not be called before fork().
/// The result contains the list of fd actions (dup2 and close), as well as the list
/// of fds opened.
pub fn dup2_list_resolve_chain(io_chain: &IoChain) -> Dup2List {
    let mut result = Dup2List { actions: vec![] };
    for io in &io_chain.0 {
        if io.source_fd() < 0 {
            result.add_close(io.fd());
        } else {
            result.add_dup2(io.source_fd(), io.fd());
        }
    }
    result
}

impl Dup2List {
    pub fn new() -> Self {
        Default::default()
    }
    /// Return the list of dup2 actions.
    pub fn get_actions(&self) -> &[Dup2Action] {
        &self.actions
    }

    /// Return the fd ultimately dup'd to a target fd, or -1 if the target is closed.
    /// For example, if target fd is 1, and we have a dup2 chain 5->3 and 3->1, then we will
    /// return 5. If the target is not referenced in the chain, returns target.
    pub fn fd_for_target_fd(&self, target: RawFd) -> RawFd {
        // Paranoia.
        if target < 0 {
            return target;
        }
        // Note we can simply walk our action list backwards, looking for src -> target dups.
        let mut cursor = target;
        for action in self.actions.iter().rev() {
            if action.target == cursor {
                // cursor is replaced by action.src
                cursor = action.src;
            } else if action.src == cursor && action.target < 0 {
                // cursor is closed.
                cursor = -1;
                break;
            }
        }
        cursor
    }

    /// Append a dup2 action.
    pub fn add_dup2(&mut self, src: RawFd, target: RawFd) {
        assert!(src >= 0 && target >= 0, "Invalid fd in add_dup2");
        // Note: record these even if src and target is the same.
        // This is a note that we must clear the CLOEXEC bit.
        self.actions.push(Dup2Action { src, target });
    }

    /// Append a close action.
    pub fn add_close(&mut self, fd: RawFd) {
        assert!(fd >= 0, "Invalid fd in add_close");
        self.actions.push(Dup2Action {
            src: fd,
            target: -1,
        });
    }
}

#[cfg(test)]
mod tests {
    use crate::io::{IoChain, IoClose, IoFd};
    use crate::redirection::dup2_list_resolve_chain;
    use std::sync::Arc;

    #[test]
    fn test_dup2s() {
        let mut chain = IoChain::new();
        chain.push(Arc::new(IoClose::new(17)));
        chain.push(Arc::new(IoFd::new(3, 19)));
        let list = dup2_list_resolve_chain(&chain);
        assert_eq!(list.get_actions().len(), 2);

        let act1 = list.get_actions()[0];
        assert_eq!(act1.src, 17);
        assert_eq!(act1.target, -1);

        let act2 = list.get_actions()[1];
        assert_eq!(act2.src, 19);
        assert_eq!(act2.target, 3);
    }

    #[test]
    fn test_dup2s_fd_for_target_fd() {
        let mut chain = IoChain::new();
        // note io_fd_t params are backwards from dup2.
        chain.push(Arc::new(IoClose::new(10)));
        chain.push(Arc::new(IoFd::new(9, 10)));
        chain.push(Arc::new(IoFd::new(5, 8)));
        chain.push(Arc::new(IoFd::new(1, 4)));
        chain.push(Arc::new(IoFd::new(3, 5)));
        let list = dup2_list_resolve_chain(&chain);

        assert_eq!(list.fd_for_target_fd(3), 8);
        assert_eq!(list.fd_for_target_fd(5), 8);
        assert_eq!(list.fd_for_target_fd(8), 8);
        assert_eq!(list.fd_for_target_fd(1), 4);
        assert_eq!(list.fd_for_target_fd(4), 4);
        assert_eq!(list.fd_for_target_fd(100), 100);
        assert_eq!(list.fd_for_target_fd(0), 0);
        assert_eq!(list.fd_for_target_fd(-1), -1);
        assert_eq!(list.fd_for_target_fd(9), -1);
        assert_eq!(list.fd_for_target_fd(10), -1);
    }
}
