use crate::prelude::*;
use std::thread::JoinHandle;

/// A CWE warning message.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord, Default)]
pub struct CweWarning {
    pub name: String,
    pub version: String,
    pub addresses: Vec<String>,
    pub tids: Vec<String>,
    pub symbols: Vec<String>,
    pub other: Vec<Vec<String>>,
    pub description: String,
}

impl CweWarning {
    /// Creates a new CweWarning by only setting name, version and description
    pub fn new(
        name: impl ToString,
        version: impl ToString,
        description: impl ToString,
    ) -> CweWarning {
        CweWarning {
            name: name.to_string(),
            version: version.to_string(),
            addresses: Vec::new(),
            tids: Vec::new(),
            symbols: Vec::new(),
            other: Vec::new(),
            description: description.to_string(),
        }
    }

    /// Sets the address field of the CweWarning
    pub fn addresses(mut self, addresses: Vec<String>) -> CweWarning {
        self.addresses = addresses;
        self
    }

    /// Sets the Tids field of the CweWarning
    pub fn tids(mut self, tids: Vec<String>) -> CweWarning {
        self.tids = tids;
        self
    }

    /// Sets the symbols field of the CweWarning
    pub fn symbols(mut self, symbols: Vec<String>) -> CweWarning {
        self.symbols = symbols;
        self
    }

    /// Sets the other field of the CweWarning
    pub fn other(mut self, other: Vec<Vec<String>>) -> CweWarning {
        self.other = other;
        self
    }
}

impl std::fmt::Display for CweWarning {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            formatter,
            "[{}] ({}) {}",
            self.name, self.version, self.description
        )
    }
}

/// A generic log message.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)]
pub struct LogMessage {
    /// The log message.
    pub text: String,
    /// The severity/type of the log message.
    pub level: LogLevel,
    /// The location inside the binary that the message is related to.
    pub location: Option<Tid>,
    /// The analysis where the message originated.
    pub source: Option<String>,
}

impl LogMessage {
    /// Create a new `Info`-level log message
    pub fn new_info(text: impl Into<String>) -> LogMessage {
        LogMessage {
            text: text.into(),
            level: LogLevel::Info,
            location: None,
            source: None,
        }
    }

    /// Create a new `Debug`-level log message
    pub fn new_debug(text: impl Into<String>) -> LogMessage {
        LogMessage {
            text: text.into(),
            level: LogLevel::Debug,
            location: None,
            source: None,
        }
    }

    /// Create a new `Error`-level log message
    pub fn new_error(text: impl Into<String>) -> LogMessage {
        LogMessage {
            text: text.into(),
            level: LogLevel::Error,
            location: None,
            source: None,
        }
    }

    /// Associate a specific location to the log message.
    pub fn location(mut self, location: Tid) -> LogMessage {
        self.location = Some(location);
        self
    }

    /// Set the name of the source analysis for the log message.
    pub fn source(mut self, source: impl Into<String>) -> LogMessage {
        self.source = Some(source.into());
        self
    }
}

/// The severity/type of a log message.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)]
pub enum LogLevel {
    /// Messages intended for debugging.
    Debug,
    /// Errors encountered during analysis.
    Error,
    /// Non-error messages intended for the user.
    Info,
}

impl std::fmt::Display for LogMessage {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self.level {
            LogLevel::Debug => write!(formatter, "DEBUG: ")?,
            LogLevel::Error => write!(formatter, "ERROR: ")?,
            LogLevel::Info => write!(formatter, "INFO: ")?,
        };
        match (&self.source, &self.location) {
            (Some(source), Some(location)) => write!(formatter, "{} @ {}: ", source, location)?,
            (Some(source), None) => write!(formatter, "{}: ", source)?,
            (None, Some(location)) => write!(formatter, "{}: ", location)?,
            (None, None) => (),
        };
        write!(formatter, "{}", self.text)
    }
}

/// Print all provided log- and CWE-messages.
///
/// Log-messages will always be printed to `stdout`.
/// CWE-warnings will either be printed to `stdout` or to the file path provided in `out_path`.
///
/// If `emit_json` is set, the CWE-warnings will be converted to json for the output.
pub fn print_all_messages(
    logs: Vec<LogMessage>,
    cwes: Vec<CweWarning>,
    out_path: Option<&str>,
    emit_json: bool,
) {
    for log in logs {
        println!("{}", log);
    }
    let output: String = if emit_json {
        serde_json::to_string_pretty(&cwes).unwrap()
    } else {
        cwes.iter()
            .map(|cwe| format!("{}", cwe))
            .collect::<Vec<String>>()
            .join("\n")
            + "\n"
    };
    if let Some(file_path) = out_path {
        std::fs::write(file_path, output).unwrap();
    } else {
        print!("{}", output);
    }
}

/// The message types a logging thread can receive.
/// See the [`LogThread`] type for more information.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)]
pub enum LogThreadMsg {
    /// A normal log message.
    Log(LogMessage),
    /// A CWE warning
    Cwe(CweWarning),
    /// If the log collector thread receives this signal,
    /// it should stop receiving new messages
    /// and instead terminate and return the collected messages prior to receiving the termination signal.
    Terminate,
}

/// A type for managing threads for collecting log messages.
///
/// With [`LogThread::spawn()`] one can create a new log thread
/// whose handle is contained in the returned `LogThread` struct.
/// By calling the [`collect()`](LogThread::collect()) method
/// one can tell the log thread to shut down
/// and return the logs collected to this point.
/// If the `LogThread` object gets dropped before calling `collect()`,
/// the corresponding logging thread will be stopped
/// and all collected logs will be discarded.
///
/// If one deliberately wants to discard all logging messages,
/// one can simply create a sender to a disconnected channel
/// via [`LogThread::create_disconnected_sender()`].
pub struct LogThread {
    msg_sender: crossbeam_channel::Sender<LogThreadMsg>,
    thread_handle: Option<JoinHandle<(Vec<LogMessage>, Vec<CweWarning>)>>,
}

impl Drop for LogThread {
    /// If the logging thread still exists,
    /// send it the `Terminate` signal.
    /// Then wait until the logging thread stopped.
    fn drop(&mut self) {
        // Make sure the logging thread gets terminated when dropping this.
        let _ = self.msg_sender.send(LogThreadMsg::Terminate);
        if let Some(handle) = self.thread_handle.take() {
            let _ = handle.join();
        }
    }
}

impl LogThread {
    /// Create a new `LogThread` object with a handle to a freshly spawned logging collector thread.
    ///
    /// The parameter is the function containing the actual log collection logic.
    /// I.e. the function should receive messages through the given receiver until the channel disconnects
    /// or until it receives a [`LogThreadMsg::Terminate`] message.
    /// After that it should return the logs collected up to that point.
    pub fn spawn<F>(collector_func: F) -> LogThread
    where
        F: FnOnce(crossbeam_channel::Receiver<LogThreadMsg>) -> (Vec<LogMessage>, Vec<CweWarning>)
            + Send
            + 'static,
    {
        let (sender, receiver) = crossbeam_channel::unbounded();
        let thread_handle = std::thread::spawn(move || collector_func(receiver));
        LogThread {
            msg_sender: sender,
            thread_handle: Some(thread_handle),
        }
    }

    /// Just create a disconnected sender to a (non-existing) logging thread.
    /// Can be used like a sender to a channel that deliberately discards all messages sent to it.
    pub fn create_disconnected_sender() -> crossbeam_channel::Sender<LogThreadMsg> {
        let (sender, _) = crossbeam_channel::unbounded();
        sender
    }

    /// Get a sender that can be used to send messages to the logging thread corresponding to this `LogThread` instance.
    pub fn get_msg_sender(&self) -> crossbeam_channel::Sender<LogThreadMsg> {
        self.msg_sender.clone()
    }

    /// Stop the logging thread by sending it the `Terminate` signal
    /// and then return all logs collected until that point.
    pub fn collect(mut self) -> (Vec<LogMessage>, Vec<CweWarning>) {
        let _ = self.msg_sender.send(LogThreadMsg::Terminate);
        if let Some(handle) = self.thread_handle.take() {
            handle.join().unwrap()
        } else {
            (Vec::new(), Vec::new())
        }
    }
}
