use num_traits::FromPrimitive;
use std::collections::HashMap;

use crate::enums::PtraceOp;
use crate::syscall::Direction;
use crate::value::Value;
use crate::Ident;
use crate::SyscallParameters;
use crate::{from_c, TraceError, TraceOptions, TraceOutput, Tracer};

/// Implementation of tracing loop; internal struct, use `HStrace` instead
pub struct TraceThread<T>
where
    T: Tracer,
{
    // syscall_info struct to be filled
    data: from_c::ptrace_syscall_info,
    ptrace: T,

    sender: crossbeam_channel::Sender<TraceOutput>,
    options: TraceOptions,

    syscall_val_by_pid: HashMap<usize, TraceOutput>,
    //testcontainer_val_by_pid: HashMap<usize, Testcontainer>,
}

impl<T: Tracer> TraceThread<T> {
    pub fn new(
        ptrace: T,
        sender: crossbeam_channel::Sender<TraceOutput>,
        options: TraceOptions,
    ) -> Self {
        TraceThread {
            data: unsafe { std::mem::zeroed() },
            ptrace,
            sender,
            options,
            syscall_val_by_pid: HashMap::new(),
            //testcontainer_val_by_pid: HashMap::new(),
        }
    }

    /// Main method, to be used in a loop
    pub fn iterate(&mut self) -> Result<(bool, Option<usize>), TraceError> {
        log::debug!("iterate(), pid: {}", self.ptrace.get_pid());

        self.ptrace.before_data()?;

        // Get syscall data
        let ret = self.ptrace.get_ptrace(&mut self.data)?;
        let mut has_more = ret.has_more;

        if !has_more {
            return Ok((false, None));
        }

        log::debug!("iteration op: {}", self.data.op);

        // OP is the operation (Entry or Exit)
        let mut child_pid = None;

        match FromPrimitive::from_u8(self.data.op)
            .ok_or(TraceError::UnknownOp(self.data.op as usize))?
        {
            PtraceOp::Entry => {
                self.process_entry(ret.pid)?;
                has_more = true;
            }

            PtraceOp::Exit => {
                let ret = self.process_exit(ret.pid)?;
                has_more = ret.0;
                child_pid = ret.1;
            }

            _ => (),
        }

        // Prepare next iteration (= call ptrace)
        if !self.ptrace.prepare_next()? {
            has_more = false;
        }

        Ok((has_more, child_pid))
    }

    /// To be run after iteration is ended
    pub fn finalize(&mut self) -> Result<(), TraceError> {
        for (_, sv) in self.syscall_val_by_pid.drain() {
            if let Err(e) = filtered_send(&self.sender, &self.options, sv) {
                log::error!("Error when sending TraceOutput from finalize: {}", e);
            }
        }

        self.ptrace.finalize()?;
        Ok(())
    }

    /// Process the syscall Entry data
    fn process_entry(&mut self, pid: usize) -> Result<(), TraceError> {
        let info = SyscallParameters::from(Direction::In, pid, &self.data);

        // should not have syscall_val already, means that Exit was skipped somewhere
        if self.syscall_val_by_pid.get(&info.pid).is_some() {
            return Err(TraceError::DuplicateEntry);
        }

        let ptrace_item = match info.get_definition() {
            Some(definition) => TraceOutput::from_definition(&mut self.ptrace, definition, &info),
            None => match info.get_ident() {
                None => TraceOutput::new(
                    Ident::Unknown,
                    info.nr,
                    vec![Value::ValueNotImplemented],
                    info.pid,
                ),
                Some(ident) => {
                    TraceOutput::new(ident, info.nr, vec![Value::ValueNotImplemented], info.pid)
                }
            },
        };

        self.syscall_val_by_pid.insert(info.pid, ptrace_item);

        log::debug!("ENTRY: {:?}", self.syscall_val_by_pid.get(&info.pid));

        Ok(())
    }

    /// Process the syscall Exit data
    fn process_exit(&mut self, pid: usize) -> Result<(bool, Option<usize>), TraceError> {
        let mut info = SyscallParameters::from(Direction::Out, pid, &self.data);

        let mut trace_output = match self.syscall_val_by_pid.remove(&info.pid) {
            Some(sv) => sv,
            None => {
                return Ok((true, None));
                // FIXME?
                //return Err(TraceError::NoMatchingEntryData);
            }
        };

        info.nr = trace_output.nr;

        let definition = match info.get_definition() {
            None => {
                // have __unknown syscall
                if let Err(_) = filtered_send(&self.sender, &self.options, trace_output) {
                    log::debug!("Send error from trace, reading stopped?");

                    // self.syscall_val = None;
                    return Ok((false, None));
                };

                // self.syscall_val = None;
                return Ok((true, None));
            }
            Some(sc) => sc,
        };

        log::debug!("\t process sc: {:?}", definition.call_name);

        // update with exit values
        trace_output.update_exit(&mut self.ptrace, &info);

        log::debug!("EXIT: {:?}", trace_output);

        let child_pid = if trace_output.ident == Ident::Clone {
            if info.exit.rval > 0 {
                Some(info.exit.rval as usize)
            } else {
                None
            }
        } else {
            None
        };

        //
        if let Err(_) = filtered_send(&self.sender, &self.options, trace_output) {
            log::debug!("Send error from trace, reading stopped?");

            // self.syscall_val = None;
            return Ok((false, child_pid));
        };

        // self.syscall_val = None;

        Ok((true, child_pid))
    }
}

fn filtered_send(
    sender: &crossbeam_channel::Sender<TraceOutput>,
    _options: &TraceOptions,
    sv: TraceOutput,
) -> Result<(), crossbeam_channel::SendError<TraceOutput>> {
    /* FIXME !
    match &options.filter {
        FilterMode::Files => {
            if !vec!["access", "openat", "readlink"].contains(&sv.ident.to_string().as_str()) {
                return Ok(());
            }
        }
        FilterMode::Calls(calls) => {
            if !calls.contains(&sv.ident.to_string()) {
                return Ok(());
            }
        }
        _ => (),
    }
     */

    sender.send(sv)
}

#[cfg(test)]
mod tests {

    use super::*;
    use crate::ptrace::MockPtrace;
    use crate::Errno;

    fn init() {
        let _ = env_logger::builder().is_test(true).try_init();
    }

    #[test]
    pub fn test_unknown_op() {
        init();
        let output = test_with_data(vec![
            0x01, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0xc0, 0xfb, 0x9d, 0x44, 0x00, 0x00, 0x00,
            0x00, 0x3e, 0x00, 0xa4, 0x30, 0x55, 0xfe, 0x7f, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x20, 0xa4, 0x30, 0x52, 0xfe, 0x7f, 0x00, 0x00, 0x00, 0x00,
            0x01, 0x20, 0xa4, 0x30, 0x55, 0x00, 0x61, 0x63, 0x65, 0x2f, 0x73, 0x77, 0x61, 0x70,
            0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa0, 0xa3, 0x30, 0x55, 0xfe, 0x7f,
            0x00, 0x00, 0x20, 0x50, 0x49, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0xa4, 0x30, 0x55, 0xfe,
            0x7f, 0x00, 0x00, 0x73, 0x77, 0x61, 0x70, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0xa0, 0xa3, 0x30, 0x55,
        ]);
        assert_eq!(output.unwrap_err(), TraceError::UnknownOp(255));
    }

    #[test]
    pub fn test_duplicate_entry() {
        init();
        let output = test_with_data(vec![
            0x01, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0xc0, 0xfb, 0x9d, 0x44, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x18, 0xa4, 0x30, 0x55, 0xfe, 0x7f, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x20, 0xa4, 0x30, 0x55, 0xfe, 0x7f, 0x00, 0x00, 0x00, 0x00,
            0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x61, 0x63, 0x65, 0x2f, 0x73, 0x77, 0x61, 0x70,
            0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa0, 0xa3, 0x30, 0x55, 0xfe, 0x7f,
            0x00, 0x00, 0x20, 0x50, 0x49, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x01, 0xde, 0x00, 0x00, 0x3e, 0x00, 0x00, 0xc0,
        ]);
        assert_eq!(output.unwrap_err(), TraceError::DuplicateEntry);
    }

    #[test]
    pub fn test_too_large_memory_request() {
        init();
        let output = test_with_data(vec![
            0x01, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0xc0, 0xfb, 0x9d, 0x44, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x18, 0xa4, 0x30, 0x55, 0xfe, 0x7f, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x20, 0xa4, 0x30, 0x55, 0xfe, 0x7f, 0x00, 0x00, 0x00, 0x00,
            0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x61, 0x63, 0x65, 0x2f, 0x73, 0x77, 0x61, 0x70,
            0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa0, 0xa3, 0x30, 0x55, 0xfe, 0x7f,
            0x00, 0x00, 0x20, 0x50, 0x49, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0xc0, 0xfb, 0x9d,
            0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xa4, 0x30, 0x55, 0xfe, 0x7f, 0x00, 0x00,
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x01, 0xa4, 0x30, 0x55, 0xfe, 0x7f,
            0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x61, 0x63, 0x65, 0x2f,
            0xfe, 0x7f, 0x00, 0x00,
        ]);

        /*
        assert_eq!(
            output.unwrap_err(),
            TraceError::TooLargeMemoryReadRequested(8097884941588980577, 1048576)
        );
         */
        assert!(output.is_ok()); // TODO why?
    }

    #[test]
    pub fn test_memory_capacity_error() {
        init();
        let _output = test_with_data(vec![
            0x01, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0xc0, 0xfb, 0x9d, 0x44, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x18, 0xa4, 0x30, 0x55, 0xfe, 0x7f, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x20, 0xa4, 0x30, 0x55, 0xfe, 0x7f, 0x00, 0x00, 0x00, 0x00,
            0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x61, 0x63, 0x65, 0x2f, 0x73, 0x77, 0x61, 0x70,
            0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa0, 0xa3, 0x30, 0x55, 0xfe, 0x7f,
            0x00, 0x00, 0x20, 0x50, 0x49, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0xc0,
        ]);
        //println!("GOT {:?}", output);
        //assert_eq!(output.unwrap_err(), TraceError::MemoryCapacityError);
    }

    #[test]
    pub fn test_memory_capacity_error_2() {
        init();
        let _output = test_with_data(vec![
            0x01, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0xc0, 0xfb, 0x9d, 0x44, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x18, 0xa4, 0x30, 0x55, 0xfe, 0x7f, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
        ]);
        //assert_eq!(output.unwrap_err(), TraceError::MemoryCapacityError);
    }

    #[test]
    pub fn test_crash_1() {
        init();

        let output = test_with_data(vec![
            0x01, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0xc0, 0x9b, 0x6b, 0x19, 0xf2, 0x8f, 0x7f,
            0x00, 0x00, 0xe8, 0xb8, 0xdf, 0x4a, 0xfd, 0x7f, 0x00, 0xf3, 0x3f, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x1a, 0xf2, 0x87, 0x7f, 0x00, 0x00, 0x04, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x92, 0x17, 0xf2, 0x87, 0x7f, 0x00, 0x00,
            0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x02, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x3b, 0x25,
            0x6e, 0xe3, 0x7f, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3b, 0x25, 0x6e, 0xe3, 0x7f, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        ])
        .unwrap();

        assert_eq!(
            output.out.unwrap().unwrap_err().to_errno(),
            Errno::UnknownErrno
        );
    }

    #[test]
    pub fn test_crash_2() {
        init();

        let output = test_with_data(vec![
            0x01, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x05, 0xc0, 0x9b, 0x6b, 0x40, 0xf2, 0x87, 0x7f,
            0xe0, 0xff, 0xe8, 0xa4, 0xdf, 0x4a, 0xfd, 0x7f, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x50, 0x07, 0x1a, 0xf2, 0x87, 0x7f, 0x00, 0x00, 0x04, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x92, 0x17, 0xf2, 0x87, 0x7f, 0x00, 0x00,
            0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0xc0, 0x9b, 0x6b,
            0x19, 0xf2, 0x87, 0x7f, 0x00, 0x00, 0xe8, 0xb8, 0xdf, 0x00, 0x00, 0x00, 0x00, 0x04,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xdb, 0x3c, 0x51, 0x4e, 0x56, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x97, 0xb0, 0x50, 0x4e,
            0x56, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x00,
            0x00, 0x1a, 0xf2, 0x87, 0x7f, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
            0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0xc0, 0x56, 0xa1, 0x17, 0x6e, 0xe3,
        ]);

        output.unwrap();
    }

    /*
    #[test]
    pub fn test_crash_3() {
        init();

        let output = test_with_data(vec![]);
    }
     */

    /// Helper to test syscall entry & exit with a specific data. Data should contain both entry & exit
    /// items in [u8] format, they will be casted to from_c::ptrace_syscall_info
    fn test_with_data(data: Vec<u8>) -> Result<TraceOutput, TraceError> {
        let (seed_sender, seed_receiver) = crossbeam_channel::bounded(5);
        let mut ptrace = MockPtrace::new(seed_receiver);
        ptrace.initialize()?;

        let (sender, r) = crossbeam_channel::bounded(5);
        let mut tracer_thread = TraceThread::new(ptrace, sender, TraceOptions::default());

        seed_sender.try_send(data.to_vec()).unwrap();
        tracer_thread.iterate()?;
        tracer_thread.iterate()?;

        let output = r.try_recv().map_err(|_| TraceError::NoOutput)?;
        tracer_thread.finalize()?;
        Ok(output)
    }
}
