use adw::prelude::*;
use gtk::{glib, ApplicationWindow, Stack, StackSidebar, StackPage, Box, Button, Label, DropDown, StringList, Entry, PasswordEntry, CheckButton, Image, Picture};
use adw::{Application, NavigationSplitView, NavigationPage, HeaderBar, ToolbarView};
use gtk::gdk;
use crate::glib::clone;
use std::process::Command;
use regex::Regex;
use quick_xml::reader::Reader as XmlReader;
use quick_xml::events::Event as XmlEvent;
use std::collections::HashMap;
use std::io::Write;
use pwhash::bcrypt;
use std::cell::RefCell;
use std::rc::Rc;
use std::io::BufRead;
use std::fs;

const APP_ID: &str = "com.mntre.reform-setup";
const MARGIN: i32 = 48;
const DESKTOP_IMG_WIDTH: i32 = 392/2;
const DESKTOP_IMG_HEIGHT: i32 = 278/2;
const WELCOME_IMG_WIDTH: i32 = 640/2;
const WELCOME_IMG_HEIGHT: i32 = 534/2;
const CSS_CLASS_TITLE: &str = "title-3";

const PATH_ETC_DEFAULT_KEYBOARD: &str = "/etc/default/keyboard";
const TEMPLATE_ETC_DEFAULT_KEYBOARD: &str = r#"# KEYBOARD CONFIGURATION FILE

# Consult the keyboard(5) manual page.

XKBMODEL="pc105"
XKBLAYOUT="${LAYOUT}"
XKBVARIANT="${VARIANT}"
XKBOPTIONS=""

BACKSPACE="guess"
"#;

const PATH_ETC_HOSTNAME: &str = "/etc/hostname";
const PATH_ETC_HOSTS: &str = "/etc/hosts";
const TEMPLATE_ETC_HOSTS: &str = r#"127.0.0.1 localhost ${HOSTNAME}
::1 localhost ip6-localhost ip6-loopback ${HOSTNAME}
"#;

const DESKTOP_BINARIES : [&str; 2] = [ "sway", "wayfire" ];
const PATH_ETC_GREETD_CONFIG: &str = "/etc/greetd/config.toml";
const TEMPLATE_ETC_GREETD_CONFIG: &str = r#"[terminal]
# The VT to run the greeter on. Can be "next", "current" or a number
# designating the VT.
vt = 7

# The default session, also known as the greeter.
[default_session]

command = "/usr/bin/tuigreet --window-padding 4 --remember --asterisks --cmd /usr/bin/${DESKTOP}"

# The user to run the command as. The privileges this user must have depends
# on the greeter. A graphical greeter may for example require the user to be
# in the `video` group.
user = "_greetd"

"#;
const PATH_SKEL_SWAY_INPUT : &str = "/etc/skel/.config/sway/config.d/input";
const TEMPLATE_SKEL_SWAY_INPUT: &str = r#"# change to de if you have a QWERTZ keyboard, for example
input * {
        xkb_layout ${LAYOUT}
        xkb_variant "${VARIANT}"
        xkb_options lv3:ralt_switch
}
"#;
const PATH_SKEL_WAYFIRE_INI: &str = "/etc/skel/.config/wayfire.ini";
const PATH_CLEANUP_SCRIPTS: &str = "/usr/share/reform-setup-wizard/cleanup.d";

// see also:
// - https://martinber.github.io/gtk-rs.github.io/docs-src/tutorial/closures
// - https://docs.rs/libadwaita/latest/libadwaita
// - https://gtk-rs.org/gtk4-rs/stable/latest/docs/gtk4/
// - https://kerkour.com/rust-cross-compilation

fn valid_password(pw: &str) -> bool {
    return pw.len() > 0
}

fn valid_hostname(hostname: &str) -> bool {
    let regex = Regex::new(r"^[a-z0-9][a-z0-9-]*$").unwrap();
    return hostname.len() > 1 && hostname.len() < 64 && regex.is_match(hostname);
}

fn valid_username(name: &str) -> bool {
    // as per https://systemd.io/USER_NAMES/#common-core
    return Regex::new(r"^[a-z][a-z0-9-]{0,30}$").unwrap().is_match(name)
}

fn run_command(description: &str, executable: &str, params: Vec<&str>) -> Result<String, String> {
    println!("STEP: {}", &description);
    return match Command::new(&executable).args(&params).output() {
        Ok(output) => {
            let stderr = match String::from_utf8(output.stderr.clone()) {
                Ok(s) => s,
                _ => return Err(format!("Error: {}: {} {:?}: failed to parse utf8 output: {:?}", description, executable, params, &output.stderr))
            };
            let stdout = match String::from_utf8(output.stdout.clone()) {
                Ok(s) => s,
                _ => return Err(format!("Error: {}: {} {:?}: failed to parse utf8 output: {:?}", description, executable, params, &output.stdout))
            };
            println!("STATUS: {:?}", output.status.code());
            println!("STDOUT: {}", &stdout);
            println!("STDERR: {}", &stderr);
            match output.status.code() {
                Some(c) if c == 0 => Ok("".to_string()),
                _ => Err(format!("Error: {}: {} {:?}: {}", description, executable, params, &stderr))
            }
        },
        Err(e) => Err(format!("Error: {}: {} {:?}: {}", description, executable, params, e))
    };
}

fn set_password(description: &str, pw: &str, username: &str) -> Result<(), String> {
    let digest = match bcrypt::hash(&pw) {
        Ok(v) => v.to_string(),
        Err(e) => return Err(format!("Error: Password not hashable: {}", e).to_string())
    };
    run_command(description, "usermod", ["--password", &digest as &str, username].to_vec())?;
    Ok(())
}

fn write_file(path: &str, content: &str) -> Result<(),String> {
    match std::path::Path::new(path).parent() {
        Some(p) => match std::fs::create_dir_all(p) {
                Ok(_) => (),
                Err(e) => return Err(format!("Error ensuring {}: {}", path, e))
        },
        _ => ()
    };
    match std::fs::File::create(path) {
        Ok(mut f) => {
            match f.write_all(content.as_bytes()) {
                Ok(_) => Ok(println!("{} successfully written.", path)),
                Err(e) => Err(format!("Error writing {}: {}", path, e))
            }
        },
        Err(e) => Err(format!("Error opening {} for writing: {}", path, e))
    }
}

fn main() -> glib::ExitCode {
    let app = Application::builder().application_id(APP_ID).build();

    app.connect_activate(build_ui);
    app.run()
}

fn build_box() -> Box {
    return Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .margin_top(MARGIN)
        .margin_bottom(MARGIN)
        .margin_start(MARGIN)
        .margin_end(MARGIN)
        .spacing(MARGIN/2)
        .valign(gtk::Align::Center)
        .build();
}

fn get_device_model() -> String {
    match fs::read_to_string("/proc/device-tree/model") {
        Ok(s) => s,
        Err(_) => "Unknown Device".to_string()
    }
}

fn is_pocket_reform() -> bool {
    get_device_model().contains("MNT Pocket Reform")
}

fn build_welcome_page(stack: &Stack) -> StackPage {
    let is_pref = is_pocket_reform();

    let img_pocket = png_picture_from_bytes(include_bytes!("../gfx/pocket-reform-magic.png"));
    img_pocket.set_size_request(WELCOME_IMG_WIDTH, WELCOME_IMG_HEIGHT);

    let img_reform = png_picture_from_bytes(include_bytes!("../gfx/mnt-reform.png"));
    img_pocket.set_size_request(WELCOME_IMG_WIDTH, WELCOME_IMG_HEIGHT);

    let button_next = Button::builder().label("Get Started").build();
    button_next.add_css_class("suggested-action");
    button_next.add_css_class("pill");
    let page_box = build_box();
    //page_box.set_spacing(0);
    let welcome_label = if is_pref {
        Label::new(Some("Welcome to MNT Pocket Reform"))
    } else {
        Label::new(Some("Welcome to MNT Reform"))
    };
    welcome_label.add_css_class(CSS_CLASS_TITLE);
    if is_pref {
        page_box.append(&img_pocket);
    } else {
        page_box.append(&img_reform);
    }
    page_box.append(&welcome_label);
    page_box.append(&button_next);

    button_next.connect_clicked(
        clone!(@weak stack => move |_| {
            stack.set_visible_child_full("keyboard", gtk::StackTransitionType::SlideLeft);
        })
    );

    return stack.add_titled(&page_box, Some("welcome"), "Welcome");
}

#[derive(Clone)]
struct XkbVariant {
    layout: String,
    variant: String,
}

fn parse_keyboard_layout_file(file_path: &str) -> HashMap<String, XkbVariant> {
     let mut layouts = HashMap::<String, XkbVariant>::new();
     let mut reader = XmlReader::from_file(file_path).unwrap();
     let mut in_layout_list = false;
     let mut in_config_item = false;
     let mut in_name = false;
     let mut in_description = false;
     let mut in_variant = false;
     let mut variant = String::new();
     let mut layout = String::new();
     let mut description = String::new();
     let mut buf = Vec::new();
     loop {
         match reader.read_event_into(&mut buf) {
             Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e),
             Ok(XmlEvent::Eof) => break,
             Ok(XmlEvent::Start(e)) => {
                 match e.name().as_ref() {
                     b"layoutList" => in_layout_list = true,
                     b"configItem" => in_config_item = true,
                     b"name" => in_name = true,
                     b"description" => in_description = true,
                     b"variant" => in_variant = true,
                     _ => ()
                 }
             },
             Ok(XmlEvent::End(e)) => {
                 match e.name().as_ref() {
                     b"layoutList" => in_layout_list = false,
                     b"layout" => {
                         layout = "".to_string();
                     },
                     b"configItem" => {
                         description = "".to_string();
                         variant = "".to_string();
                         in_config_item = false;
                     },
                     b"name" => in_name = false,
                     b"description" => in_description = false,
                     b"variant" => in_variant = false,
                     _ => ()
                 };
             },
             Ok(XmlEvent::Text(e)) if in_layout_list && in_config_item  => {
                 let val = e.unescape().unwrap().clone();
                 if in_name {
                     if in_variant {
                        variant = val.to_string();
                     } else {
                        layout = val.to_string();
                     }
                 } else if in_description {
                     description = val.to_string();
                 }
                 if !layout.is_empty() && !description.is_empty() {
                     layouts.entry(description.to_string()).or_insert(XkbVariant {
                         layout: layout.to_string(),
                         variant: variant.to_string()
                     });
                 }
             },
             _ => ()
         }
         buf.clear();
     }
     return layouts;
}

fn build_keyboard_page(stack: &Stack) -> StackPage {
    let button_next = Button::builder().label("Next").build();
    button_next.add_css_class("suggested-action");
    button_next.add_css_class("pill");
    let page_box = build_box();

    let mut layouts_map = parse_keyboard_layout_file("/usr/share/X11/xkb/rules/base.xml");
    layouts_map.extend(parse_keyboard_layout_file("/usr/share/X11/xkb/rules/base.extras.xml"));
    layouts_map.retain(|_, v| v.layout != "custom");
    let mut dropdown_pairs: Vec<(String, XkbVariant)> = layouts_map.into_iter().collect();
    dropdown_pairs.sort_by(|a, b| a.0.cmp(&b.0));
    let layout_keys: StringList = dropdown_pairs.iter().map(|(k, _)| k.clone()).collect();
    let layout_vals: Vec<XkbVariant> = dropdown_pairs.iter().map(|(_, v)| v.clone()).collect();
    let dropdown = DropDown::builder()
        .model(&layout_keys)
        .enable_search(true)
        .build();
    let search_exp = gtk::PropertyExpression::new(gtk::StringObject::static_type(), None::<gtk::Expression>, "string");
    dropdown.set_expression(Some(search_exp));
    dropdown.set_search_match_mode(gtk::StringFilterMatchMode::Substring);
    let default_index = layout_vals.iter().position(|v| v.layout == "eu" && v.variant == "").unwrap();
    dropdown.set_selected(default_index as u32);

    let layout_label = &Label::new(Some("Please select your keyboard layout:"));
    layout_label.add_css_class(CSS_CLASS_TITLE);
    page_box.append(layout_label);
    page_box.append(&dropdown);
    let error_label = Label::new(None);
    error_label.add_css_class("error");
    error_label.set_selectable(true);
    error_label.set_wrap(true);
    page_box.append(&error_label);
    page_box.append(&button_next);

    button_next.connect_clicked(
        clone!(@weak stack => move |_| {
            // TODO: find out to what degree this actually sets the keyboard layout
            let layout: &XkbVariant = &layout_vals[dropdown.selected() as usize];
            println!("Selected keyboard layout: {:?} variant: {:?}", &layout.layout, &layout.variant);
            // normally, the Setup Wizard runs under sway.
            // so we immediately apply the keyboard layout here.
            let _ = run_command("apply keyboard layout in running sway session", "swaymsg", ["input", "*", "xkb_layout", format!("\"{}\"", &layout.layout).as_str()].to_vec());
            let _ = run_command("apply keyboard layout variant in running sway session", "swaymsg", ["input", "*", "xkb_variant", format!("\"{}\"", &layout.variant).as_str()].to_vec());

            let new_default = TEMPLATE_ETC_DEFAULT_KEYBOARD.replace("${LAYOUT}", &layout.layout).replace("${VARIANT}", &layout.variant);
            match write_file(PATH_ETC_DEFAULT_KEYBOARD, &new_default) {
                Ok(_) => (),
                Err(e) => {
                    error_label.set_label(&e);
                    return;
                }
            }
            let new_sway_input = TEMPLATE_SKEL_SWAY_INPUT.replace("${LAYOUT}", &layout.layout).replace("${VARIANT}", &layout.variant);
            match write_file(PATH_SKEL_SWAY_INPUT, &new_sway_input) {
                Ok(_) => (),
                Err(e) => {
                    error_label.set_label(&e);
                    return;
                }
            }
            match run_command("write xkb_layout into wayfire.ini", "sh", ["-c", &format!("sed -i -E 's/^xkb_layout ?=.*$/xkb_layout = {}/g' {}", &layout.layout, PATH_SKEL_WAYFIRE_INI)].to_vec()) {
                Ok(_) => (),
                Err(e) => {
                    error_label.set_label(&e);
                    return
                }
            };
            match run_command("write xkb_variant into wayfire.ini", "sh", ["-c", &format!("sed -i -E 's/^xkb_variant ?=.*$/xkb_variant = {}/g' {}", &layout.variant, PATH_SKEL_WAYFIRE_INI)].to_vec()) {
                Ok(_) => (),
                Err(e) => {
                    error_label.set_label(&e);
                    return
                }
            };
            stack.set_visible_child_full("time", gtk::StackTransitionType::SlideLeft);
        })
    );

    return stack.add_titled(&page_box, Some("keyboard"), "Keyboard Layout");
}

fn build_time_page(stack: &Stack) -> StackPage {
    let button_next = Button::builder().label("Next").build();
    button_next.add_css_class("suggested-action");
    button_next.add_css_class("pill");
    let page_box = build_box();

    // let output = String::from_utf8(Command::new("timedatectl")
    //                                .args(["list-timezones"])
    //                                .output()
    //                                .expect("failed to execute timedatectl").stdout)
    //     .expect("failed to parse utf8 output of timedatectl");

    // NB: above wouldn't run easily within chroot, so for now let's use this
    let output = String::from_utf8(Command::new("sh")
                                   .args(["-c", "awk '/^Z/ { print $2 }; /^L/ { print $3 }' /usr/share/zoneinfo/tzdata.zi | sort"])
                                   .output()
                                   .expect("failed to collect timezones").stdout)
        .expect("failed to parse utf8 output of awk");

    let lines:Vec<&str> = output.lines().collect();
    let list = StringList::new(&lines);
    let dropdown = DropDown::builder()
        .model(&list)
        .enable_search(true)
        .build();
    let search_exp = gtk::PropertyExpression::new(gtk::StringObject::static_type(), None::<gtk::Expression>, "string");
    dropdown.set_expression(Some(search_exp));
    dropdown.set_search_match_mode(gtk::StringFilterMatchMode::Substring);
    let default_index = lines.iter().position(|v| v == &"Europe/Berlin").unwrap();
    dropdown.set_selected(default_index as u32);

    let tz_label = &Label::new(Some("Please choose your timezone:"));
    tz_label.add_css_class(CSS_CLASS_TITLE);
    page_box.append(tz_label);
    page_box.append(&dropdown);
    let error_label = Label::new(None);
    error_label.add_css_class("error");
    error_label.set_selectable(true);
    error_label.set_wrap(true);
    page_box.append(&error_label);
    page_box.append(&button_next);

    button_next.connect_clicked(
        clone!(@weak stack => move |_| {
            let tz = match list.string(dropdown.selected()) {
                Some(s) => s,
                _ => {
                    error_label.set_label("no timezone in dropdown for selection");
                    return
                }
            };
            match run_command("set timezone", "sh", ["-c", &format!("echo '{}' > /etc/timezone", tz)].to_vec()) {
                Ok(_) => (),
                Err(e) => {
                    error_label.set_label(&e);
                    return
                }
            };
            match run_command("set localtime", "ln", ["-sf", &format!("/usr/share/zoneinfo/{}", tz), "/etc/localtime"].to_vec()) {
                Ok(_) => (),
                Err(e) => {
                    error_label.set_label(&e);
                    return
                }
            };
            match std::env::var("CHROOT_MODE") {
                Ok(_) => (),
                _ => match run_command("set timezone", "timedatectl", ["set-timezone", &tz].to_vec()) {
                    Ok(_) => (),
                    Err(e) => {
                        error_label.set_label(&e);
                        return
                    }
                }
            };
            stack.set_visible_child_full("desktop", gtk::StackTransitionType::SlideLeft);
        })
    );

    return stack.add_titled(&page_box, Some("time"), "Time");
}

fn svg_image_from_bytes(bytes: &[u8]) -> Image {
    let loader = gdk_pixbuf::PixbufLoader::with_type("svg").unwrap();
    loader.write(bytes).unwrap();
    loader.close().unwrap();
    let pixbuf = loader.pixbuf().unwrap();
    return gtk::Image::from_paintable(Some(&gdk::Texture::for_pixbuf(&pixbuf)));
}

fn png_picture_from_bytes(bytes: &[u8]) -> Picture {
    let loader = gdk_pixbuf::PixbufLoader::with_type("png").unwrap();
    loader.write(bytes).unwrap();
    loader.close().unwrap();
    let pixbuf = loader.pixbuf().unwrap();
    //return gtk::Picture::from_paintable(Some(&gdk::Texture::for_pixbuf(&pixbuf)));
    return gtk::Picture::for_paintable(&gdk::Texture::for_pixbuf(&pixbuf));
}

fn build_desktop_page(stack: &Stack, desktop_index: Rc<RefCell<usize>>) -> StackPage {
    let button_next = Button::builder().label("Next").build();
    button_next.add_css_class("suggested-action");
    button_next.add_css_class("pill");
    let page_box = build_box();

    let choice_sway = CheckButton::builder().label("Sway (Tiling)").build();
    let choice_wayfire = CheckButton::builder().label("Wayfire (Traditional)").build();
    choice_wayfire.set_group(Some(&choice_sway));
    choice_wayfire.set_active(true);

    let sway_box = Box::builder()
        .orientation(gtk::Orientation::Horizontal)
        .valign(gtk::Align::Center)
        .build();
    let img_sway = svg_image_from_bytes(include_bytes!("../gfx/setup-wizard-sway.svg"));
    img_sway.set_size_request(DESKTOP_IMG_WIDTH, DESKTOP_IMG_HEIGHT);
    sway_box.append(&img_sway);
    let label_sway = &Label::new(Some("<b>Sway:</b> A tiling desktop with a strong focus on keyboard shortcuts, for advanced users."));
    label_sway.set_use_markup(true);
    label_sway.set_wrap(true);
    label_sway.set_hexpand(true);
    label_sway.set_xalign(0.0);
    sway_box.append(label_sway);
    choice_sway.set_child(Some(&sway_box));

    let wayfire_box = Box::builder()
        .orientation(gtk::Orientation::Horizontal)
        .valign(gtk::Align::Center)
        .build();
    let img_wayfire = svg_image_from_bytes(include_bytes!("../gfx/setup-wizard-wayfire.svg"));
    img_wayfire.set_size_request(DESKTOP_IMG_WIDTH, DESKTOP_IMG_HEIGHT);
    wayfire_box.append(&img_wayfire);
    let label_wayfire = &Label::new(Some("<b>Wayfire:</b> A more traditional desktop with overlapping windows, suitable for beginners."));
    label_wayfire.set_use_markup(true);
    label_wayfire.set_wrap(true);
    label_wayfire.set_hexpand(true);
    label_wayfire.set_xalign(0.0);
    wayfire_box.append(label_wayfire);
    choice_wayfire.set_child(Some(&wayfire_box));

    let desktop_label = &Label::new(Some("Please select your preferred desktop:"));
    desktop_label.add_css_class(CSS_CLASS_TITLE);
    page_box.append(desktop_label);
    page_box.append(&choice_wayfire);
    page_box.append(&choice_sway);
    let error_label = Label::new(None);
    error_label.add_css_class("error");
    error_label.set_selectable(true);
    error_label.set_wrap(true);
    page_box.append(&error_label);
    page_box.append(&button_next);

    button_next.connect_clicked(
        clone!(@weak stack => move |_| {
            let mut index = desktop_index.borrow_mut();
            *index = choice_wayfire.is_active() as usize;
            stack.set_visible_child_full("root-password", gtk::StackTransitionType::SlideLeft);
        })
    );

    return stack.add_titled(&page_box, Some("desktop"), "Desktop");
}

fn build_root_pw_page(stack: &Stack) -> StackPage {
    let button_next = Button::builder().label("Next").build();
    button_next.add_css_class("suggested-action");
    button_next.add_css_class("pill");
    let page_box = build_box();

    let pw_entry = PasswordEntry::builder().build();
    let pw_entry_rep = PasswordEntry::builder().build();

    let pw_label = &Label::new(Some("Please choose a password for the root account:"));
    pw_label.add_css_class(CSS_CLASS_TITLE);
    pw_label.set_wrap(true);
    let instr_label = &Label::new(Some("The root (superuser) account can modify anything in the system. Use a strong password."));
    instr_label.set_wrap(true);
    page_box.append(pw_label);
    page_box.append(&pw_entry);
    let pw_rep_label = &Label::new(Some("And repeat it for safety:"));
    pw_rep_label.add_css_class(CSS_CLASS_TITLE);
    page_box.append(pw_rep_label);
    page_box.append(&pw_entry_rep);
    let error_label = Label::new(None);
    error_label.add_css_class("error");
    error_label.set_selectable(true);
    error_label.set_wrap(true);
    page_box.append(instr_label);
    page_box.append(&error_label);
    page_box.append(&button_next);

    button_next.connect_clicked(
        clone!(@weak stack => move |_| {
            let pw = pw_entry.text();
            let pw_retry = pw_entry_rep.text();
            if pw != pw_retry {
                error_label.set_label("Error: Passwords don't match.");
                return;
            } else if !valid_password(&pw) {
                error_label.set_label(&format!("Error: Password must have at least one character."));
                return;
            }
            match set_password("set root pw", &pw, "root") {
                Ok(_) => (),
                Err(e) => {
                    error_label.set_label(&format!("Error: Cannot set password: {}", e));
                    return
                }
            };
            stack.set_visible_child_full("hostname", gtk::StackTransitionType::SlideLeft);
        })
    );

    return stack.add_titled(&page_box, Some("root-password"), "Root Password");
}

fn build_hostname_page(stack: &Stack) -> StackPage {
    let button_next = Button::builder().label("Next").build();
    button_next.add_css_class("suggested-action");
    button_next.add_css_class("pill");
    let page_box = build_box();

    let hostname_entry = Entry::builder().build();
    let hostname_label = &Label::new(Some("Please give this computer a name:"));
    hostname_label.add_css_class(CSS_CLASS_TITLE);
    hostname_label.set_wrap(true);
    let instr_label = &Label::new(Some("Lower-case characters only."));
    instr_label.set_wrap(true);

    page_box.append(hostname_label);
    page_box.append(&hostname_entry);

    let error_label = Label::new(None);
    error_label.add_css_class("error");
    error_label.set_selectable(true);
    error_label.set_wrap(true);
    page_box.append(instr_label);
    page_box.append(&error_label);
    page_box.append(&button_next);

    button_next.connect_clicked(
        clone!(@weak stack => move |_| {
            let hostname = hostname_entry.text();
            if !valid_hostname(&hostname) {
                error_label.set_label("Error: Invalid hostname format.");
                return;
            }
            match write_file(PATH_ETC_HOSTNAME, &hostname) {
                 Ok(_) => (),
                 Err(e) => {
                     error_label.set_label(&format!("Error: Cannot write {}: {}", PATH_ETC_HOSTNAME, e));
                     return
                 }
            };
            let head_hosts = TEMPLATE_ETC_HOSTS.replace("${HOSTNAME}", &hostname);
            let hosts_file = match std::fs::File::open(PATH_ETC_HOSTS) {
                 Ok(f) => f,
                 Err(e) => {
                     error_label.set_label(&format!("Error: Cannot write {}: {}", PATH_ETC_HOSTNAME, e));
                     return
                 }
            };
            let reader = std::io::BufReader::new(hosts_file);
            let tail_hosts = match reader.lines().skip(2).
                map(|result| result.map(|line| line + "\n")).
                collect::<Result<String, std::io::Error>>() {
                 Ok(s) => s,
                 Err(e) => {
                     error_label.set_label(&format!("Error: Cannot properly read {}: {}", PATH_ETC_HOSTNAME, e));
                     return
                 }
            };
            let to_join = [head_hosts, tail_hosts];
            match write_file(PATH_ETC_HOSTS, &to_join.join("\n")) {
                 Ok(_) => (),
                 Err(e) => {
                     error_label.set_label(&format!("Error: Cannot update {}: {}", PATH_ETC_HOSTS, e));
                     return
                 }
            }
            match run_command("run hostname", "hostname", ["--file", PATH_ETC_HOSTNAME].to_vec()) {
                Ok(_) => (),
                Err(e) => {
                    error_label.set_label(&e);
                    return
                }
            };
            stack.set_visible_child_full("account", gtk::StackTransitionType::SlideLeft);
        })
    );

    return stack.add_titled(&page_box, Some("hostname"), "Computer Name");
}

fn build_account_page(stack: &Stack, desktop_index: Rc<RefCell<usize>>, window: &ApplicationWindow) -> StackPage {
    let button_next = Button::builder().label("Next").build();
    button_next.add_css_class("suggested-action");
    button_next.add_css_class("pill");
    let page_box = build_box();

    let username_entry = Entry::builder().build();
    let pw_entry = PasswordEntry::builder().build();
    let pw_entry_rep = PasswordEntry::builder().build();

    let username_label = &Label::new(Some("Please choose a username for your personal account:"));
    username_label.add_css_class(CSS_CLASS_TITLE);
    username_label.set_wrap(true);
    page_box.append(username_label);
    page_box.append(&username_entry);
    let pw_label = &Label::new(Some("And a password for your account:"));
    pw_label.add_css_class(CSS_CLASS_TITLE);
    page_box.append(pw_label);
    page_box.append(&pw_entry);
    let pw_rep_label = &Label::new(Some("And repeat it for safety:"));
    pw_rep_label.add_css_class(CSS_CLASS_TITLE);
    page_box.append(pw_rep_label);
    page_box.append(&pw_entry_rep);
    let error_label = Label::new(None);
    error_label.add_css_class("error");
    error_label.set_selectable(true);
    error_label.set_wrap(true);
    page_box.append(&error_label);
    page_box.append(&button_next);

    let cleanup_script = format!("for script in `ls -1 {}`; do \"{}/$script\"; done", PATH_CLEANUP_SCRIPTS, PATH_CLEANUP_SCRIPTS);
    button_next.connect_clicked(
        clone!(@weak window => move |_| {
            let username = username_entry.text();
            if !valid_username(&username) {
                error_label.set_label("Error: Invalid username format.");
                return;
            }
            let pw = pw_entry.text();
            let pw_retry = pw_entry_rep.text();
            if pw != pw_retry {
                error_label.set_label("Error: Passwords don't match.");
                return;
            } else if !valid_password(&pw) {
                error_label.set_label("Error: Password must have at least one character.");
                return;
            }
            match run_command("adduser", "adduser", ["--disabled-password", "--comment", "", &username].to_vec()) {
                Ok(_) => (),
                Err(e) => {
                    error_label.set_label(&e);
                    return
                }
            };
            match run_command("add to sudoers", "usermod", ["-aG", "sudo", &username].to_vec()) {
                Ok(_) => (),
                Err(e) => {
                    error_label.set_label(&e);
                    return
                }
            };
            let greetd_config = TEMPLATE_ETC_GREETD_CONFIG.replace("${DESKTOP}", &DESKTOP_BINARIES[*desktop_index.borrow()]);
            match write_file(PATH_ETC_GREETD_CONFIG, &greetd_config) {
                Ok(_) => (),
                Err(e) => {
                    error_label.set_label(&e);
                    return;
                }
            }
            match set_password("set user pw", &pw, &username) {
                Ok(_) => (),
                Err(e) => {
                    error_label.set_label(&format!("Error: Cannot set password: {}", e));
                    return
                }
            };
            match run_command("run cleanup scripts", "sh", ["-c", &cleanup_script].to_vec()) {
                Ok(_) => (),
                Err(e) => {
                    error_label.set_label(&e);
                    return
                }
            };
            let _ = window.close();
        })
    );

    return stack.add_titled(&page_box, Some("account"), "Account");
}

fn build_ui(app: &Application) {
    let header_bar = HeaderBar::default();

    let stack = Stack::builder().build();

    let toolbar_view = ToolbarView::builder()
        .content(&stack)
        .build();
    let content_page = NavigationPage::builder()
        .title("Content")
        .child(&toolbar_view)
        .build();

    // navigation sidebar
    let sidebar = StackSidebar::builder()
        .stack(&stack)
        .build();
    let nav_page = NavigationPage::builder()
        .title("Sidebar")
        .child(&sidebar)
        .build();

    // split view
    let nav_view = NavigationSplitView::builder()
        .sidebar(&nav_page)
        .content(&content_page)
        .build();

    let window = ApplicationWindow::builder()
        .application(app)
        .default_width(960)
        .default_height(540)
        .title("MNT Reform Setup Wizard")
        .child(&nav_view)
        .build();

    window.set_titlebar(Some(&header_bar));
    window.present();

    let desktop_index = Rc::new(RefCell::new(0));

    // main content stack
    let _welcome_page = build_welcome_page(&stack);
    let _keyboard_page = build_keyboard_page(&stack);
    let _time_page = build_time_page(&stack);
    let _desktop_page = build_desktop_page(&stack, desktop_index.clone());
    let _root_pw_page = build_root_pw_page(&stack);
    let _hostname_page = build_hostname_page(&stack);
    let _account_page = build_account_page(&stack, desktop_index.clone(), &window);
}
