use std::path::{Path, PathBuf};
use object::{Object, ObjectSymbol};
use tera::{Context, Tera};
#[derive(Debug, Clone, clap::Parser)]
pub struct CliConfig {
    #[arg(long)]
    pub runner_cfg: Option<PathBuf>,
    #[arg(long, short = 'v')]
    pub verbose: bool,
    #[command(subcommand)]
    pub cmd: Cmd,
}
#[derive(Debug, Clone)]
pub struct Config {
    pub runner_cfg: RunnerConfig,
    pub verbose: bool,
    pub workspace_dir: PathBuf,
    pub embedded_dir: PathBuf,
}
#[derive(Debug, Clone, clap::Parser)]
pub enum Cmd {
    Run(RunConfig),
    #[command(subcommand)]
    Mantra(mantra::cmd::Cmd),
}
#[derive(Debug, Clone, clap::Parser)]
pub struct RunConfig {
    pub binary: PathBuf,
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct RunnerConfig {
    pub load: String,
    #[serde(alias = "openocd-cfg")]
    pub openocd_cfg: Option<PathBuf>,
    #[serde(alias = "openocd-log")]
    pub openocd_log: Option<PathBuf>,
    #[serde(alias = "pre-runner")]
    pub pre_runner: Option<Command>,
    #[serde(alias = "post-runner")]
    pub post_runner: Option<Command>,
    #[serde(alias = "rtt-port")]
    pub rtt_port: Option<u16>,
    pub mantra: Option<MantraConfig>,
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct MantraConfig {
    #[serde(alias = "db-url")]
    pub db_url: Option<String>,
    pub extract: Option<mantra::cmd::extract::Config>,
    #[serde(alias = "extern-traces")]
    pub extern_traces: Option<Vec<PathBuf>>,
    #[serde(default, alias = "dry-run")]
    pub dry_run: bool,
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct Command {
    pub name: String,
    pub args: Vec<String>,
}
#[derive(Debug, thiserror::Error)]
pub enum CfgError {
    #[error("Could not find rtt block in binary. Cause: {}", .0)]
    FindingRttBlock(String),
    #[error("Could not resolve the load section. Cause: {}", .0)]
    ResolvingLoadSection(String),
}
impl RunnerConfig {
    pub fn gdb_script(&self, binary: &Path) -> Result<String, CfgError> {
        let resolved_load = resolve_load(&self.load, binary)?;
        let (rtt_address, rtt_length) = find_rtt_block(binary)?;
        #[cfg(target_os = "windows")]
        let sleep_cmd = "timeout";
        #[cfg(not(target_os = "windows"))]
        let sleep_cmd = "sleep";
        Ok(format!(
            "
set pagination off
target extended-remote | openocd -c \"gdb_port pipe; log_output {}\" -f {}
{}
b main
continue
monitor rtt setup 0x{:x} {} \"SEGGER RTT\"
monitor rtt start
monitor rtt server start {} 0
shell {sleep_cmd} 1
continue
shell {sleep_cmd} 1
quit        
",
            self.openocd_log
                .clone()
                .unwrap_or(PathBuf::from(".embedded/openocd.log"))
                .to_string_lossy(),
            self.openocd_cfg
                .clone()
                .unwrap_or(PathBuf::from(".embedded/openocd.cfg"))
                .to_string_lossy(),
            resolved_load,
            rtt_address,
            rtt_length,
            self.rtt_port.unwrap_or(super::DEFAULT_RTT_PORT)
        ))
    }
}
fn find_rtt_block(binary: &Path) -> Result<(u64, u64), CfgError> {
    let data = std::fs::read(binary).map_err(|err| {
        CfgError::FindingRttBlock(format!("Could not read binary file. Cause: {err}"))
    })?;
    let file = object::File::parse(&*data).map_err(|err| {
        CfgError::FindingRttBlock(format!("Could not parse binary file. Cause: {err}"))
    })?;
    for symbol in file.symbols() {
        if symbol.name() == Ok("_SEGGER_RTT") {
            return Ok((symbol.address(), symbol.size()));
        }
    }
    Err(CfgError::FindingRttBlock(
        "No _SEGGER_RTT symbol in binary!".to_string(),
    ))
}
fn resolve_load(load: &str, binary: &Path) -> Result<String, CfgError> {
    let mut context = Context::new();
    let parent = binary.parent().map(|p| p.to_path_buf()).unwrap_or_default();
    context.insert("binary_path", &parent);
    context.insert(
        "binary_filepath_noextension",
        &Path::join(
            &parent,
            binary.file_stem().ok_or_else(|| {
                CfgError::ResolvingLoadSection(format!(
                    "Given binary '{}' has no valid filename.",
                    binary.display()
                ))
            })?,
        ),
    );
    context.insert("binary_filepath", &binary);
    Tera::one_off(load, &context, false).map_err(|err| {
        CfgError::ResolvingLoadSection(format!("Failed rendering the load template. Cause: {err}"))
    })
}
#[cfg(test)]
mod test {
    use std::path::PathBuf;
    use super::{find_rtt_block, resolve_load};
    #[cfg(target_os = "windows")]
    #[test]
    fn load_template() {
        let load = "load \"{{ binary_path }}\\debug_config.ihex\"
load \"{{ binary_filepath_noextension }}.ihex\"
file \"{{ binary_filepath }}\"";
        let binary = PathBuf::from(".\\target\\debug\\hello.exe");
        let resolved = resolve_load(load, &binary).unwrap();
        assert!(
            resolved.contains("target\\debug\\debug_config.ihex"),
            "Binary path not resolved."
        );
        assert!(
            resolved.contains("target\\debug\\hello.ihex"),
            "Binary file path without extension not resolved."
        );
        assert!(
            resolved.contains("target\\debug\\hello.exe"),
            "Binary file path with extension not resolved."
        );
    }
    #[cfg(not(target_os = "windows"))]
    #[test]
    fn load_template() {
        let load = "load \"{{ binary_path }}/debug_config.ihex\"
load \"{{ binary_filepath_noextension }}.ihex\"
file \"{{ binary_filepath }}\"";
        let binary = PathBuf::from("./target/debug/hello.exe");
        let resolved = resolve_load(load, &binary).unwrap();
        assert!(
            resolved.contains("target/debug/debug_config.ihex"),
            "Binary path not resolved."
        );
        assert!(
            resolved.contains("target/debug/hello.ihex"),
            "Binary file path without extension not resolved."
        );
        assert!(
            resolved.contains("target/debug/hello.exe"),
            "Binary file path with extension not resolved."
        );
    }
    #[test]
    fn rtt_block_in_binary() {
        let binary = PathBuf::from("test_binaries/emb-runner-test");
        let (address, size) = find_rtt_block(&binary).unwrap();
        dbg!(address);
        dbg!(size);
    }
}