use crate::app::PatchpalApplication;
use gtk::glib;
use gtk::prelude::*;
use std::{cell::Cell, io::Read, io::Write, ffi::OsStr, ffi::OsString, fs::File,
    path::Path, rc::Rc};

struct CallbackData {
    func: Box<dyn FnOnce(Option<Vec<u8>>)>,
    name: OsString,
    data: Vec<u8>,
    tmpf: Option<tempfile::NamedTempFile>,
}

#[derive(Clone)]
struct Callback(Rc<Cell<Option<CallbackData>>>);

#[derive(Clone, Debug)]
pub(crate) struct Editor {
    application: PatchpalApplication,
}

fn strerr(err: impl ToString) -> String {
    err.to_string()
}

/// See https://github.com/gtk-rs/gtk-rs-core/issues/1450
fn workaround_envp_bug() -> Vec<OsString> {
    let mut result = vec![];
    for (mut key, value) in std::env::vars_os() {
        key.push("=");
        key.push(&value);
        result.push(key);
    }
    result
}

impl CallbackData {
    fn call(self, exit_code: i32) {
        if exit_code == 0 {
            if let Ok(mut f) = File::open(self.tmpf.unwrap().path()) {
                let mut data = vec![];
                if f.read_to_end(&mut data).is_ok() {
                    (self.func)(Some(data));
                    return;
                }
            }
        }
        (self.func)(None);
    }
}

impl Callback {
    fn new<F>(name: OsString, data: Vec<u8>, func: F) -> Self
            where F: FnOnce(Option<Vec<u8>>) + 'static {
        Self(Rc::new(Cell::new(Some(CallbackData {
            func: Box::new(func),
            name,
            data,
            tmpf: None,
        }))))
    }

    fn take(&self) -> Option<CallbackData> {
        self.0.take()
    }

    fn call_failed(&self) {
        if let Some(cb) = self.0.take() {
            (cb.func)(None);
        }
    }
}

impl Editor {
    pub(crate) fn new(application: &PatchpalApplication) -> Self {
        Self {
            application: application.clone(),
        }
    }

    fn app_window(&self) -> &gtk::Window {
        self.application.app_window().upcast_ref()
    }

    pub(crate) fn launch<F>(&self, filename: OsString, data: Vec<u8>, done: F)
            where F: FnOnce(Option<Vec<u8>>) + 'static {
        let cb = Callback::new(filename, data, done);
        match self.application.config().get::<String>("editor") {
            Some(cmdline) => self.run_editor(&cmdline, cb.take().unwrap()),
            None => self.config_dialog(cb)
        };
    }

    fn run_editor(&self, cmdline: &str, mut cbd: CallbackData) {
        match self.run_editor_fallible(cmdline, &mut cbd) {
            Err(msg) => self.show_error(msg, cbd),
            Ok(pid) => {
                glib::spawn_future_local(async move {
                    let (_, exit_code) = glib::child_watch_future(pid).await;
                    cbd.call(exit_code);
                });
            }
        }
    }

    fn run_editor_fallible(&self, cmdline: &str, cbd: &mut CallbackData)
            -> Result<glib::Pid, String> {
        let mut suffix = OsString::new();
        suffix.push(".");
        suffix.push(Path::new(&cbd.name).file_name().unwrap_or(&OsStr::new("c")));
        let mut tmpf = tempfile::NamedTempFile::with_suffix(suffix).map_err(strerr)?;
        tmpf.write_all(&cbd.data).map_err(strerr)?;
        tmpf.flush().map_err(strerr)?;
        let mut cmdline = glib::shell_parse_argv(OsStr::new(cmdline)).map_err(strerr)?;
        let mut found = false;
        for part in cmdline.iter_mut() {
            if part == "%f" {
                part.clear();
                part.push(tmpf.path().as_os_str());
                found = true;
            }
        }
        if !found {
            // no %f present, add the path as the last argument
            cmdline.push(tmpf.path().as_os_str().to_os_string());
        }
        let cmdlineref: Vec<_> = cmdline.iter().map(|p| Path::new(p)).collect();
        let envp = workaround_envp_bug();
        let envpref: Vec<_> = envp.iter().map(|p| Path::new(p)).collect();
        cbd.tmpf = Some(tmpf);
        glib::spawn_async(None::<&Path>, &cmdlineref, &envpref,
                glib::SpawnFlags::DO_NOT_REAP_CHILD | glib::SpawnFlags::SEARCH_PATH,
                None)
            .map_err(strerr)
    }

    fn show_error(&self, msg: String, cbd: CallbackData) {
        let alert = gtk::AlertDialog::builder()
            .detail(&msg)
            .message("Error running editor")
            .modal(true)
            .build();
        alert.show(Some(self.app_window()));
        (cbd.func)(None);
    }

    fn config_dialog(&self, cb: Callback) {
        let label = gtk::Label::builder()
            .label("Enter the command line to invoke an external editor. \
                %f will be replaced with the file path.")
            .wrap(true)
            .build();
        let cmdline = gtk::Entry::builder()
            .placeholder_text("e.g. gvim -f %f")
            .activates_default(true)
            .hexpand(true)
            .build();
        let button_set = gtk::Button::builder()
            .label("Set")
            .build();
        let button_cancel = gtk::Button::builder()
            .label("Cancel")
            .build();
        let hbox = gtk::Box::builder()
            .orientation(gtk::Orientation::Horizontal)
            .spacing(10)
            .build();
        hbox.append(&button_set);
        hbox.append(&button_cancel);
        let vbox = gtk::Box::builder()
            .margin_bottom(10)
            .margin_end(10)
            .margin_start(10)
            .margin_top(10)
            .orientation(gtk::Orientation::Vertical)
            .spacing(10)
            .build();
        vbox.append(&label);
        vbox.append(&cmdline);
        vbox.append(&hbox);
        let window = gtk::Window::builder()
            .child(&vbox)
            .default_widget(&button_set)
            .modal(true)
            .title("Set editor")
            .transient_for(self.app_window())
            .application(&self.application)
            .build();

        button_set.connect_clicked(glib::clone!(
            #[weak] window,
            #[weak] cmdline,
            #[strong] cb,
            #[strong(rename_to = editor)] self,
            move |_| {
                if let Some(cbd) = cb.take() {
                    let text = cmdline.buffer().text();
                    let config = editor.application.config();
                    config.set("editor", text.to_string());
                    let _ = config.save();   // ignore errors
                    editor.run_editor(text.as_str(), cbd);
                }
                window.close();
            }
        ));
        button_cancel.connect_clicked(glib::clone!(
            #[weak] window,
            #[strong] cb,
            move |_| {
                cb.call_failed();
                window.close();
            }
        ));
        window.connect_close_request(glib::clone!(
            #[strong] cb,
            move |_| {
                cb.call_failed();
                glib::Propagation::Proceed
            }
        ));
        window.present();
    }
}
