/*
 * Main.vala
 *
 * Copyright 2012 Tony George <teejee2008@gmail.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 *
 *
 */

using GLib;
using Gtk;
using Gee;
using Json;

using TeeJee.Logging;
using TeeJee.FileSystem;
using TeeJee.Devices;
using TeeJee.JSON;
using TeeJee.ProcessManagement;
using TeeJee.GtkHelper;
using TeeJee.Multimedia;
using TeeJee.System;
using TeeJee.Misc;

public Main App;
public const string AppName = "Timeshift BTRFS";
public const string AppShortName = "timeshift-btrfs";
public const string AppVersion = "1.1.3";
public const string AppAuthor = "Tony George";
public const string AppAuthorEmail = "teejee2008@gmail.com";

const string GETTEXT_PACKAGE = "";
const string LOCALE_DIR = "/usr/share/locale";

extern void exit(int exit_code);

public class Main : GLib.Object{
	public string app_path = "";
	public string share_folder = "";
	public string app_conf_path = "";

	public Gee.ArrayList<TimeShiftBackup?> snapshot_list;
	public Gee.HashMap<string,Device> partition_map;
	public Gee.ArrayList<MountEntry> mount_list;

	public Device root_device;
	public Device snapshot_device;

	public Subvolume subvol_root;
	public Subvolume subvol_home;

	public string mount_point_app = "/mnt/timeshift-btrfs";
	public string mount_point_btrfs = "/mnt/timeshift-btrfs/btrfs";

	public DistInfo current_distro;

	public bool _is_scheduled = false;
	public bool schedule_monthly = false;
	public bool schedule_weekly = false;
	public bool schedule_daily = true;
	public bool schedule_hourly = false;
	public bool schedule_boot = true;
	public int count_monthly = 2;
	public int count_weekly = 3;
	public int count_daily = 5;
	public int count_hourly = 6;
	public int count_boot = 5;

	public string app_mode = "";
	public bool is_ondemand = false;

	public string snapshot_comments = "";
	public bool is_success = false;
	public bool in_progress = false;

	public int startup_delay_interval_mins = 10;
	public int retain_snapshots_max_days = 200;
	public int minimum_free_disk_space_mb = 2048;

	public string cmd_backup_device = "";
	public string cmd_snapshot = "";
	public bool cmd_confirm = false;
	public bool cmd_verbose = true;

	public string log_dir;
	public string log_file;
	public string lock_dir;
	public string lock_file;

	public TimeShiftBackup snapshot_to_delete;
	public TimeShiftBackup snapshot_to_restore;

	public string progress_text = "";
	public int snapshot_list_start_index = 0;

	//global vars for controlling threads
	public bool thr_success = false;
	public bool thr_running = false;
	public int thr_retval = -1;
	public string thr_arg1 = "";
	public bool thr_timeout_active = false;
	public string thr_timeout_cmd = "";

	//initialization

	public static int main (string[] args) {

		set_locale();

		//show help and exit
		if (args.length > 1) {
			switch (args[1].down()) {
				case "--help":
				case "-h":
					stdout.printf (Main.help_message ());
					return 0;
			}
		}

		//init TMP
		LOG_ENABLE = false;
		init_tmp();
		LOG_ENABLE = true;

		/*
		 * Note:
		 * init_tmp() will fail if timeshift-btrfs is run as normal user
		 * logging will be disabled temporarily so that the error is not displayed to user
		 */

		/*
		var map = PartitionInfo.get_mounted_filesystems_using_mtab();
		foreach(PartitionInfo pi in map.values){
			LOG_TIMESTAMP = false;
			log_msg(pi.description_full());
		}
		exit(0);
		*/

		App = new Main(args);
		bool success = App.start_application(args);
		App.exit_app();

		return (success) ? 0 : 1;
	}

	private static void set_locale(){
		Intl.setlocale(GLib.LocaleCategory.MESSAGES, "timeshift-btrfs");
		Intl.textdomain(GETTEXT_PACKAGE);
		Intl.bind_textdomain_codeset(GETTEXT_PACKAGE, "utf-8");
		Intl.bindtextdomain(GETTEXT_PACKAGE, LOCALE_DIR);
	}

	public Main(string[] args){
		string msg = "";

		//parse arguments (initial) ------------

		parse_arguments(args);

		//check for admin access before logging is initialized
		//since writing to log directory requires admin access

		if (!user_is_admin()){
			msg = _("TimeShift needs admin access to backup and restore system files.") + "\n";
			msg += _("Please run the application as admin (using 'sudo' or 'su')");

			LOG_TIMESTAMP = false;
			log_error(msg);

			if (app_mode == ""){
				string title = _("Admin Access Required");
				gtk_messagebox(title, msg, null, true);
			}

			exit(0);
		}

		//init log ------------------

		try {
			DateTime now = new DateTime.now_local();
			log_dir = "/var/log/timeshift-btrfs";
			log_file = log_dir + "/" + now.format("%Y-%m-%d_%H-%M-%S") + ".log";

			var file = File.new_for_path (log_dir);
			if (!file.query_exists ()) {
				file.make_directory_with_parents();
			}

			file = File.new_for_path (log_file);
			if (file.query_exists ()) {
				file.delete ();
			}

			dos_log = new DataOutputStream (file.create(FileCreateFlags.REPLACE_DESTINATION));
			if ((app_mode == "")||(LOG_DEBUG)){
				log_msg(_("Session log file") + ": %s".printf(log_file));
			}
		}
		catch (Error e) {
			is_success = false;
			log_error (e.message);
		}

		//get Linux distribution info -----------------------

		this.current_distro = DistInfo.get_dist_info("/");
		if ((app_mode == "")||(LOG_DEBUG)){
			log_msg(_("Distribution") + ": " + current_distro.full_name(),true);
		}

		//check dependencies ---------------------

		string message;
		if (!check_dependencies(out message)){
			if (app_mode == ""){
				string title = _("Missing Dependencies");
				gtk_messagebox(title, message, null, true);
			}
			exit(0);
		}

		//check and create lock ------------------

		lock_dir = "/var/run/lock/timeshift-btrfs";
		lock_file = lock_dir + "/lock";
		if (!create_lock()){
			if (app_mode == ""){
				string txt = read_file(lock_file);
				//string pid = txt.split(";")[0].strip();
				string mode = txt.split(";")[1].strip();

				if (mode == "backup"){
					msg = _("A scheduled job is currently taking a system snapshot.") + "\n";
					msg += _("Please wait for a few minutes and try again.");
				}
				else{
					msg = _("Another instance of Timeshift BTRFS is currently running!") + "\n";
					msg += _("Please check if you have multiple windows open.") + "\n";
				}

				string title = _("Error");
				gtk_messagebox(title, msg, null, true);
			}
			else{
				//already logged - do nothing
			}
			exit(0);
		}

		//initialize variables ------------------

		this.app_path = (File.new_for_path (args[0])).get_parent().get_path ();
		this.share_folder = "/usr/share";
		this.app_conf_path = "/etc/timeshift-btrfs.json";

		//check if running locally -------------

		string local_exec = args[0];
		string local_conf = app_path + "/timeshift-btrfs.json";
		string local_share = app_path + "/share";

		var f_local_exec = File.new_for_path(local_exec);
		if (f_local_exec.query_exists()){

			var f_local_conf = File.new_for_path(local_conf);
			if (f_local_conf.query_exists()){
				this.app_conf_path = local_conf;
			}

			var f_local_share = File.new_for_path(local_share);
			if (f_local_share.query_exists()){
				this.share_folder = local_share;
			}
		}
		else{
			//timeshift-btrfs is running from system directory - update app_path
			this.app_path = get_cmd_path("timeshift-btrfs");
		}

		//initialize lists -------------------------

		snapshot_list = new Gee.ArrayList<TimeShiftBackup>();
		partition_map = new Gee.HashMap<string,Device>();
		mount_list = new Gee.ArrayList<MountEntry>();

		//initialize app --------------------

		update_partition_list();

		if (root_device != null){
			log_msg(_("/ is mapped to device: %s, UUID=%s").printf(root_device.device,root_device.uuid));
		}

		if (partition_map.size == 0){
			msg = _("Timeshift could not find any BTRFS partitions on your system.\nTimeshift BTRFS can be used only with BTRFS file system.") + "\n";

			LOG_TIMESTAMP = false;
			log_error(msg);

			if (app_mode == ""){
				string title = _("BTRFS Partition Not Found");
				gtk_messagebox(title, msg, null, true);
			}

			exit(0);
		}

		subvol_root = new Subvolume("@", mount_point_btrfs + "/@");
		subvol_home = new Subvolume("@home", mount_point_btrfs + "/@home");

		//finish initialization --------------

		load_app_config();
		update_snapshot_list();
	}

	//console functions

	private void parse_arguments(string[] args){
		for (int k = 1; k < args.length; k++) // Oth arg is app path
		{
			switch (args[k].down()){
				case "--backup":
					LOG_TIMESTAMP = false;
					LOG_DEBUG = false;
					app_mode = "backup";
					break;

				case "--backup-now":
					LOG_TIMESTAMP = false;
					LOG_DEBUG = false;
					app_mode = "ondemand";
					break;

				case "--delete":
					LOG_TIMESTAMP = false;
					LOG_DEBUG = false;
					app_mode = "delete";
					break;

				case "--delete-all":
					LOG_TIMESTAMP = false;
					LOG_DEBUG = false;
					app_mode = "delete-all";
					break;

				case "--restore":
					LOG_TIMESTAMP = false;
					LOG_DEBUG = false;
					app_mode = "restore";
					break;

				case "--verbose":
					cmd_verbose = true;
					break;

				case "--quiet":
					cmd_verbose = false;
					break;

				case "--yes":
					cmd_confirm = true;
					break;

				case "--device":
					cmd_backup_device = args[++k];
					break;

				case "--snapshot":
				case "--snapshot-name":
					cmd_snapshot = args[++k];
					break;

				case "--debug":
					LOG_COMMANDS = true;
					LOG_DEBUG = true;
					break;

				case "--list":
				case "--list-snapshots":
					app_mode = "list-snapshots";
					LOG_TIMESTAMP = false;
					LOG_DEBUG = false;
					break;

				case "--list-devices":
					app_mode = "list-devices";
					LOG_TIMESTAMP = false;
					LOG_DEBUG = false;
					break;

				default:
					LOG_TIMESTAMP = false;
					log_error(_("Invalid command line arguments") + ": %s".printf(args[k]), true);
					log_msg(Main.help_message());
					exit(1);
					break;
			}
		}

		/* LOG_ENABLE = false; 		disables all console output
		 * LOG_TIMESTAMP = false;	disables the timestamp prepended to every line in terminal output
		 * LOG_DEBUG = false;		disables additional console messages
		 * LOG_COMMANDS = true;		enables printing of all commands on terminal
		 * */

		if (app_mode == ""){
			//Initialize GTK
			LOG_TIMESTAMP = true;
			//LOG_ENABLE = true;
			Gtk.init(ref args);
		}
		else{
			//LOG_ENABLE = false;
		}
	}

	public bool start_application(string[] args){
		bool is_success = true;

		switch(app_mode){
			case "backup":
			case "ondemand":
			case "restore":
			case "delete":
			case "delete-all":
			case "list-snapshots":
				//set backup device from commandline argument (if available) or prompt user (if device is null)
				get_backup_device_from_cmd(false, null);
				break;
		}

		switch(app_mode){
			case "backup":
				is_success = take_snapshot(false, "", null);
				return is_success;

			case "restore":
				is_success = restore_snapshot(null);
				return is_success;

			case "delete":
				is_success = delete_snapshot();
				return is_success;

			case "delete-all":
				is_success = delete_all_snapshots();
				return is_success;

			case "ondemand":
				is_success = take_snapshot(true,"",null);
				return is_success;

			case "list-snapshots":
				LOG_ENABLE = true;
				if (snapshot_list.size > 0){
					log_msg(_("Snapshots on device %s").printf(snapshot_device.full_name_with_alias) + ":\n");
					list_snapshots(false);
					return true;
				}
				else{
					log_msg(_("No snapshots found on device") + " '%s'".printf(snapshot_device.device));
					return false;
				}

			case "list-devices":
				LOG_ENABLE = true;
				log_msg(_("Devices with BTRFS and encrypted file systems") + ":\n");
				list_devices();
				return true;

			default:
				//Initialize main window
				var window = new MainWindow ();
				window.destroy.connect(Gtk.main_quit);
				window.show_all();

				//start event loop
				Gtk.main();

				return true;
		}
	}

	public static string help_message (){
		string msg = "\n" + AppName + " v" + AppVersion + " by Tony George (teejee2008@gmail.com)" + "\n";
		msg += "\n";
		msg += "Syntax:\n";
		msg += "\n";
		msg += "  timeshift-btrfs --list-{snapshots|devices} [OPTIONS]\n";
		msg += "  timeshift-btrfs --backup[-now] [OPTIONS]\n";
		msg += "  timeshift-btrfs --restore [OPTIONS]\n";
		msg += "  timeshift-btrfs --delete-[all] [OPTIONS]\n";
		msg += "\n";
		msg += _("Options") + ":\n";
		msg += "\n";
		msg += _("List") + ":\n";
		msg += "  --list[-snapshots]         " + _("List snapshots") + "\n";
		msg += "  --list-devices             " + _("List devices") + "\n";
		msg += "\n";
		msg += _("Backup") + ":\n";
		msg += "  --backup                   " + _("Take scheduled backup") + "\n";
		msg += "  --backup-now               " + _("Take on-demand backup") + "\n";
		msg += "\n";
		msg += _("Restore") + ":\n";
		msg += "  --restore                  " + _("Restore snapshot") + "\n";
		msg += "  --snapshot <name>          " + _("Specify snapshot to restore") + "\n";
		msg += "\n";
		msg += _("Delete") + ":\n";
		msg += "  --delete                   " + _("Delete snapshot") + "\n";
		msg += "  --delete-all               " + _("Delete all snapshots") + "\n";
		msg += "\n";
		msg += _("Global") + ":\n";
		msg += "  --device <device>          " + _("Specify BTRFS device") + "\n";
		msg += "  --debug                    " + _("Show additional debug messages") + "\n";
		msg += "  --help                     " + _("Show all options") + "\n";
		msg += "\n";
		return msg;
	}


	public void list_snapshots(bool paginate){
		int count = 0;
		for(int index = 0; index < snapshot_list.size; index++){
			if (!paginate || ((index >= snapshot_list_start_index) && (index < snapshot_list_start_index + 10))){
				count++;
			}
		}

		string[,] grid = new string[count+1,5];
		bool[] right_align = { false, false, false, false, false};

		int row = 0;
		int col = -1;
		grid[row, ++col] = _("Num");
		grid[row, ++col] = "";
		grid[row, ++col] = _("Name");
		grid[row, ++col] = _("Tags");
		grid[row, ++col] = _("Description");
		row++;

		for(int index = 0; index < snapshot_list.size; index++){
			TimeShiftBackup bak = snapshot_list[index];
			if (!paginate || ((index >= snapshot_list_start_index) && (index < snapshot_list_start_index + 10))){
				col = -1;
				grid[row, ++col] = "%d".printf(index);
				grid[row, ++col] = ">";
				grid[row, ++col] = "%s".printf(bak.name);
				grid[row, ++col] = "%s".printf(bak.taglist_short);
				grid[row, ++col] = "%s".printf(bak.description);
				row++;
			}
		}

		print_grid(grid, right_align);
	}

	public void list_devices(){
		int count = 0;
		foreach(Device pi in partition_list) {
			if (!pi.has_linux_filesystem()) { continue; }
			count++;
		}

		string[,] grid = new string[count+1,6];
		bool[] right_align = { false, false, false, true, true, false};

		int row = 0;
		int col = -1;
		grid[row, ++col] = _("Num");
		grid[row, ++col] = "";
		grid[row, ++col] = _("Device");
		//grid[row, ++col] = _("UUID");
		grid[row, ++col] = _("Size");
		grid[row, ++col] = _("Type");
		grid[row, ++col] = _("Label");
		row++;

		foreach(Device pi in partition_list) {
			if (!pi.has_linux_filesystem()) { continue; }

			col = -1;
			grid[row, ++col] = "%d".printf(row - 1);
			grid[row, ++col] = ">";
			grid[row, ++col] = "%s".printf(pi.full_name_with_alias);
			//grid[row, ++col] = "%s".printf(pi.uuid);
			grid[row, ++col] = "%s".printf((pi.size_mb > 0) ? "%s GB".printf(pi.size) : "?? GB");
			grid[row, ++col] = "%s".printf(pi.type);
			grid[row, ++col] = "%s".printf(pi.label);
			row++;
		}

		print_grid(grid, right_align);
	}

	public void print_grid(string[,] grid, bool[] right_align, bool has_header = true){
		int[] col_width = new int[grid.length[1]];

		for(int col=0; col<grid.length[1]; col++){
			for(int row=0; row<grid.length[0]; row++){
				if (grid[row,col].length > col_width[col]){
					col_width[col] = grid[row,col].length;
				}
			}
		}

		for(int row=0; row<grid.length[0]; row++){
			for(int col=0; col<grid.length[1]; col++){
				string fmt = "%" + (right_align[col] ? "+" : "-") + col_width[col].to_string() + "s  ";
				stdout.printf(fmt.printf(grid[row,col]));
			}
			stdout.printf("\n");

			if (has_header && (row == 0)){
				stdout.printf(string.nfill(78, '-'));
				stdout.printf("\n");
			}
		}
	}

	//input prompt timeout

	public void start_timeout_counter(string kill_cmd = ""){
		try {
			thr_timeout_cmd = kill_cmd;
			thr_timeout_active = true;
			Thread.create<void> (start_timeout_counter_thread, true);
		}
		catch (Error e) {
			log_error (e.message);
		}
	}

	public void start_timeout_counter_thread(){
		int count = 20 * 1000;
		while (thr_timeout_active && (count > 0)){
			Thread.usleep((ulong) GLib.TimeSpan.MILLISECOND * 500);
			count -= 500;
		}

		if (thr_timeout_active){
			thr_timeout_active = false;
			stdout.printf("\n");

			if (thr_timeout_cmd.length > 0){
				Posix.system("killall " + thr_timeout_cmd);
			}

			log_error(_("No response from user"));
			log_msg(_("Aborted."));
			exit(0);
		}
	}

	public void stop_timeout_counter(){
		thr_timeout_active = false;
	}

	//prompt for input

	public Device? read_stdin_device(Gee.ArrayList<Device> device_list){
		start_timeout_counter();
		string? line = stdin.read_line();
		stop_timeout_counter();

		line = (line != null) ? line.strip() : line;

		Device selected_device = null;

		if (line.down() == "a"){
			log_msg(_("Aborted."));
			exit_app();
			exit(0);
		}
		else if ((line == null)||(line.length == 0)){
			log_error("Invalid input");
		}
		else if (line.contains("/")){
			selected_device = get_device_from_name(device_list, line);
			if (selected_device == null){
				log_error("Invalid input");
			}
		}
		else{
			selected_device = get_device_from_index(device_list, line);
			if (selected_device == null){
				log_error("Invalid input");
			}
		}

		return selected_device;
	}

	public Device? get_device_from_index(Gee.ArrayList<Device> device_list, string index_string){
		int64 index;
		if (int64.try_parse(index_string, out index)){
			int i = -1;
			foreach(Device pi in device_list) {
				if ((pi.devtype == "partition") && !pi.has_linux_filesystem()) { continue; }
				if (++i == index){
					return pi;
				}
			}
		}

		return null;
	}

	public Device? get_device_from_name(Gee.ArrayList<Device> device_list, string device_name){
		foreach(Device pi in device_list) {
			if (!pi.has_linux_filesystem()) { continue; }
			if ((pi.device == device_name)||(pi.device.replace("/dev/","") == device_name)){
				return pi;
			}
			else {
				foreach(string symlink in pi.symlinks){
					if ((symlink == device_name)||(symlink.replace("/dev/","").replace("/dev/mapper/","") == device_name)){
						return pi;
					}
				}
			}
		}

		return null;
	}

	public TimeShiftBackup read_stdin_snapshot(){
		start_timeout_counter();
		string? line = stdin.read_line();
		stop_timeout_counter();

		line = (line != null) ? line.strip() : line;

		TimeShiftBackup selected_snapshot = null;

		if (line.down() == "a"){
			log_msg("Aborted.");
			exit_app();
			exit(0);
		}
		else if (line.down() == "p"){
			snapshot_list_start_index -= 10;
			if (snapshot_list_start_index < 0){
				snapshot_list_start_index = 0;
			}
			log_msg("");
			list_snapshots(true);
			log_msg("");
		}
		else if (line.down() == "n"){
			if ((snapshot_list_start_index + 10) < snapshot_list.size){
				snapshot_list_start_index += 10;
			}
			log_msg("");
			list_snapshots(true);
			log_msg("");
		}
		else if (line.contains("_")||line.contains("-")){
			//TODO
			log_error("Invalid input");
		}
		else if ((line == null)||(line.length == 0)){
			log_error("Invalid input");
		}
		else{
			int64 index;
			if (int64.try_parse(line, out index)){
				if (index < snapshot_list.size){
					selected_snapshot = snapshot_list[(int) index];
				}
				else{
					log_error("Invalid input");
				}
			}
			else{
				log_error("Invalid input");
			}
		}

		return selected_snapshot;
	}

	public bool read_stdin_restore_confirm(){
		start_timeout_counter();
		string? line = stdin.read_line();
		stop_timeout_counter();

		line = (line != null) ? line.strip() : line;

		if ((line.down() == "a")||(line.down() == "n")){
			log_msg("Aborted.");
			exit_app();
			exit(0);
			return true;
		}
		else if ((line == null)||(line.length == 0)){
			log_error("Invalid input");
			return false;
		}
		else if (line.down() == "y"){
			cmd_confirm = true;
			return true;
		}
		else if ((line == null)||(line.length == 0)){
			log_error("Invalid input");
			return false;
		}
		else{
			log_error("Invalid input");
			return false;
		}
	}

	//utility methods

	public bool check_dependencies(out string msg){
		msg = "";

		string[] dependencies = { "btrfs", "mkfs.btrfs", "/sbin/blkid","df","mount","umount","fuser","crontab","cp","rm","touch","ln","sync"}; //"shutdown","chroot",

		string path;
		foreach(string cmd_tool in dependencies){
			path = get_cmd_path (cmd_tool);
			if ((path == null) || (path.length == 0)){
				msg += " * " + cmd_tool + "\n";
			}
		}

		if (msg.length > 0){
			msg = _("Commands listed below are not available on this system") + ":\n\n" + msg + "\n";
			msg += _("Please install required packages and try running Timeshift again");
			log_error(msg);
			return false;
		}
		else{
			return true;
		}
	}

	public string snapshot_dir {
		owned get{
			return "%s/timeshift-btrfs/snapshots".printf(mount_point_btrfs);
		}
	}

	public bool create_lock(){
		try{

			string current_pid = ((long)Posix.getpid()).to_string();

			var file = File.new_for_path (lock_dir);
			if (!file.query_exists()) {
				file.make_directory_with_parents();
			}

			file = File.new_for_path (lock_file);
			if (file.query_exists()) {

				string txt = read_file(lock_file);
				string process_id = txt.split(";")[0].strip();
				//string mode = txt.split(";")[1].strip();
				long pid = long.parse(process_id);

				if (process_is_running(pid)){
					log_msg(_("Another instance of Timeshift is currently running") + " (PID=%ld)".printf(pid));
					return false;
				}
				else{
					log_msg(_("Warning: Deleted invalid lock"));
					file.delete();
					write_file(lock_file, current_pid);
					return true;
				}
			}
			else{
				write_file(lock_file, current_pid + ";" + app_mode);
				return true;
			}
		}
		catch (Error e) {
			log_error (e.message);
			return false;
		}
	}

	public void remove_lock(){
		try{
			var file = File.new_for_path (lock_file);
			if (file.query_exists()) {
				file.delete();
			}
		}
		catch (Error e) {
			log_error (e.message);
		}
	}

	public bool is_scheduled{
		get{
			return _is_scheduled;
		}
		set{
			_is_scheduled = value;
		}
	}

	//backup

	public bool take_snapshot(bool ondemand, string comments, Gtk.Window? parent_win){
		bool status;
		bool update_symlinks = false;

		is_ondemand = ondemand;
		snapshot_comments = comments;

		//create a timestamp
		DateTime now = new DateTime.now_local();

		//mount device
		if (!mount_backup_device(parent_win)){
			return false;
		}

		//check device
		string msg;
		int status_code = check_backup_device(out msg);
		if (status_code != 0){
			log_error(msg);
			return false;
		}

		//check subvolume layout
		bool ok = check_btrfs_layout_on_snapshot_device();
		if (!ok){
			return false;
		}

		//create snapshot root if missing
		create_dir(snapshot_dir);

		//ondemand
		if (is_ondemand){
			is_success = backup_and_rotate ("ondemand",now);
			if(!is_success){
				log_error(_("On-demand snapshot failed!"));
				return false;
			}
			else{
				update_symlinks = true;
			}
		}
		else if (is_scheduled){
			TimeShiftBackup last_snapshot_boot = get_latest_snapshot("boot");
			TimeShiftBackup last_snapshot_hourly = get_latest_snapshot("hourly");
			TimeShiftBackup last_snapshot_daily = get_latest_snapshot("daily");
			TimeShiftBackup last_snapshot_weekly = get_latest_snapshot("weekly");
			TimeShiftBackup last_snapshot_monthly = get_latest_snapshot("monthly");

			DateTime dt_sys_boot = now.add_seconds((-1) * get_system_uptime_seconds());
			bool take_new = false;

			if (schedule_boot){

				log_msg(_("Boot snapshots are enabled"));

				if (last_snapshot_boot == null){
					log_msg(_("Last boot snapshot not found"));
					take_new = true;
				}
				else if (last_snapshot_boot.date.compare(dt_sys_boot) < 0){
					log_msg(_("Last boot snapshot is older than system start time"));
					take_new = true;
				}
				else{
					int hours = (int) ((float) now.difference(last_snapshot_boot.date) / TimeSpan.HOUR);
					log_msg(_("Last boot snapshot is %d hours old").printf(hours));
					take_new = false;
				}

				if (take_new){
					status = backup_and_rotate ("boot",now);
					if(!status){
						log_error(_("Boot snapshot failed!"));
						return false;
					}
					else{
						update_symlinks = true;
					}
				}
			}

			if (schedule_hourly){

				log_msg(_("Hourly snapshots are enabled"));

				if (last_snapshot_hourly == null){
					log_msg(_("Last hourly snapshot not found"));
					take_new = true;
				}
				else if (last_snapshot_hourly.date.compare(now.add_hours(-1).add_minutes(1)) < 0){
					log_msg(_("Last hourly snapshot is more than 1 hour old"));
					take_new = true;
				}
				else{
					int mins = (int) ((float) now.difference(last_snapshot_hourly.date) / TimeSpan.MINUTE);
					log_msg(_("Last hourly snapshot is %d minutes old").printf(mins));
					take_new = false;
				}

				if (take_new){
					status = backup_and_rotate ("hourly",now);
					if(!status){
						log_error(_("Hourly snapshot failed!"));
						return false;
					}
					else{
						update_symlinks = true;
					}
				}
			}

			if (schedule_daily){

				log_msg(_("Daily snapshots are enabled"));

				if (last_snapshot_daily == null){
					log_msg(_("Last daily snapshot not found"));
					take_new = true;
				}
				else if (last_snapshot_daily.date.compare(now.add_days(-1).add_minutes(1)) < 0){
					log_msg(_("Last daily snapshot is more than 1 day old"));
					take_new = true;
				}
				else{
					int hours = (int) ((float) now.difference(last_snapshot_daily.date) / TimeSpan.HOUR);
					log_msg(_("Last daily snapshot is %d hours old").printf(hours));
					take_new = false;
				}

				if (take_new){
					status = backup_and_rotate ("daily",now);
					if(!status){
						log_error(_("Daily snapshot failed!"));
						return false;
					}
					else{
						update_symlinks = true;
					}
				}
			}

			if (schedule_weekly){

				log_msg(_("Weekly snapshots are enabled"));

				if (last_snapshot_weekly == null){
					log_msg(_("Last weekly snapshot not found"));
					take_new = true;
				}
				else if (last_snapshot_weekly.date.compare(now.add_weeks(-1).add_minutes(1)) < 0){
					log_msg(_("Last weekly snapshot is more than 1 week old"));
					take_new = true;
				}
				else{
					int days = (int) ((float) now.difference(last_snapshot_weekly.date) / TimeSpan.DAY);
					log_msg(_("Last weekly snapshot is %d days old").printf(days));
					take_new = false;
				}

				if (take_new){
					status = backup_and_rotate ("weekly",now);
					if(!status){
						log_error(_("Weekly snapshot failed!"));
						return false;
					}
					else{
						update_symlinks = true;
					}
				}
			}

			if (schedule_monthly){

				log_msg(_("Monthly snapshot are enabled"));

				if (last_snapshot_monthly == null){
					log_msg(_("Last monthly snapshot not found"));
					take_new = true;
				}
				else if (last_snapshot_monthly.date.compare(now.add_months(-1).add_minutes(1)) < 0){
					log_msg(_("Last monthly snapshot is more than 1 month old"));
					take_new = true;
				}
				else{
					int days = (int) ((float) now.difference(last_snapshot_monthly.date) / TimeSpan.DAY);
					log_msg(_("Last monthly snapshot is %d days old").printf(days));
					take_new = false;
				}

				if (take_new){
					status = backup_and_rotate ("monthly",now);
					if(!status){
						log_error(_("Monthly snapshot failed!"));
						return false;
					}
					else{
						update_symlinks = true;
					}
				}
			}
		}
		else{
			log_msg(_("Scheduled snapshots are disabled"));
			log_msg(_("Nothing to do!"));
			cron_job_update();
		}

		auto_delete_backups();

		if (update_symlinks){
			update_snapshot_list();
			create_symlinks();
		}

		return true;
	}

	public bool backup_and_rotate(string tag, DateTime dt_created){
		string cmd = "";
		string std_out;
		string std_err;
		int ret_val;
		string msg;

		string time_stamp = dt_created.format("%Y-%m-%d_%H-%M-%S");
		DateTime now = new DateTime.now_local();
		bool backup_taken = false;

		string snapshot_name = time_stamp;
		string snapshot_path = snapshot_dir + "/" + snapshot_name;

		try{
			DateTime dt_sys_boot = now.add_seconds((-1) * get_system_uptime_seconds());

			//check if we can rotate an existing backup -------------

			TimeShiftBackup last_snapshot = get_latest_snapshot();
			DateTime dt_filter = null;

			if ((tag != "ondemand") && (last_snapshot != null)){
				switch(tag){
					case "boot":
						dt_filter = dt_sys_boot;
						break;
					case "hourly":
					case "daily":
					case "weekly":
					case "monthly":
						dt_filter = now.add_hours(-1);
						break;
					default:
						log_error(_("Unknown snapshot type") + ": %s".printf(tag));
						return false;
				}

				TimeShiftBackup backup_to_rotate = null;
				foreach(TimeShiftBackup bak in snapshot_list){
					if (bak.date.compare(dt_filter) > 0){
						backup_to_rotate = bak;
						break;
					}
				}

				if (backup_to_rotate != null){
					backup_to_rotate.add_tag(tag);
					backup_taken = true;
					msg = _("Snapshot") + " '%s' ".printf(backup_to_rotate.name) + _("tagged") + " '%s'".printf(tag);
					log_msg(msg);
				}
			}

			if (!backup_taken){

				//take new backup ---------------------------------

				DateTime dt_begin = new DateTime.now_local();

				progress_text = _("Creating snapshot...");
				log_msg(progress_text);

				//create snapshot directory
				if (!dir_exists(snapshot_path)){
					create_dir(snapshot_path);
				}

				foreach(string subvol_name in new string[] { "@","@home" }){
					//create subvolume snapshot
					cmd = "btrfs subvolume snapshot '%s/%s' '%s/%s' \n".printf(mount_point_btrfs, subvol_name, snapshot_path, subvol_name);
					if (LOG_COMMANDS) { log_debug(cmd); }
					Process.spawn_command_line_sync(cmd, out std_out, out std_err, out ret_val);
					if (ret_val != 0){
						log_error (std_err);
						log_error(_("btrfs returned an error") + ": %d".printf(ret_val));
						log_error(_("Failed to create subvolume snapshot") + ": %s".printf(subvol_name));
						return false;
					}
				}

				//save control file
				write_snapshot_control_file(snapshot_path, dt_created, tag);

				//display elapsed time ----------

				DateTime dt_end = new DateTime.now_local();
				TimeSpan elapsed = dt_end.difference(dt_begin);
				long seconds = (long)(elapsed * 1.0 / TimeSpan.SECOND);
				msg = _("Snapshot saved successfully") + " (%lds)".printf(seconds);
				log_msg(msg);
				notify_send("TimeShift",msg,10000,"low");

				log_msg(_("Snapshot") + " '%s' ".printf(time_stamp) + _("tagged") + " '%s'".printf(tag));

				update_snapshot_list();
			}
		}
		catch(Error e){
			log_error (e.message);
			return false;
		}

		return true;
	}

	public void auto_delete_backups(){
		DateTime now = new DateTime.now_local();
		int count = 0;
		bool show_msg = false;
		DateTime dt_limit;

		//delete older backups - boot ---------------

		var list = get_snapshot_list("boot");

		if (list.size > count_boot){
			log_msg(_("Maximum backups exceeded for backup level") + " '%s'".printf("boot"));
			while (list.size > count_boot){
				list[0].remove_tag("boot");
				log_msg(_("Snapshot") + " '%s' ".printf(list[0].name) + _("un-tagged") + " '%s'".printf("boot"));
				list = get_snapshot_list("boot");
			}
		}

		//delete older backups - hourly, daily, weekly, monthly ---------

		string[] levels = { "hourly","daily","weekly","monthly" };

		foreach(string level in levels){
			list = get_snapshot_list(level);

			if (list.size == 0) { continue; }

			switch (level){
				case "hourly":
					dt_limit = now.add_hours(-1 * count_hourly);
					break;
				case "daily":
					dt_limit = now.add_days(-1 * count_daily);
					break;
				case "weekly":
					dt_limit = now.add_weeks(-1 * count_weekly);
					break;
				case "monthly":
					dt_limit = now.add_months(-1 * count_monthly);
					break;
				default:
					dt_limit = now.add_years(-1 * 10);
					break;
			}

			if (list[0].date.compare(dt_limit) < 0){

				log_msg(_("Maximum backups exceeded for backup level") + " '%s'".printf(level));

				while (list[0].date.compare(dt_limit) < 0){
					list[0].remove_tag(level);
					log_msg(_("Snapshot") + " '%s' ".printf(list[0].name) + _("un-tagged") + " '%s'".printf(level));
					list = get_snapshot_list(level);
				}
			}
		}

		//delete older backups - max days -------

		show_msg = true;
		count = 0;
		foreach(TimeShiftBackup bak in snapshot_list){
			if (bak.date.compare(now.add_days(-1 * retain_snapshots_max_days)) < 0){
				if (!bak.has_tag("ondemand")){

					if (show_msg){
						log_msg(_("Removing backups older than") + " %d ".printf(retain_snapshots_max_days) + _("days..."));
						show_msg = false;
					}

					log_msg(_("Snapshot") + " '%s' ".printf(bak.name) + _("un-tagged"));
					bak.tags.clear();
					count++;
				}
			}
		}

		delete_untagged_snapshots();

		//delete older backups - minimum space -------

		update_partition_list();

		show_msg = true;
		count = 0;
		while ((snapshot_device.size_mb - snapshot_device.used_mb) < minimum_free_disk_space_mb){
			list = get_snapshot_list();
			if (list.size > 0){
				if (!list[0].has_tag("ondemand")){

					if (show_msg){
						log_msg(_("Free space is less than") + " %d GB".printf(minimum_free_disk_space_mb));
						log_msg(_("Removing older backups to free disk space"));
						show_msg = false;
					}

					delete_snapshot(list[0]);
				}
			}
			update_partition_list(); //TODO: update snapshot_device only
		}
	}

	public void delete_untagged_snapshots(){
		bool show_msg = true;

		foreach(TimeShiftBackup bak in snapshot_list){
			if (bak.tags.size == 0){

				if (show_msg){
					log_msg(_("Removing un-tagged snapshots..."));
					show_msg = false;
				}

				delete_snapshot(bak);
			}
		}
	}

	public void create_symlinks(){
		string cmd = "";
		string std_out;
		string std_err;
		int ret_val;

		try{
			cleanup_symlink_dir("boot");
			cleanup_symlink_dir("hourly");
			cleanup_symlink_dir("daily");
			cleanup_symlink_dir("weekly");
			cleanup_symlink_dir("monthly");
			cleanup_symlink_dir("ondemand");

			string path;

			foreach(TimeShiftBackup bak in snapshot_list){
				foreach(string tag in bak.tags){
					path = mount_point_btrfs + "/timeshift-btrfs/snapshots-%s".printf(tag);
					cmd = "ln --symbolic \"../snapshots/%s\" -t \"%s\"".printf(bak.name, path);

					if (LOG_COMMANDS) { log_debug(cmd); }

					Process.spawn_command_line_sync(cmd, out std_out, out std_err, out ret_val);
					if (ret_val != 0){
						log_error (std_err);
						log_error(_("Failed to create symlinks") + ": snapshots-%s".printf(tag));
						return;
					}
				}
			}

			log_debug (_("Symlinks updated"));
		}
		catch (Error e) {
	        log_error (e.message);
	    }
	}

	public void cleanup_symlink_dir(string tag){
		string cmd = "";
		string std_out;
		string std_err;
		int ret_val;

		try{
			string path = mount_point_btrfs + "/timeshift-btrfs/snapshots-%s".printf(tag);
			var f = File.new_for_path(path);
			if (f.query_exists()){
				cmd = "rm -rf \"%s\"".printf(path + "/");

				if (LOG_COMMANDS) { log_debug(cmd); }

				Process.spawn_command_line_sync(cmd, out std_out, out std_err, out ret_val);
				if (ret_val != 0){
					log_error (std_err);
					log_error(_("Failed to delete symlinks") + ": 'snapshots-%s'".printf(tag));
					return;
				}
			}

			f.make_directory_with_parents();
		}
		catch (Error e) {
	        log_error (e.message);
	    }
	}

	public TimeShiftBackup write_snapshot_control_file(string snapshot_path, DateTime dt_created, string tag, string comments = "", bool is_live = false){
		var ctl_path = snapshot_path + "/info.json";
		var config = new Json.Object();

		config.set_string_member("created", dt_created.to_utc().to_unix().to_string());
		config.set_string_member("sys-uuid", (root_device == null) ? "" : root_device.uuid);
		config.set_string_member("sys-distro", current_distro.full_name());
		config.set_string_member("app-version", AppVersion);
		config.set_string_member("tags", tag);
		config.set_string_member("comments", comments);
		config.set_string_member("live", is_live.to_string());

		var json = new Json.Generator();
		json.pretty = true;
		json.indent = 2;
		var node = new Json.Node(NodeType.OBJECT);
		node.set_object(config);
		json.set_root(node);

		try{
			var f = File.new_for_path(ctl_path);
			if (f.query_exists()){
				f.delete();
			}
			json.to_file(ctl_path);
		} catch (Error e) {
	        log_error (e.message);
	    }

	    return (new TimeShiftBackup(snapshot_path));
	}

	//restore

	public void get_backup_device_from_cmd(bool prompt_if_empty, Gtk.Window? parent_win){
		if (cmd_backup_device.length > 0){
			//set backup device from command line argument
			Device cmd_dev = get_device_from_name(partition_list, cmd_backup_device);
			if (cmd_dev != null){
				snapshot_device = cmd_dev;
				if (!mount_backup_device(parent_win)){
					exit_app();
					exit(1);
				}
				update_snapshot_list();
			}
			else{
				log_error(_("Could not find device") + ": '%s'".printf(cmd_backup_device));
				exit_app();
				exit(1);
			}
		}
		else{
			if ((snapshot_device == null) || (prompt_if_empty && (snapshot_list.size == 0))){
				//prompt user for backup device
				log_msg("");

				log_msg(TERM_COLOR_YELLOW + _("Select backup device") + ":\n" + TERM_COLOR_RESET);
				list_devices();
				log_msg("");

				snapshot_device = null;
				while (snapshot_device == null){
					stdout.printf(TERM_COLOR_YELLOW + _("Enter device name or number (a=Abort)") + ": " + TERM_COLOR_RESET);
					stdout.flush();
					snapshot_device = read_stdin_device(partition_list);
				}

				log_msg("");

				if (snapshot_device != null){
					mount_backup_device(parent_win);
					update_snapshot_list();
				}
			}
		}
	}

	public bool is_current_system_restore{
		get {
			if ((root_device != null) && (snapshot_device.uuid == root_device.uuid)){
				return true;
			}
			else{
				return false;
			}
		}
	}

	public bool restore_snapshot(Gtk.Window? parent_win){
		string cmd = "";
		string std_out;
		string std_err;
		int ret_val;
		bool found = false;

		//set snapshot device -----------------------------------------------

		if (snapshot_device != null){
			//print snapshot_device name
			log_msg(TERM_COLOR_YELLOW + string.nfill(78, '*') + TERM_COLOR_RESET);
			log_msg(_("Backup Device") + ": %s".printf(snapshot_device.device), true);
			log_msg(TERM_COLOR_YELLOW + string.nfill(78, '*') + TERM_COLOR_RESET);
			mount_backup_device(parent_win);
			update_snapshot_list();
		}
		else{
			//print error
			log_error(_("Backup device not specified!"));
			return false;
		}

		//set snapshot -----------------------------------------------

		if (app_mode != ""){ //command-line mode

			if (cmd_snapshot.length > 0){

				//check command line arguments
				found = false;
				foreach(TimeShiftBackup bak in snapshot_list) {
					if (bak.name == cmd_snapshot){
						snapshot_to_restore = bak;
						found = true;
						break;
					}
				}

				//check if found
				if (!found){
					log_error(_("Could not find snapshot") + ": '%s'".printf(cmd_snapshot));
					return false;
				}
			}

			//prompt user for snapshot
			if (snapshot_to_restore == null){

				if (snapshot_list.size == 0){
					log_error(_("No snapshots found on device") + ": '%s'".printf(snapshot_device.device));
					return false;
				}

				log_msg("");
				log_msg(TERM_COLOR_YELLOW + _("Select snapshot to restore") + ":\n" + TERM_COLOR_RESET);
				list_snapshots(true);
				log_msg("");

				while (snapshot_to_restore == null){
					stdout.printf(TERM_COLOR_YELLOW + _("Enter snapshot number (a=Abort, p=Previous, n=Next)") + ": " + TERM_COLOR_RESET);
					stdout.flush();
					snapshot_to_restore = read_stdin_snapshot();
				}
				log_msg("");
			}
		}

		if (snapshot_to_restore != null){
			//print snapshot name
			log_msg(TERM_COLOR_YELLOW + string.nfill(78, '*') + TERM_COLOR_RESET);
			log_msg(_("Snapshot") + ": %s ~ %s".printf(snapshot_to_restore.name, snapshot_to_restore.description), true);
			log_msg(TERM_COLOR_YELLOW + string.nfill(78, '*') + TERM_COLOR_RESET);
		}
		else{
			//print error
			log_error(_("Snapshot to restore not specified!"));
			return false;
		}

		query_subvolume_info();

		DateTime now = new DateTime.now_local();
		string time_stamp = now.format("%Y-%m-%d_%H-%M-%S");
		string snapshot_name = time_stamp;
		string snapshot_path = snapshot_dir + "/" + snapshot_name;

		try{
			//check if current system is being restored
			bool restore_current_system;
			if ((root_device != null) && ((snapshot_device.device == root_device.device) || (snapshot_device.uuid == root_device.uuid))){
				restore_current_system = true;
			}
			else{
				restore_current_system = false;
			}

			//pre-restore backup ---------------------------------------------------

			/* Note:
			 * The @ and @home subvolumes need to be backed-up only if they are in-use by the system.
			 * If user restores a snapshot and then tries to restore another snapshot before the next reboot
			 * then the @ and @home subvolumes are the ones that were previously restored and need to be deleted.
			 * */

			bool create_pre_restore_backup = false;

			if (restore_current_system){
				//check for an existing pre-restore backup ------------

				//search
				TimeShiftBackup bak_pre_restore = null;
				found = false;
				foreach(TimeShiftBackup bak in snapshot_list){
					if (bak.is_live){
						found = true;
						bak_pre_restore = bak;
						break;
					}
				}

				if (found){
					//delete system subvolumes
					subvol_root.remove();
					subvol_home.remove();

					//update description for pre-restore backup
					bak_pre_restore.description = "Before restoring '%s'".printf(snapshot_to_restore.date_formatted);
					bak_pre_restore.update_control_file();
				}
				else{
					create_pre_restore_backup = true;
				}
			}
			else{
				create_pre_restore_backup = true;
			}

			if (create_pre_restore_backup){

				//create pre-restore backup ------------------

				//create snapshot directory
				if (!dir_exists(snapshot_path)){
					create_dir(snapshot_path);
				}

				//move subvolumes
				bool no_subvolumes_found = true;
				foreach(string subvol_name in new string[] { "@","@home" }){
					string subvol_path = "%s/%s".printf(mount_point_btrfs, subvol_name);
					if (dir_exists(subvol_path)){
						no_subvolumes_found = false;

						cmd = "mv '%s/%s' '%s/%s'".printf(mount_point_btrfs, subvol_name, snapshot_path, subvol_name);
						if (LOG_COMMANDS) { log_debug(cmd); }
						Process.spawn_command_line_sync(cmd, out std_out, out std_err, out ret_val);
						if (ret_val != 0){
							log_error (std_err);
							log_error(_("Failed to move subvolume") + ": %s".printf(subvol_name));
							return false;
						}
					}
				}

				if (no_subvolumes_found){
					//could not find system subvolumes for backing up(!)
					file_delete(snapshot_path); //cleanup and ignore
				}
				else{
					//create new control file
					var bak_pre_restore = write_snapshot_control_file(snapshot_path, now, "ondemand");
					bak_pre_restore.description = "Before restoring '%s'".printf(snapshot_to_restore.date_formatted);
					if (restore_current_system){
						bak_pre_restore.is_live = true;
					}
					bak_pre_restore.update_control_file();
					update_snapshot_list();
				}
			}

			//restore snapshot subvolumes by creating new subvolume snapshots ------------------

			foreach(string subvol_name in new string[] { "@","@home" }){
				string subvol_path = "%s/%s".printf(snapshot_to_restore.path, subvol_name);
				if (dir_exists(subvol_path)){
					cmd = "btrfs subvolume snapshot '%s/%s' '%s/%s'".printf(snapshot_to_restore.path, subvol_name, mount_point_btrfs, subvol_name);
					if (LOG_COMMANDS) { log_debug(cmd); }
					Process.spawn_command_line_sync(cmd, out std_out, out std_err, out ret_val);
					if (ret_val != 0){
						log_error (std_err);
						log_error(_("btrfs returned an error") + ": %d".printf(ret_val));
						log_error(_("Failed to restore subvolume") + ": %s".printf(subvol_name));
						return false;
					}
				}
			}

			log_msg(_("Restore completed without errors"));
			if (restore_current_system){
				log_msg(_("Snapshot will become active after system is rebooted."));
			}
			return true;
		}
		catch(Error e){
			log_error (e.message);
			return false;
		}
		finally{
			snapshot_to_restore = null;
		}
	}

	//delete

	public bool delete_snapshot(TimeShiftBackup? snapshot = null){
		bool found = false;
		snapshot_to_delete = snapshot;

		//set snapshot -----------------------------------------------

		if (app_mode != ""){ //command-line mode

			if (cmd_snapshot.length > 0){

				//check command line arguments
				found = false;
				foreach(TimeShiftBackup bak in snapshot_list) {
					if (bak.name == cmd_snapshot){
						snapshot_to_delete = bak;
						found = true;
						break;
					}
				}

				//check if found
				if (!found){
					log_error(_("Could not find snapshot") + ": '%s'".printf(cmd_snapshot));
					return false;
				}
			}

			//prompt user for snapshot
			if (snapshot_to_delete == null){

				if (snapshot_list.size == 0){
					log_msg(_("No snapshots found on device") + " '%s'".printf(snapshot_device.device));
					return false;
				}

				log_msg("");
				log_msg(TERM_COLOR_YELLOW + _("Select snapshot to delete") + ":\n" + TERM_COLOR_RESET);
				list_snapshots(true);
				log_msg("");

				while (snapshot_to_delete == null){
					stdout.printf(TERM_COLOR_YELLOW + _("Enter snapshot number (a=Abort, p=Previous, n=Next)") + ": " + TERM_COLOR_RESET);
					stdout.flush();
					snapshot_to_delete = read_stdin_snapshot();
				}
				log_msg("");
			}
		}

		if (snapshot_to_delete == null){
			//print error
			log_error(_("Snapshot to delete not specified!"));
			return false;
		}

		try {
			in_progress = true;
			is_success = false;
			Thread.create<void> (delete_snapshot_thread, true);
		} catch (ThreadError e) {
			in_progress = false;
			is_success = false;
			log_error (e.message);
		}

		while (in_progress){
			gtk_do_events ();
			Thread.usleep((ulong) GLib.TimeSpan.MILLISECOND * 100);
		}

		snapshot_to_delete = null;

		return is_success;
	}

	public void delete_snapshot_thread(){
		in_progress = true;
		is_success = snapshot_to_delete.remove();
		in_progress = false;
	}

	public bool delete_all_snapshots(){
		bool success;
		string timeshift_dir = mount_point_btrfs + "/timeshift-btrfs";

		if (dir_exists(timeshift_dir)){
			//delete snapshots
			foreach(TimeShiftBackup bak in snapshot_list){
				success = delete_snapshot(bak);
				if (!success){ return success; }
			}

			//delete /timeshift-btrfs directory
			return delete_directory(timeshift_dir);
		}
		else{
			log_msg(_("No snapshots found on device") + " '%s'".printf(snapshot_device.device));
			return true;
		}
	}

	public bool delete_directory(string dir_path){
		thr_arg1 = dir_path;

		try {
			thr_running = true;
			thr_success = false;
			Thread.create<void> (delete_directory_thread, true);
		} catch (ThreadError e) {
			thr_running = false;
			thr_success = false;
			log_error (e.message);
		}

		while (thr_running){
			gtk_do_events ();
			Thread.usleep((ulong) GLib.TimeSpan.MILLISECOND * 100);
		}

		thr_arg1 = null;

		return thr_success;
	}

	public void delete_directory_thread(){
		string cmd = "";
		string std_out;
		string std_err;
		int ret_val;

		try{
			var f = File.new_for_path(thr_arg1);
			if(f.query_exists()){
				cmd = "rm -rf \"%s\"".printf(thr_arg1);

				if (LOG_COMMANDS) { log_debug(cmd); }

				Process.spawn_command_line_sync(cmd, out std_out, out std_err, out ret_val);

				if (ret_val != 0){
					log_error(_("Failed to remove") + ": '%s'".printf(thr_arg1));
					thr_success = false;
					thr_running = false;
					return;
				}
				else{
					log_msg(_("Removed") + ": '%s'".printf(thr_arg1));
					thr_success = true;
					thr_running = false;
					return;
				}
			}
			else{
				log_error(_("Directory not found") + ": '%s'".printf(thr_arg1));
				thr_success = true;
				thr_running = false;
			}
		}
		catch(Error e){
			log_error (e.message);
			thr_success = false;
			thr_running = false;
			return;
		}
	}

	//config

	public void save_app_config(){
		var config = new Json.Object();
		//config.set_string_member("enabled", is_scheduled.to_string());

		config.set_string_member("backup_device_uuid", (snapshot_device == null) ? "" : snapshot_device.uuid);

		config.set_string_member("is_scheduled", is_scheduled.to_string());

		config.set_string_member("schedule_monthly", schedule_monthly.to_string());
		config.set_string_member("schedule_weekly", schedule_weekly.to_string());
		config.set_string_member("schedule_daily", schedule_daily.to_string());
		config.set_string_member("schedule_hourly", schedule_hourly.to_string());
		config.set_string_member("schedule_boot", schedule_boot.to_string());

		config.set_string_member("count_monthly", count_monthly.to_string());
		config.set_string_member("count_weekly", count_weekly.to_string());
		config.set_string_member("count_daily", count_daily.to_string());
		config.set_string_member("count_hourly", count_hourly.to_string());
		config.set_string_member("count_boot", count_boot.to_string());

		config.set_string_member("max_days", retain_snapshots_max_days.to_string());
		config.set_string_member("min_space", minimum_free_disk_space_mb.to_string());

		var json = new Json.Generator();
		json.pretty = true;
		json.indent = 2;
		var node = new Json.Node(NodeType.OBJECT);
		node.set_object(config);
		json.set_root(node);

		try{
			json.to_file(this.app_conf_path);
		} catch (Error e) {
	        log_error (e.message);
	    }

	    if ((app_mode == "")||(LOG_DEBUG)){
			log_msg(_("App config saved") + ": '%s'".printf(this.app_conf_path));
		}
	}

	public void load_app_config(){
		var f = File.new_for_path(this.app_conf_path);
		if (!f.query_exists()) { return; }

		var parser = new Json.Parser();
        try{
			parser.load_from_file(this.app_conf_path);
		} catch (Error e) {
	        log_error (e.message);
	    }
        var node = parser.get_root();
        var config = node.get_object();

        string uuid = json_get_string(config,"backup_device_uuid","");

        foreach(Device pi in partition_list){
			if (pi.uuid == uuid){
				snapshot_device = pi;
				break;
			}
		}

		if (cmd_backup_device.length > 0){
			Device cmd_dev = get_device_from_name(partition_list, cmd_backup_device);
			if (cmd_dev != null){
				snapshot_device = cmd_dev;
			}
			else{
				log_error(_("Could not find device") + ": '%s'".printf(cmd_backup_device));
				exit_app();
				exit(1);
			}
		}

		if ((uuid.length == 0) || (snapshot_device == null)){
			if ((root_device != null) && (app_mode == "")){
				log_msg (_("Warning: Backup device not set! Defaulting to system device"));
				snapshot_device = root_device;
			}
		}

		/* The backup device specified in config file will be mounted at this point if:
		 * 1) app is running in GUI mode, OR
		 * 2) app is running command mode without backup device argument
		 * */

		if (snapshot_device != null){
			if ((app_mode == "") || (cmd_backup_device.length == 0)){
				if (mount_backup_device(null)){
					update_partition_list();
				}
				else{
					snapshot_device = null;
				}
			}
		}

		this.is_scheduled = json_get_bool(config,"is_scheduled", is_scheduled);

        this.schedule_monthly = json_get_bool(config,"schedule_monthly",schedule_monthly);
		this.schedule_weekly = json_get_bool(config,"schedule_weekly",schedule_weekly);
		this.schedule_daily = json_get_bool(config,"schedule_daily",schedule_daily);
		this.schedule_hourly = json_get_bool(config,"schedule_hourly",schedule_hourly);
		this.schedule_boot = json_get_bool(config,"schedule_boot",schedule_boot);

		this.count_monthly = json_get_int(config,"count_monthly",count_monthly);
		this.count_weekly = json_get_int(config,"count_weekly",count_weekly);
		this.count_daily = json_get_int(config,"count_daily",count_daily);
		this.count_hourly = json_get_int(config,"count_hourly",count_hourly);
		this.count_boot = json_get_int(config,"count_boot",count_boot);

		this.retain_snapshots_max_days = json_get_int(config,"max_days",retain_snapshots_max_days);
		this.minimum_free_disk_space_mb = json_get_int(config,"min_space",minimum_free_disk_space_mb);

		if ((app_mode == "")||(LOG_DEBUG)){
			log_msg(_("App config loaded") + ": '%s'".printf(this.app_conf_path));
		}
	}

	//core

	public bool update_snapshot_list(){

		snapshot_list.clear();

		string path = mount_point_btrfs + "/timeshift-btrfs/snapshots";

		if (!dir_exists(path)){
			return false;
		}

		try{
			var dir = File.new_for_path (path);
			var enumerator = dir.enumerate_children ("*", 0);

			var info = enumerator.next_file ();
			while (info != null) {
				if (info.get_file_type() == FileType.DIRECTORY) {
					TimeShiftBackup bak = new TimeShiftBackup(path + "/" + info.get_name());
					if (bak.is_valid){
						snapshot_list.add(bak);
					}
				}
				info = enumerator.next_file ();
			}
		}
		catch(Error e){
			log_error (e.message);
			return false;
		}

		//reset the 'live' flag if required
		DateTime dt_boot = new DateTime.now_local();
		dt_boot = dt_boot.add_seconds(-1.0 * get_system_uptime_seconds());
		foreach(TimeShiftBackup bak in snapshot_list){
			if (bak.is_live){
				if ((root_device == null)||(root_device.uuid != bak.sys_uuid)){
					//we are accessing the snapshot from some other system
					bak.is_live = false;
					bak.update_control_file();
				}
				else{
					 if (bak.date.difference(dt_boot) < 0){
						//snapshot was created before the last reboot
						bak.is_live = false;
						bak.update_control_file();
					}
					else{
						//do nothing, snapshot is still in use by system
					}
				}
			}
		}

		//sort
		snapshot_list.sort((a,b) => {
			TimeShiftBackup t1 = (TimeShiftBackup) a;
			TimeShiftBackup t2 = (TimeShiftBackup) b;
			return t1.date.compare(t2.date);
		});

		return true;
	}

	public Gee.ArrayList<Device> partition_list {
		owned get{
			var list = new Gee.ArrayList<Device>();
			foreach(Device pi in partition_map.values) {
				list.add(pi);
			}
			list.sort((a,b) => {
					Device p1 = (Device) a;
					Device p2 = (Device) b;

					return strcmp(p1.device,p2.device);
				});
			return list;
		}
	}

	public void update_partition_list(){
		partition_map.clear();
		partition_map = new Gee.HashMap<string,Device>();

		var map_all = Device.get_filesystems();
		if (map_all.size == 0){
			log_error(_("Failed to get partition list."));
			return;
		}

		foreach(Device pi in map_all.values){
			if ((pi.type == "btrfs")||(pi.type == "luks")){
				if (pi.mount_points.contains("/")){
					root_device = pi;
				}
				if ((snapshot_device != null) && (pi.uuid == snapshot_device.uuid)){
					snapshot_device = pi;
				}
				if (pi.is_mounted){
					pi.dist_info = DistInfo.get_dist_info(pi.mount_points[0]).full_name();
				}

				partition_map.set(pi.uuid, pi);
			}
		}
	}

	public Gee.ArrayList<TimeShiftBackup?> get_snapshot_list(string tag = ""){
		var list = new Gee.ArrayList<TimeShiftBackup?>();

		foreach(TimeShiftBackup bak in snapshot_list){
			if (tag == "" || bak.has_tag(tag)){
				list.add(bak);
			}
		}
		list.sort((a,b) => {
			TimeShiftBackup t1 = (TimeShiftBackup) a;
			TimeShiftBackup t2 = (TimeShiftBackup) b;
			return (t1.date.compare(t2.date));
		});

		return list;
	}

	public TimeShiftBackup? get_latest_snapshot(string tag = ""){
		var list = get_snapshot_list(tag);

		if (list.size > 0)
			return list[list.size - 1];
		else
			return null;
	}

	public TimeShiftBackup? get_live_snapshot(){
		foreach(TimeShiftBackup bak in snapshot_list){
			if (bak.is_live){
				return bak;
			}
		}

		return null;
	}

	public TimeShiftBackup? get_oldest_snapshot(string tag = ""){
		var list = get_snapshot_list(tag);

		if (list.size > 0)
			return list[0];
		else
			return null;
	}

	public bool mount_backup_device(Gtk.Window? parent_win){
		if (snapshot_device != null){

			//unlock if required
			if (snapshot_device.type == "luks"){
				snapshot_device = unlock_and_find_device(snapshot_device, parent_win);
				if (snapshot_device == null){
					log_error(_("Backup device not found"));
					unmount_backup_device(); //unmount any previous device
					return false;
				}
			}

			if (snapshot_device.type == "btrfs"){
				if (!Device.get_mount_points(snapshot_device.uuid).contains(mount_point_btrfs)){ //if device is not mounted	already
					//unmount any previous device
					unmount_backup_device();

					//mount
					check_and_create_dir_with_parents(mount_point_btrfs);
					if (mount(snapshot_device.uuid, mount_point_btrfs, "")){
						if ((app_mode == "")||(LOG_DEBUG)){
							log_msg(_("Mounted backup device '%s' at '%s'").printf(snapshot_device.device, mount_point_btrfs));
						}
						update_partition_list(); //update free space info
						return true;
					}
					else{
						return false;
					}
				}
				else{
					return true;
				}
			}
		}

		unmount_backup_device(); //unmount any previous device
		return false;
	}

	public void unmount_backup_device(bool exit_on_error = true){
		if (unmount_device(mount_point_btrfs, false)){
			if (dir_exists(mount_point_btrfs) && (get_file_count(mount_point_btrfs) == 0)){
				file_delete(mount_point_btrfs);
				log_debug(_("Removed mount directory: '%s'").printf(mount_point_btrfs));
			}
		}
	}

	public bool unmount_device(string mount_point, bool exit_on_error = true){
		bool is_unmounted = unmount(mount_point);
		if (!is_unmounted){
			if (exit_on_error){
				if (app_mode == ""){
					string title = _("Critical Error");
					string msg = _("Failed to unmount device!") + "\n" + _("Application will exit");
					gtk_messagebox(title, msg, null, true);
				}
				exit_app();
				exit(0);
			}
		}
		return is_unmounted;
	}

	public string unlock_encrypted_device(Device dev, Gtk.Window? parent_win){
		string mapped_name = "%s_unlocked".printf(dev.name);
		string[] name_list = { "%s_unlocked".printf(dev.name), "%s_crypt".printf(dev.name), "luks-%s".printf(dev.uuid)};

		if ((parent_win == null)&&(app_mode != "")){

			//check if unlocked
			foreach(string name in name_list){
				if (device_exists("/dev/mapper/%s".printf(name))){
					//already unlocked
					log_msg(_("Unlocked device is mapped to '%s'").printf(name));
					log_msg("");
					return name;
				}
			}

			start_timeout_counter("cryptsetup");

			//prompt user to unlock
			string cmd = "cryptsetup luksOpen '%s' '%s'".printf(dev.device, mapped_name);
			int retval = Posix.system(cmd);
			stop_timeout_counter();
			log_msg("");

			switch (retval){
				case 512: //invalid passphrase
					log_error(_("Wrong Passphrase") + ": " + _("Failed to unlock device"));
					return ""; //return
				case 1280: //already unlocked and mapped to unknown name
					log_error(_("Encrypted device '%s' is already unlocked and mapped to another device name. Select the unlocked device instead of selecting the encrypted device.").printf(dev.device));
					break;
				case 0: //success
					log_msg(_("Unlocked device is mapped to '%s'").printf(mapped_name));
					break;
				default: //unknown error
					log_error(_("Failed to unlock device"));
					return ""; //return
			}

			update_partition_list();
			return mapped_name;
		}
		else{
			//check if unlocked
			foreach(string name in name_list){
				if (device_exists("/dev/mapper/%s".printf(name))){
					//already unlocked
					//gtk_messagebox(_("Encrypted Device"),_("Unlocked device is mapped to '%s'.").printf(name), parent_win);
					return name;
				}
			}

			//prompt user to unlock
			string passphrase = gtk_inputbox(_("Encrypted Device"), _("Enter passphrase to unlock '%s'").printf(dev.name), parent_win, true);
			string cmd = "echo '%s' | cryptsetup luksOpen '%s' '%s'".printf(passphrase, dev.device, mapped_name);
			int retval = execute_script_sync(cmd, false);
			log_debug("cryptsetup:" + retval.to_string());

			switch(retval){
				case 512: //invalid passphrase
					gtk_messagebox(_("Wrong Passphrase"),_("Wrong Passphrase") + ": " + _("Failed to unlock device"), parent_win);
					return ""; //return
				case 1280: //already unlocked and mapped to unknown name
					gtk_messagebox(_("Unlocked Device"),_("Encrypted device '%s' is already unlocked and mapped to another device name. Select the unlocked device instead of selecting the encrypted device.").printf(dev.device), parent_win);
					break;
				case 0: //success
					gtk_messagebox(_("Unlocked Successfully"),_("Unlocked device is mapped to '%s'.").printf(mapped_name), parent_win);
					break;
				default: //unknown error
					gtk_messagebox(_("Error"),_("Failed to unlock device"), parent_win, true);
					return ""; //return
			}

			update_partition_list();
			return mapped_name;
		}
	}

	public Device? unlock_and_find_device(Device enc_dev, Gtk.Window? parent_win){
		if (enc_dev.type == "luks"){
			string mapped_name = unlock_encrypted_device(enc_dev, parent_win);
			string mapped_device = "/dev/mapper/%s".printf(mapped_name);
			if (mapped_name.length == 0) { return null; }
			enc_dev = null;

			//find unlocked device
			if (mapped_name.length > 0){
				foreach(Device dev in partition_list){
					foreach(string sym in dev.symlinks){
						if (sym == mapped_device){
							return dev;
						}
					}
				}
			}

			return null;
		}
		else{
			return enc_dev;
		}
	}

	public int check_backup_device(out string message){

		/*
		-1 - device un-available
		 0 - device available
		*/

		int status_code = 0;

		//free space message
		if ((snapshot_device != null) && (snapshot_device.free.length > 0)){
			message = "%s ".printf(snapshot_device.free) + _("free");
			message = message.strip();
		}
		else{
			message = "";
		}

		if (!backup_device_online()){
			if (snapshot_device == null){
				message = _("Backup device not selected");
			}
			else{
				message = _("Backup device not available");
			}

			status_code = -1;
		}
		else{
			if (snapshot_device.size_mb == 0){
				message = _("Backup device not available");
				status_code = -1;
			}
		}

		log_debug("Checked backup device (status=%d)".printf(status_code));

		return status_code;
	}

	public bool backup_device_online(){
		if (snapshot_device != null){
			if (Device.get_mount_points(snapshot_device.uuid).size > 0){
				return true;
			}
		}
		return false;
	}

	public void clean_logs(){

		Gee.ArrayList<string> list = new Gee.ArrayList<string>();

		try{
			var dir = File.new_for_path (log_dir);
			var enumerator = dir.enumerate_children ("*", 0);

			var info = enumerator.next_file ();
			string path;

			while (info != null) {
				if (info.get_file_type() == FileType.REGULAR) {
					path = log_dir + "/" + info.get_name();
					if (path != log_file) {
						list.add(path);
					}
				}
				info = enumerator.next_file ();
			}

			CompareDataFunc<string> compare_func = (a, b) => {
				return strcmp(a,b);
			};

			list.sort((owned) compare_func);

			if (list.size > 500){
				for(int k=0; k<100; k++){
					var file = File.new_for_path (list[k]);
					if (file.query_exists()){
						file.delete();
					}
				}
				log_msg(_("Older log files removed"));
			}
		}
		catch(Error e){
			log_error (e.message);
		}
	}

	public void exit_app (){

		foreach(TimeShiftBackup bak in snapshot_list){
			bak.update_control_file();
		}

		save_app_config();

		cron_job_update();

		unmount_backup_device(false);

		clean_logs();
		remove_lock();

		//Gtk.main_quit ();
	}

	//cron jobs

	public void cron_job_update(){
		string current_entry = "";
		string new_entry = "";
		bool new_entry_exists = false;
		string search_string = "";

		//scheduled job ----------------------------------

		new_entry = get_crontab_entry_scheduled();
		new_entry_exists = false;

		//check and remove crontab entries created by previous versions of timeshift-btrfs

		search_string = "*/30 * * * * timeshift-btrfs --backup";
		current_entry = crontab_read_entry(search_string);
		if (current_entry.length > 0) {
			//remove entry
			crontab_remove_job(current_entry);
		}

		//check for regular entries
		foreach(string interval in new string[] {"@monthly","@weekly","@daily","@hourly"}){

			search_string = "%s timeshift-btrfs --backup".printf(interval);

			//read
			current_entry = crontab_read_entry(search_string);

			if (current_entry.length == 0) { continue; } //not found

			//check
			if (current_entry == new_entry){
				//keep entry
				new_entry_exists = true;
			}
			else{
				//remove current entry
				crontab_remove_job(current_entry);
			}
		}

		//add new entry if missing
		if (!new_entry_exists && new_entry.length > 0){
			crontab_add_job(new_entry);
		}

		//boot job ----------------------------------

		search_string = """@reboot sleep [0-9]*m && timeshift-btrfs --backup""";

		new_entry = get_crontab_entry_boot();
		new_entry_exists = false;

		//read
		current_entry = crontab_read_entry(search_string, true);

		if (current_entry.length > 0) {
			//check
			if (current_entry == new_entry){
				//keep entry
				new_entry_exists = true;
			}
			else{
				//remove current entry
				crontab_remove_job(current_entry);
			}
		}

		//add new entry if missing
		if (!new_entry_exists && new_entry.length > 0){
			crontab_add_job(new_entry);
		}
	}

	private string get_crontab_entry_scheduled(){
		if (is_scheduled && (snapshot_list.size > 0)){
			if (schedule_hourly){
				return "@hourly timeshift-btrfs --backup";
			}
			else if (schedule_daily){
				return "@daily timeshift-btrfs --backup";
			}
			else if (schedule_weekly){
				return "@weekly timeshift-btrfs --backup";
			}
			else if (schedule_monthly){
				return "@monthly timeshift-btrfs --backup";
			}
		}

		return "";
	}

	private string get_crontab_entry_boot(){
		if (is_scheduled && (snapshot_list.size > 0)){
			if (schedule_boot || schedule_hourly || schedule_daily || schedule_weekly || schedule_monthly){
				return "@reboot sleep %dm && timeshift-btrfs --backup".printf(startup_delay_interval_mins);
			}
		}

		return "";
	}

	private bool crontab_add_job(string entry){
		if (crontab_add(entry)){
			log_msg(_("Cron job added") + ": %s".printf(entry));
			return true;
		}
		else {
			log_error(_("Failed to add cron job"));
			return false;
		}
	}

	private bool crontab_remove_job(string search_string){
		if (crontab_remove(search_string)){
			log_msg(_("Cron job removed") + ": %s".printf(search_string));
			return true;
		}
		else{
			log_error(_("Failed to remove cron job"));
			return false;
		}
	}

	//btrfs

	public bool check_btrfs_layout_on_snapshot_device(){
		//check if snapshot device is a BTRFS volume
		if ((snapshot_device != null) && (snapshot_device.type == "btrfs")){
			//check subvolume layout
			if (check_btrfs_volume(mount_point_btrfs) == false){
				string msg = _("BTRFS partition '%s' has an unsupported subvolume layout.").printf(snapshot_device.device) + "\n";
				msg += _("Only ubuntu-type layouts with @ and @home subvolumes are currently supported.");
				string title = _("Not Supported");

				if (app_mode == ""){
					gtk_messagebox(title, msg, null, true);
				}
				else{
					log_error(msg);
				}
				return false;
			}
		}

		return true;
	}

	public bool check_btrfs_volume(string mount_path){
		return dir_exists(mount_point_btrfs + "/@") && dir_exists(mount_point_btrfs + "/@home");
	}

	public void query_subvolume_info(){
		try {
			in_progress = true;
			is_success = false;
			Thread.create<void> (query_subvolume_info_thread, true);
		} catch (ThreadError e) {
			in_progress = false;
			is_success = false;
			log_error (e.message);
		}

		while (in_progress){
			gtk_do_events ();
			Thread.usleep((ulong) GLib.TimeSpan.MILLISECOND * 100);
		}
	}

	public void query_subvolume_info_thread(){
		in_progress = true;

		//query IDs
		bool ok = query_subvolume_ids();
		if (!ok){
			is_success = false;
			in_progress = false;
			return;
		}

		//query quota
		ok = query_subvolume_quota();
		if (!ok){
			//try enabling quota
			ok = enable_subvolume_quota();
			if (!ok){
				is_success = false;
				in_progress = false;
				return;
			}

			//query quota again
			ok = query_subvolume_quota();
			if (!ok){
				is_success = false;
				in_progress = false;
				return;
			}
		}

		is_success = true;
		in_progress = false;
		return;
	}

	public bool query_subvolume_ids(){
		string cmd = "";
		string std_out;
		string std_err;
		int ret_val;

		try{
			cmd = "btrfs subvolume list '%s'".printf(mount_point_btrfs);
			if (LOG_COMMANDS) { log_debug(cmd); }
			Process.spawn_command_line_sync(cmd, out std_out, out std_err, out ret_val);
			if (ret_val != 0){
				log_error (std_err);
				log_error(_("btrfs returned an error") + ": %d".printf(ret_val));
				log_error(_("Failed to query subvolume list"));
				return false;
			}
			else{
				/* Sample Output:
				 *
				ID 257 gen 56 top level 5 path timeshift-btrfs/snapshots/2014-09-26_23-34-08/@
			    ID 258 gen 52 top level 5 path timeshift-btrfs/snapshots/2014-09-26_23-34-08/@home
				* */

				foreach(string line in std_out.split("\n")){
					if (line == null) { continue; }

					string[] parts = line.split(" ");
					if (parts.length < 2) { continue; }

					Subvolume subvol = null;

					if (line.has_suffix(subvol_root.path.replace(mount_point_btrfs + "/"," "))){
						subvol = subvol_root;
					}
					else if (line.has_suffix(subvol_home.path.replace(mount_point_btrfs + "/"," "))){
						subvol = subvol_home;
					}
					else {
						foreach(TimeShiftBackup bak in snapshot_list){
							foreach(Subvolume sub in bak.subvolumes.values){
								if (line.has_suffix(sub.path.replace(mount_point_btrfs + "/",""))){
									subvol = sub;
								}
							}
						}
					}

					if (subvol != null){
						subvol.id = long.parse(parts[1]);
					}
				}

				return true;
			}
		}
		catch(Error e){
			log_error (e.message);
		}

		return false;
	}

	public bool query_subvolume_quota(){
		string cmd = "";
		string std_out;
		string std_err;
		int ret_val;

		try{
			cmd = "btrfs qgroup show '%s'".printf(mount_point_btrfs);
			if (LOG_COMMANDS) { log_debug(cmd); }
			Process.spawn_command_line_sync(cmd, out std_out, out std_err, out ret_val);
			if (ret_val != 0){
				log_error (std_err);
				log_error(_("btrfs returned an error") + ": %d".printf(ret_val));
				log_error(_("Failed to query subvolume quota"));
				return false;
			}
			else {
				/* Sample Output:
				 *
				qgroupid rfer       excl
				-------- ----       ----
				0/5      106496     106496
				0/257    3825262592 557056
				0/258    12689408   49152
				 * */

				foreach(string line in std_out.split("\n")){
					if (line == null) { continue; }

					string[] parts = line.split(" ");
					if (parts.length < 3) { continue; }
					if (parts[0].split("/").length < 2) { continue; }

					int subvol_id = int.parse(parts[0].split("/")[1]);

					Subvolume subvol = null;

					if (subvol_root.id == subvol_id){
						subvol = subvol_root;
					}
					else if (subvol_home.id == subvol_id){
						subvol = subvol_home;
					}
					else {
						foreach(TimeShiftBackup bak in snapshot_list){
							foreach(Subvolume sub in bak.subvolumes.values){
								if (sub.id == subvol_id){
									subvol = sub;
								}
							}
						}
					}

					if (subvol != null){
						int part_num = -1;
						foreach(string part in parts){
							if (part.strip().length > 0){
								part_num ++;
								switch (part_num){
									case 1:
										subvol.total_bytes = int64.parse(part);
										break;
									case 2:
										subvol.unshared_bytes = int64.parse(part);
										break;
									default:
										//ignore
										break;
								}
							}
						}
					}
				}

				foreach(TimeShiftBackup bak in snapshot_list){
					bak.update_control_file();
				}

				log_msg(_("Checked subvolume quota"));
				return true;
			}
		}
		catch(Error e){
			log_error (e.message);
		}

		return false;
	}

	public bool enable_subvolume_quota(){
		string cmd = "";
		string std_out;
		string std_err;
		int ret_val;

		try{
			cmd = "btrfs quota enable '%s'".printf(mount_point_btrfs);
			if (LOG_COMMANDS) { log_debug(cmd); }
			Process.spawn_command_line_sync(cmd, out std_out, out std_err, out ret_val);
			if (ret_val != 0){
				log_error (std_err);
				log_error(_("btrfs returned an error") + ": %d".printf(ret_val));
				log_error(_("Failed to enable subvolume quota"));
				return false;
			}
			else{
				log_msg(_("Enabled subvolume quota support"));
				return true;
			}
		}
		catch (Error e) {
			log_error (e.message);
			return false;
		}
	}

	public bool rescan_subvolume_quota(){
		string cmd = "";
		string std_out;
		string std_err;
		int ret_val;

		try{
			cmd = "btrfs quota rescan '%s'".printf(mount_point_btrfs);
			if (LOG_COMMANDS) { log_debug(cmd); }
			Process.spawn_command_line_sync(cmd, out std_out, out std_err, out ret_val);
			if (ret_val != 0){
				log_error (std_err);
				log_error(_("btrfs returned an error") + ": %d".printf(ret_val));
				log_error(_("Failed to rescan subvolume quota"));
				return false;
			}

			return true;
		}
		catch (Error e) {
			log_error (e.message);
			return false;
		}
	}
}

public class TimeShiftBackup : GLib.Object{
	public string path = "";
	public string name = "";
	public DateTime date;
	public string sys_uuid = "";
	public string sys_distro = "";
	public string app_version = "";
	public string description = "";

	public Gee.ArrayList<string> tags;
	public Gee.ArrayList<string> exclude_list;
	public Gee.HashMap<string,Subvolume> subvolumes;

	public bool is_valid = true;
	public bool is_live = false;

	public TimeShiftBackup(string dir_path){

		try{
			var f = File.new_for_path(dir_path);
			var info = f.query_info("*", FileQueryInfoFlags.NONE);

			path = dir_path;
			name = info.get_name();
			description = "";

			date = new DateTime.from_unix_utc(0); //1970
			tags = new Gee.ArrayList<string>();
			exclude_list = new Gee.ArrayList<string>();
			subvolumes = new Gee.HashMap<string,Subvolume>();

			foreach(string subvol_name in new string[]{ "@","@home" }){
				string subvol_path = path + "/" + subvol_name;
				f = File.new_for_path(subvol_path);
				if (f.query_exists()) {
					subvolumes.set(subvol_name, new Subvolume(subvol_name, subvol_path));
				}
			}

			read_control_file();
		}
		catch(Error e){
			log_error (e.message);
		}
	}

	public string taglist{
		owned get{
			string str = "";
			foreach(string tag in tags){
				str += " " + tag;
			}
			return str.strip();
		}
		set{
			tags.clear();
			foreach(string tag in value.split(" ")){
				if (!tags.contains(tag.strip())){
					tags.add(tag.strip());
				}
			}
		}
	}

	public string taglist_short{
		owned get{
			string str = "";
			foreach(string tag in tags){
				str += " " + tag.replace("ondemand","O").replace("boot","B").replace("hourly","H").replace("daily","D").replace("weekly","W").replace("monthly","M");
			}
			return str.strip();
		}
	}

	public string date_formatted{
		owned get{
			return date.format("%Y-%m-%d %I:%M %p");
		}
	}

	public string total_formatted{
		owned get{
			int64 size = 0;
			foreach(Subvolume subvol in subvolumes.values){
				size += subvol.total_bytes;
			}
			return format_file_size_auto(size);
		}
	}

	public string unshared_formatted{
		owned get{
			int64 size = 0;
			foreach(Subvolume subvol in subvolumes.values){
				size += subvol.unshared_bytes;
			}
			return format_file_size_auto(size);
		}
	}

	public void add_tag(string tag){
		if (!tags.contains(tag.strip())){
			tags.add(tag.strip());
			update_control_file();
		}
	}

	public void remove_tag(string tag){
		if (tags.contains(tag.strip())){
			tags.remove(tag.strip());
			update_control_file();
		}
	}

	public bool has_tag(string tag){
		return tags.contains(tag.strip());
	}

	public void read_control_file(){
		string ctl_file = path + "/info.json";

		var f = File.new_for_path(ctl_file);
		if (f.query_exists()) {
			var parser = new Json.Parser();
			try{
				parser.load_from_file(ctl_file);
			} catch (Error e) {
				log_error (e.message);
			}
			var node = parser.get_root();
			var config = node.get_object();

			if ((node == null)||(config == null)){
				is_valid = false;
				return;
			}

			string val = json_get_string(config,"created","");
			if (val.length > 0) {
				DateTime date_utc = new DateTime.from_unix_utc(int64.parse(val));
				date = date_utc.to_local();
			}

			sys_uuid = json_get_string(config,"sys-uuid","");
			sys_distro = json_get_string(config,"sys-distro","");
			taglist = json_get_string(config,"tags","");
			description = json_get_string(config,"comments","");
			app_version = json_get_string(config,"app-version","");
			is_live = json_get_bool(config,"live",false);

			if (config.has_member("subvolumes")){
				var subvols = (Json.Object) config.get_object_member("subvolumes");
				foreach(string subvol_name in new string[] { "@","@home" }){
					if (subvols.has_member(subvol_name) && subvolumes.has_key(subvol_name)){
						var subvolume = subvolumes.get(subvol_name);
						int index = -1;
						foreach(Json.Node jnode in subvols.get_array_member(subvol_name).get_elements()) {
							string item = jnode.get_string();
							switch (++index){
								case 0:
									subvolume.name = item;
									break;
								case 1:
									subvolume.id = long.parse(item);
									break;
								case 2:
									subvolume.total_bytes = int64.parse(item);
									break;
								case 3:
									subvolume.unshared_bytes = int64.parse(item);
									break;
							}
						}
					}
				}
			}
		}
	}

	public void update_control_file(){

		try{
			string ctl_file = path + "/info.json";
			var f = File.new_for_path(ctl_file);
			if (f.query_exists()) {
				var parser = new Json.Parser();
				try{
					parser.load_from_file(ctl_file);
				} catch (Error e) {
					log_error (e.message);
				}
				var node = parser.get_root();
				var config = node.get_object();

				config.set_string_member("tags", taglist);
				config.set_string_member("comments", description);
				config.set_string_member("live", is_live.to_string());

				var subvols = new Json.Object();
				config.set_object_member("subvolumes",subvols);
				foreach(Subvolume subvol in subvolumes.values){
					Json.Array arr = new Json.Array();
					arr.add_string_element(subvol.name);
					arr.add_string_element(subvol.id.to_string());
					arr.add_string_element(subvol.total_bytes.to_string());
					arr.add_string_element(subvol.unshared_bytes.to_string());
					subvols.set_array_member(subvol.name,arr);
				}

				var json = new Json.Generator();
				json.pretty = true;
				json.indent = 2;
				node.set_object(config);
				json.set_root(node);
				f.delete();
				json.to_file(ctl_file);
			}
		} catch (Error e) {
			log_error (e.message);
		}
	}

	public bool remove(){
		string cmd = "";
		string std_out;
		string std_err;
		int ret_val;

		try{
			var f = File.new_for_path(path);
			if (f.query_exists()){
				//delete subvolumes
				foreach(Subvolume subvolume in subvolumes.values){
					bool ok = subvolume.remove();
					if (!ok) { return false; }
				}

				//delete snapshot directory
				cmd = "rm -rf '%s'".printf(path);
				if (LOG_COMMANDS) { log_debug(cmd); }
				Process.spawn_command_line_sync(cmd, out std_out, out std_err, out ret_val);
				if (ret_val != 0){
					log_error(_("Failed to delete snapshot directory") + ": '%s'".printf(name));
					return false;
				}
				else{
					log_msg(_("Snapshot deleted") + ": '%s'".printf(name));
					return true;
				}
			}
			else{
				return false;
			}
		}
		catch(Error e){
			log_error (e.message);
			return false;
		}
	}
}

public class AppExcludeEntry : GLib.Object{
	public string relpath = "";
	public bool is_include = false;
	public bool is_file = false;
	public bool enabled = false;

	public AppExcludeEntry(string _relpath, bool _is_file, bool _is_include = false){
		relpath = _relpath;
		is_file = _is_file;
		is_include = _is_include;
	}

	public string pattern(bool root_home = false){
		string str = (is_include) ? "+ " : "";
		str += (root_home) ? "/root" : "/home/*";
		str += relpath[1:relpath.length];
		str += (is_file) ? "" : "/**";
		return str.strip();
	}

}

public class BootDeviceEntry : GLib.Object{
	public string device = "";
	public string description = "";

	public BootDeviceEntry(string _device, string _description){
		this.device = _device;
		this.description = _description;
	}
}

public class Subvolume : GLib.Object{
	public string name = "";
	public string path = "";
	public long id = -1;
	public int64 total_bytes = 0;
	public int64 unshared_bytes = 0;

	public Subvolume(string name, string path){
		this.name = name;
		this.path = path;
	}

	public string total_formatted{
		owned get{
			return format_file_size_auto(total_bytes);
		}
	}

	public string unshared_formatted{
		owned get{
			return format_file_size_auto(unshared_bytes);
		}
	}

	public bool remove(){
		string cmd = "";
		string std_out;
		string std_err;
		int ret_val;

		try{
			if (dir_exists(path)){
				cmd = "btrfs subvolume delete '%s'".printf(path);
				if (LOG_COMMANDS) { log_debug(cmd); }
				Process.spawn_command_line_sync(cmd, out std_out, out std_err, out ret_val);
				if (ret_val != 0){
					log_error(_("Failed to delete snapshot subvolume") + ": '%s'".printf(path));
					return false;
				}

				if (id > 0){
					cmd = "btrfs qgroup destroy 0/%ld '%s'".printf(id, App.mount_point_btrfs);
					if (LOG_COMMANDS) { log_debug(cmd); }
					Process.spawn_command_line_sync(cmd, out std_out, out std_err, out ret_val);
					if (ret_val != 0){
						log_error(_("Failed to destroy qgroup") + ": '0/%ld'".printf(id));
						return false;
					}
				}

				return true;
			}
			else{
				return false;
			}
		}
		catch(Error e){
			log_error (e.message);
			return false;
		}
	}
}
