use std::fmt;
use std::str::FromStr;

fn parse_path(s: &str) -> Option<(&str, &str)> {
    let path = s.strip_prefix("[string \"")?;
    let (path, after) = path.split_once("\"]:")?;

    // Remove line number after any found colon, this may
    // exist if the source path is from a rust source file
    let path = match path.split_once(':') {
        Some((before, _)) => before,
        None => path,
    };

    Some((path, after))
}

fn parse_function_name(s: &str) -> Option<&str> {
    s.strip_prefix("in function '")
        .and_then(|s| s.strip_suffix('\''))
}

fn parse_line_number(s: &str) -> (Option<usize>, &str) {
    match s.split_once(':') {
        Some((before, after)) => (before.parse::<usize>().ok(), after),
        None => (None, s),
    }
}

/**
    Source of a stack trace line parsed from a [`LuaError`].
*/
#[derive(Debug, Default, Clone, Copy)]
pub enum StackTraceSource {
    /// Error originated from a C / Rust function.
    C,
    /// Error originated from a Lua (user) function.
    #[default]
    Lua,
}

/**
    Stack trace line parsed from a [`LuaError`].
*/
#[derive(Debug, Default, Clone)]
pub struct StackTraceLine {
    source: StackTraceSource,
    path: Option<String>,
    line_number: Option<usize>,
    function_name: Option<String>,
}

impl StackTraceLine {
    /**
        Returns the source of the stack trace line.
    */
    #[must_use]
    pub fn source(&self) -> StackTraceSource {
        self.source
    }

    /**
        Returns the path, if it exists.
    */
    #[must_use]
    pub fn path(&self) -> Option<&str> {
        self.path.as_deref()
    }

    /**
        Returns the line number, if it exists.
    */
    #[must_use]
    pub fn line_number(&self) -> Option<usize> {
        self.line_number
    }

    /**
        Returns the function name, if it exists.
    */
    #[must_use]
    pub fn function_name(&self) -> Option<&str> {
        self.function_name.as_deref()
    }
}

impl FromStr for StackTraceLine {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if let Some(after) = s.strip_prefix("[C]: ") {
            let function_name = parse_function_name(after).map(ToString::to_string);

            Ok(Self {
                source: StackTraceSource::C,
                path: None,
                line_number: None,
                function_name,
            })
        } else if let Some((path, after)) = parse_path(s) {
            let (line_number, after) = parse_line_number(after);
            let function_name = parse_function_name(after).map(ToString::to_string);

            Ok(Self {
                source: StackTraceSource::Lua,
                path: Some(path.to_string()),
                line_number,
                function_name,
            })
        } else {
            Err(String::from("unknown format"))
        }
    }
}

impl fmt::Display for StackTraceLine {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if matches!(self.source, StackTraceSource::C) {
            write!(f, "Script '[C]'")?;
        } else {
            write!(f, "Script '{}'", self.path.as_deref().unwrap_or("[?]"))?;
            if let Some(line_number) = self.line_number {
                write!(f, ", Line {line_number}")?;
            }
        }
        if let Some(function_name) = self.function_name.as_deref() {
            write!(f, " - function '{function_name}'")?;
        }
        Ok(())
    }
}

/**
    Stack trace parsed from a [`LuaError`].
*/
#[derive(Debug, Default, Clone)]
pub struct StackTrace {
    lines: Vec<StackTraceLine>,
}

impl StackTrace {
    /**
        Returns the individual stack trace lines.
    */
    #[must_use]
    pub fn lines(&self) -> &[StackTraceLine] {
        &self.lines
    }
}

impl FromStr for StackTrace {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let (_, after) = s
            .split_once("stack traceback:")
            .ok_or_else(|| String::from("missing 'stack traceback:' prefix"))?;
        let lines = after
            .trim()
            .lines()
            .filter_map(|line| {
                let line = line.trim();
                if line.is_empty() {
                    None
                } else {
                    Some(line.parse())
                }
            })
            .collect::<Result<Vec<_>, _>>()?;
        Ok(StackTrace { lines })
    }
}
