use std::path::Path;
use std::process::Command;

use crate::context::HeadlineLevelFilter;
use crate::settings::GlobalSettings;

/// Generate elisp to configure org-mode parsing settings
///
/// Currently only org-list-allow-alphabetical is supported.
fn global_settings_elisp(global_settings: &GlobalSettings) -> String {
    // This string concatenation is wildly inefficient but its only called in tests 🤷.
    let mut ret = "".to_owned();
    if global_settings.list_allow_alphabetical {
        ret += "(setq org-list-allow-alphabetical t)\n"
    }
    if global_settings.tab_width != crate::settings::DEFAULT_TAB_WIDTH {
        ret += format!("(setq-default tab-width {})", global_settings.tab_width).as_str();
    }
    if global_settings.odd_levels_only != HeadlineLevelFilter::default() {
        ret += match global_settings.odd_levels_only {
            HeadlineLevelFilter::Odd => "(setq org-odd-levels-only t)\n",
            HeadlineLevelFilter::OddEven => "(setq org-odd-levels-only nil)\n",
        };
    }
    ret
}

pub(crate) fn emacs_parse_anonymous_org_document<C>(
    file_contents: C,
    global_settings: &GlobalSettings,
) -> Result<String, Box<dyn std::error::Error>>
where
    C: AsRef<str>,
{
    let escaped_file_contents = escape_elisp_string(file_contents);
    let elisp_script = format!(
        r#"(progn
     (erase-buffer)
     (require 'org)
     (defun org-table-align () t)
     (insert "{escaped_file_contents}")
     {global_settings}
     (org-mode)
     (message "%s" (pp-to-string (org-element-parse-buffer)))
)"#,
        escaped_file_contents = escaped_file_contents,
        global_settings = global_settings_elisp(global_settings)
    );
    let mut cmd = Command::new("emacs");
    let cmd = cmd
        .arg("-q")
        .arg("--no-site-file")
        .arg("--no-splash")
        .arg("--batch")
        .arg("--eval")
        .arg(elisp_script);
    let out = cmd.output()?;
    let status = out.status.exit_ok();
    if status.is_err() {
        eprintln!(
            "Emacs errored out: {}\n{}",
            String::from_utf8(out.stdout)?,
            String::from_utf8(out.stderr)?
        );
        status?;
        unreachable!();
    }
    let org_sexp = out.stderr;
    Ok(String::from_utf8(org_sexp)?)
}

pub(crate) fn emacs_parse_file_org_document<P>(
    file_path: P,
    global_settings: &GlobalSettings,
) -> Result<String, Box<dyn std::error::Error>>
where
    P: AsRef<Path>,
{
    let file_path = file_path.as_ref().canonicalize()?;
    let containing_directory = file_path.parent().ok_or(format!(
        "Failed to get containing directory for path {}",
        file_path.display()
    ))?;
    let elisp_script = format!(
        r#"(progn
     (require 'org)
     (defun org-table-align () t)
     (setq vc-handled-backends nil)
     {global_settings}
     (find-file-read-only "{file_path}")
     (org-mode)
     (message "%s" (pp-to-string (org-element-parse-buffer)))
)"#,
        global_settings = global_settings_elisp(global_settings),
        file_path = file_path
            .as_os_str()
            .to_str()
            .expect("File name should be valid utf-8.")
    );
    let mut cmd = Command::new("emacs");
    let cmd = cmd
        .current_dir(containing_directory)
        .arg("-q")
        .arg("--no-site-file")
        .arg("--no-splash")
        .arg("--batch")
        .arg("--eval")
        .arg(elisp_script);
    let out = cmd.output()?;
    let status = out.status.exit_ok();
    if status.is_err() {
        eprintln!(
            "Emacs errored out: {}\n{}",
            String::from_utf8(out.stdout)?,
            String::from_utf8(out.stderr)?
        );
        status?;
        unreachable!();
    }
    let org_sexp = out.stderr;
    Ok(String::from_utf8(org_sexp)?)
}

fn escape_elisp_string<C>(file_contents: C) -> String
where
    C: AsRef<str>,
{
    let source = file_contents.as_ref();
    let source_len = source.len();
    // We allocate a string 10% larger than the source to account for escape characters. Without this, we would have more allocations during processing.
    let mut output = String::with_capacity(source_len + (source_len / 10));
    for c in source.chars() {
        match c {
            '"' | '\\' => {
                output.push('\\');
                output.push(c);
            }
            _ => {
                output.push(c);
            }
        }
    }
    output
}

pub fn get_emacs_version() -> Result<String, Box<dyn std::error::Error>> {
    let elisp_script = r#"(progn
     (message "%s" (version))
)"#;
    let mut cmd = Command::new("emacs");
    let cmd = cmd
        .arg("-q")
        .arg("--no-site-file")
        .arg("--no-splash")
        .arg("--batch")
        .arg("--eval")
        .arg(elisp_script);

    let out = cmd.output()?;
    out.status.exit_ok()?;
    Ok(String::from_utf8(out.stderr)?)
}

pub fn get_org_mode_version() -> Result<String, Box<dyn std::error::Error>> {
    let elisp_script = r#"(progn
     (org-mode)
     (message "%s" (org-version nil t nil))
)"#;
    let mut cmd = Command::new("emacs");
    let cmd = cmd
        .arg("-q")
        .arg("--no-site-file")
        .arg("--no-splash")
        .arg("--batch")
        .arg("--eval")
        .arg(elisp_script);

    let out = cmd.output()?;
    out.status.exit_ok()?;
    Ok(String::from_utf8(out.stderr)?)
}