
/*
 * Device.vala
 *
 * Copyright 2012-2018 Tony George <teejeetech@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.
 *
 *
 */
 
/* Functions and classes for handling disk partitions */

using TeeJee.Logging;
using TeeJee.FileSystem;
using TeeJee.ProcessHelper;
using TeeJee.GtkHelper;

public class Device : GLib.Object{

	/* Class for storing disk information */

	public static double KB = 1000;
	public static double MB = 1000 * KB;
	public static double GB = 1000 * MB;

	public static double KiB = 1024;
	public static double MiB = 1024 * KiB;
	public static double GiB = 1024 * MiB;
	
	public string device = "";
	public string name = "";
	public string kname = "";
	public string pkname = "";
	public string pkname_toplevel = "";
	public string mapped_name = "";
	public string uuid = "";
	public string label = "";
	public string partuuid = "";
	public string partlabel = "";
	
	public int major = -1;
	public int minor = -1;

	public string device_mapper = "";
	public string device_by_uuid = "";
	public string device_by_label = "";
	public string device_by_partuuid = "";  // gpt only
	public string device_by_partlabel = ""; // gpt only

	public string type = ""; // disk, part, crypt, loop, rom, lvm
	public string fstype = ""; // iso9660, ext4, btrfs, ...

	public int order = -1;

	public string vendor = "";
	public string model = "";
	public string serial = "";
	public string revision = "";

	public bool removable = false;
	public bool read_only = false;
	
	public uint64 size_bytes = 0;
	public uint64 used_bytes = 0;
	public uint64 available_bytes = 0;
	
	public string used_percent = "";
	public string dist_info = "";
	public Gee.ArrayList<MountEntry> mount_points;
	public Gee.ArrayList<string> symlinks;

	public Device parent = null;
	public Gee.ArrayList<Device> children = null;

	private static string lsblk_version = "";
	private static bool lsblk_is_ancient = false;
	
	private static Gee.ArrayList<Device> device_list;

	public Device(){
		mount_points = new Gee.ArrayList<MountEntry>();
		symlinks = new Gee.ArrayList<string>();
		children = new Gee.ArrayList<Device>();

		test_lsblk_version();
	}

	public static void test_lsblk_version(){

		if ((lsblk_version != null) && (lsblk_version.length > 0)){
			return;
		}

		string std_out, std_err;
		int status = exec_sync("lsblk --bytes --pairs --output HOTPLUG,PKNAME,VENDOR,SERIAL,REV", out std_out, out std_err);
		if (status == 0){
			lsblk_version = std_out;
			lsblk_is_ancient = false;
		}
		else{
			lsblk_version = "ancient";
			lsblk_is_ancient = true;
		}
	}
	
	public uint64 free_bytes{
		get{
			return (used_bytes == 0) ? 0 : (size_bytes - used_bytes);
		}
	}

	public string size{
		owned get{
			if (size_bytes < GB){
				return "%.1f MB".printf(size_bytes / MB);
			}
			else if (size_bytes > 0){
				return "%.1f GB".printf(size_bytes / GB);
			} 
			else{
				return "";
			}
		}
	}

	public string used{
		owned get{
			return (used_bytes == 0) ? "" : "%.1f GB".printf(used_bytes / GB);
		}
	}

	public string free{
		owned get{
			return (free_bytes == 0) ? "" : "%.1f GB".printf(free_bytes / GB);
		}
	}

	public bool is_mounted{
		get{
			return (mount_points.size > 0);
		}
	}

	public bool is_mounted_at_path(string subvolname, string mount_path){
		
		foreach (var mnt in mount_points){
			if (mnt.mount_point == mount_path){
				if (subvolname.length == 0){
					return true;
				}
				else if (mnt.mount_options.contains("subvol=%s".printf(subvolname))
					|| mnt.mount_options.contains("subvol=/%s".printf(subvolname))){

					return true;
				}
			}
		}
		
		return false;
	}

	public bool has_linux_filesystem(){
		switch (fstype){
			case "ext2":
			case "ext3":
			case "ext4":
			case "f2fs":
			case "reiserfs":
			case "reiser4":
			case "xfs":
			case "jfs":
			case "zfs":
			case "zfs_member":
			case "btrfs":
			case "lvm":
			case "lvm2":
			case "lvm2_member":
			case "luks":
			case "crypt":
			case "crypto_luks":
				return true;
			default:
				return false;
		}
	}

	public bool is_encrypted_partition(){
		return (type == "part") && fstype.down().contains("luks");
	}

	public bool is_on_encrypted_partition(){
		return (type == "crypt");
	}

	public bool is_lvm_partition(){
		return (type == "part") && fstype.down().contains("lvm2_member");
	}

	public bool has_children(){
		return (children.size > 0);
	}

	public Device? first_linux_child(){
		
		foreach(var child in children){
			if (child.has_linux_filesystem()){
				return child;
			}
		}
		
		return null;
	}

	public bool has_parent(){
		return (parent != null);
	}

	// static --------------------------------
	
	public static Gee.ArrayList<Device> get_filesystems(bool get_space = true, bool get_mounts = true){

		/* Returns list of block devices
		   Populates all fields in Device class */

		var list = get_block_devices_using_lsblk();

		if (get_space){
			//get used space for mounted filesystems
			var list_df = get_disk_space_using_df();
			foreach(var dev_df in list_df){
				var dev = find_device_in_list(list, dev_df.uuid);
				if (dev != null){
					dev.size_bytes = dev_df.size_bytes;
					dev.used_bytes = dev_df.used_bytes;
					dev.available_bytes = dev_df.available_bytes;
					dev.used_percent = dev_df.used_percent;
				}
			}
		}

		if (get_mounts){
			//get mount points
			var list_mtab = get_mounted_filesystems_using_mtab();
			foreach(var dev_mtab in list_mtab){
				var dev = find_device_in_list(list, dev_mtab.uuid);
				if (dev != null){
					dev.mount_points = dev_mtab.mount_points;
				}
			}
		}

		//print_device_list(list);

		//print_device_mounts(list);

		log_debug("Device: get_filesystems(): %d".printf(list.size));
		
		return list;
	}

	private static void find_child_devices(Gee.ArrayList<Device> list, Device parent){
		if (lsblk_is_ancient && (parent.type == "disk")){
			foreach (var part in list){
				if ((part.kname != parent.kname) && part.kname.has_prefix(parent.kname)){
					parent.children.add(part);
					part.parent = parent;
					part.pkname = parent.kname;
					log_debug("%s -> %s".printf(parent.kname, part.kname));
				}
			}
		}
		else{
			foreach (var part in list){
				if (part.pkname == parent.kname){
					parent.children.add(part);
					part.parent = parent;
				}
			}
		}
	}

	private static void find_toplevel_parent(Gee.ArrayList<Device> list, Device dev){

		if (dev.pkname.length == 0){ return; }

		var top_kname = dev.pkname;
		
		foreach (var part in list){
			if (part.kname == top_kname){
				if (part.pkname.length > 0){
					top_kname = part.pkname; // get parent's parent if not empty
				}
			}
		}

		dev.pkname_toplevel = top_kname;

		//log_debug("%s -> %s -> %s".printf(dev.pkname_toplevel, dev.pkname, dev.kname));
	}

	private static void find_child_devices_using_dmsetup(Gee.ArrayList<Device> list){

		string std_out, std_err;
		exec_sync("dmsetup deps -o blkdevname", out std_out, out std_err);

		/*
		sdb3_crypt: 1 dependencies	: (sdb3)
		sda5_crypt: 1 dependencies	: (sda5)
		mmcblk0_crypt: 1 dependencies	: (mmcblk0)
		*/

		Regex rex;
		MatchInfo match;

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

			try{

				rex = new Regex("""([^:]*)\:.*\((.*)\)""");

				if (rex.match (line, 0, out match)){

					string child_name = match.fetch(1).strip();
					string parent_kname = match.fetch(2).strip();

					Device parent = null;
					foreach(var dev in list){
						if ((dev.kname == parent_kname)){
							parent = dev;
							break;
						}
					}

					Device child = null;
					foreach(var dev in list){
						if ((dev.mapped_name == child_name)){
							child = dev;
							break;
						}
					}

					if ((parent != null) && (child != null)){
						child.pkname = parent.kname;
						//log_debug("%s -> %s".printf(parent.kname, child.kname));
					}

				}
				else{
					log_debug("no-match: %s".printf(line));
				}
			}
			catch(Error e){
				log_error (e.message);
			}
		}
	}


	public static Gee.ArrayList<Device> get_block_devices_using_lsblk(string dev_name = ""){

		//log_debug("Device: get_block_devices_using_lsblk()");
		
		/* Returns list of mounted partitions using 'lsblk' command
		   Populates device, type, uuid, label */

		test_lsblk_version();

		var list = new Gee.ArrayList<Device>();

		string std_out;
		string std_err;
		string cmd;
		int ret_val;
		Regex rex;
		MatchInfo match;

		if (lsblk_is_ancient){
			cmd = "lsblk --bytes --pairs --output NAME,KNAME,LABEL,UUID,TYPE,FSTYPE,SIZE,MOUNTPOINT,MODEL,RO,RM,MAJ:MIN";
		}
		else{
			cmd = "lsblk --bytes --pairs --output NAME,KNAME,LABEL,UUID,TYPE,FSTYPE,SIZE,MOUNTPOINT,MODEL,RO,HOTPLUG,MAJ:MIN,PARTLABEL,PARTUUID,PKNAME,VENDOR,SERIAL,REV";
		}

		if (dev_name.length > 0){
			cmd += " %s".printf(dev_name);
		}

		ret_val = exec_sync(cmd, out std_out, out std_err);

		/*
		sample output
		-----------------
		NAME="sda" KNAME="sda" PKNAME="" LABEL="" UUID="" FSTYPE="" SIZE="119.2G" MOUNTPOINT="" HOTPLUG="0"

		NAME="sda1" KNAME="sda1" PKNAME="sda" LABEL="" UUID="5345-E139" FSTYPE="vfat" SIZE="47.7M" MOUNTPOINT="/boot/efi" HOTPLUG="0"

		NAME="mmcblk0p1" KNAME="mmcblk0p1" PKNAME="mmcblk0" LABEL="" UUID="3c0e4bbf" FSTYPE="crypto_LUKS" SIZE="60.4G" MOUNTPOINT="" HOTPLUG="1"

		NAME="luks-3c0" KNAME="dm-1" PKNAME="mmcblk0p1" LABEL="" UUID="f0d933c0-" FSTYPE="ext4" SIZE="60.4G" MOUNTPOINT="/mnt/sdcard" HOTPLUG="0"
		*/

		/*
		Note: Multiple loop devices can have same UUIDs.
		Example: Loop devices created by mounting the same ISO multiple times.
		*/

		// parse output and add to list -------------

		int index = -1;

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

			try{
				if (lsblk_is_ancient){
					rex = new Regex("""NAME="(.*)" KNAME="(.*)" LABEL="(.*)" UUID="(.*)" TYPE="(.*)" FSTYPE="(.*)" SIZE="(.*)" MOUNTPOINT="(.*)" MODEL="(.*)" RO="([0-9]+)" RM="([0-9]+)" MAJ[_:]MIN="([0-9:]+)"""");
				}
				else{
					rex = new Regex("""NAME="(.*)" KNAME="(.*)" LABEL="(.*)" UUID="(.*)" TYPE="(.*)" FSTYPE="(.*)" SIZE="(.*)" MOUNTPOINT="(.*)" MODEL="(.*)" RO="([0-9]+)" HOTPLUG="([0-9]+)" MAJ[_:]MIN="([0-9:]+)" PARTLABEL="(.*)" PARTUUID="(.*)" PKNAME="(.*)" VENDOR="(.*)" SERIAL="(.*)" REV="(.*)"""");
				}

				if (rex.match (line, 0, out match)){

					Device pi = new Device();

					int pos = 0;
					
					pi.name = match.fetch(++pos).strip();
					pi.kname = match.fetch(++pos).strip();
					
					pi.label = match.fetch(++pos); // don't strip; labels can have leading or trailing spaces
					pi.uuid = match.fetch(++pos).strip();

					pi.type = match.fetch(++pos).strip().down();

					pi.fstype = match.fetch(++pos).strip().down();
					pi.fstype = (pi.fstype == "crypto_luks") ? "luks" : pi.fstype;
					pi.fstype = (pi.fstype == "lvm2_member") ? "lvm2" : pi.fstype;

					pi.size_bytes = int64.parse(match.fetch(++pos).strip());

					var mp = match.fetch(++pos).strip();
					if (mp.length > 0){
						pi.mount_points.add(new MountEntry(pi,mp,""));
					}

					pi.model = match.fetch(++pos).strip();

					pi.read_only = (match.fetch(++pos).strip() == "1");

					pi.removable = (match.fetch(++pos).strip() == "1");

					string txt = match.fetch(++pos).strip();
					if (txt.contains(":")){
						pi.major = int.parse(txt.split(":")[0]);
						pi.minor = int.parse(txt.split(":")[1]);
					}
					
					if (!lsblk_is_ancient){
						
						pi.partlabel = match.fetch(++pos); // don't strip; labels can have leading or trailing spaces
						pi.partuuid = match.fetch(++pos).strip();
					
						pi.pkname = match.fetch(++pos).strip();
						pi.vendor = match.fetch(++pos).strip();
						pi.serial = match.fetch(++pos).strip();
						pi.revision = match.fetch(++pos).strip();
					}

					pi.order = ++index;
					pi.device = "/dev/%s".printf(pi.kname);

					if (pi.uuid.length > 0){
						pi.device_by_uuid = "/dev/disk/by-uuid/%s".printf(pi.uuid);
					}

					if (pi.label.length > 0){
						pi.device_by_label = "/dev/disk/by-label/%s".printf(pi.label);
					}

					if (pi.partuuid.length > 0){
						pi.device_by_partuuid = "/dev/disk/by-partuuid/%s".printf(pi.partuuid);
					}

					if (pi.partlabel.length > 0){
						pi.device_by_partlabel = "/dev/disk/by-partlabel/%s".printf(pi.partlabel);
					}

					//if ((pi.type == "crypt") && (pi.pkname.length > 0)){
					//	pi.name = "%s (unlocked)".printf(pi.pkname);
					//}

					//if ((pi.uuid.length > 0) && (pi.pkname.length > 0)){
						list.add(pi);
					//}
				}
				else{
					log_debug("no-match: %s".printf(line));
				}
			}
			catch(Error e){
				log_error (e.message);
			}
		}

		// already sorted
		/*list.sort((a,b)=>{
			return (a.order - b.order);
		});*/

		// add aliases from /dev/disk/by-uuid/

		foreach(var dev in list){
			var dev_by_uuid = path_combine("/dev/disk/by-uuid/", dev.uuid);
			if (file_exists(dev_by_uuid)){
				dev.symlinks.add(dev_by_uuid);
			}
		}

		// add aliases from /dev/mapper/

		try
		{
			File f_dev_mapper = File.new_for_path ("/dev/mapper");

			FileEnumerator enumerator = f_dev_mapper.enumerate_children (
				"%s,%s".printf(
					FileAttribute.STANDARD_NAME, FileAttribute.STANDARD_SYMLINK_TARGET),
				FileQueryInfoFlags.NOFOLLOW_SYMLINKS);

			FileInfo info;
			while ((info = enumerator.next_file ()) != null) {

				if (info.get_name() == "control") { continue; }

				File f_mapped = f_dev_mapper.resolve_relative_path(info.get_name());

				string mapped_file = f_mapped.get_path();
				string mapped_device = info.get_symlink_target();
				mapped_device = mapped_device.replace("..","/dev");
				//log_debug("info.get_name(): %s".printf(info.get_name()));
				//log_debug("info.get_symlink_target(): %s".printf(info.get_symlink_target()));
				//log_debug("mapped_file: %s".printf(mapped_file));
				//log_debug("mapped_device: %s".printf(mapped_device));

				foreach(var dev in list){
					if (dev.device == mapped_device){
						dev.mapped_name = mapped_file.replace("/dev/mapper/","");
						dev.symlinks.add(mapped_file);
						//log_debug("found link: %s -> %s".printf(mapped_file, dev.device));
						break;
					}
				}
			}
		}
		catch (Error e) {
			log_error (e.message);
		}

		device_list = list;

		foreach (var part in list){
			find_child_devices(list, part);
			find_toplevel_parent(list, part);
		}

		// Changes for "raid5" -------------------------------------------------------------------------

        // Cleanup for raid: remove member disks and double children
        for (int i = list.size - 1; i >= 0; --i) {
            if (list[i].type == "raid5") {
                // This is a raid5 device, lsblk shows one member disk and a partition
                // as parents and we remove them

                for (int j = i - 1; j >= 0; --j) {
                    if (list[j].kname == list[i].pkname) {
                        list.remove_at(j);
                        list.remove_at(j-1);
                        i-=2; // we are removing 2 elements before i
                        break;
                    }
                }

                // Does not have a parent anymore
                list[i].pkname = "";

                // Its children have to be unique (e.g. when mirroring,
                // lsblk shows each member partition twice)
                for (int j = list.size - 1; j > i; --j) {
                    if (list[j].pkname == list[i].kname) {
                        for (int k = j - 1; k >= 0; --k) {
                            if (list[k].kname == list[j].kname) {
                                list.remove_at(k);
                                --j; // we are removing an element between i and j
                            }
                        }
                    }
                }
            }
        }

        // Some more cleanup: raid are to be seen as disks and need deduplication
        for (int i = list.size - 1; i >= 0; --i) {
            if (list[i].type == "raid5") {
                // It's now a disk
                list[i].type  = "disk";
                list[i].model = list[i].name;

                // We remove other copies of the same raid device
                for (int j = list.size - 1; j >= 0; --j) {
                    if ((i != j) && (list[j].type == "raid5") && (list[j].kname == list[i].kname)) {
                        list.remove_at(j);
                        if (j < i)
                            --i; // we are removing an element before i
                    }
                }
            }
        }

        // changes for "dmraid" -------------------------------------------------------------------------

		// Cleanup for dmraid: remove member disks and double children
		for (int i = list.size - 1; i >= 0; --i) {
			if (list[i].type == "dmraid") {
				// This is a dmraid device, lsblk shows one member disk
				// as parent and we remove it

				for (int j = i - 1; j >= 0; --j) {
					if (list[j].kname == list[i].pkname) {
						list.remove_at(j);
						--i; // we are removing an element before i
						break;
					}
				}

				// Does not have a parent anymore
				list[i].pkname = "";

				// Its children have to be unique (e.g. when mirroring,
				// lsblk shows each member partition twice)
				for (int j = list.size - 1; j > i; --j) {
					if (list[j].pkname == list[i].kname) {
						for (int k = j - 1; k >= 0; --k) {
							if (list[k].kname == list[j].kname) {
								list.remove_at(k);
								--j; // we are removing an element between i and j
							}
						}
					}
				}
			}
		}

		// Some more cleanup: dmraid are to be seen as disks and need deduplication
		for (int i = list.size - 1; i >= 0; --i) {
			if (list[i].type == "dmraid") {
				// It's now a disk
				list[i].type  = "disk";
				list[i].model = list[i].name;

				// We remove other copies of the same dmraid device
				for (int j = list.size - 1; j >= 0; --j) {
					if ((i != j) && (list[j].type == "dmraid") && (list[j].kname == list[i].kname)) {
						list.remove_at(j);
						if (j < i)
							--i; // we are removing an element before i
					}
				}
			}
		}


		//find_toplevel_parent();

		if (lsblk_is_ancient){
			find_child_devices_using_dmsetup(list);
		}

		//print_device_list(list);

		//log_debug("Device: get_block_devices_using_lsblk(): %d".printf(list.size));

		return list;
	}

	// deprecated: use get_block_devices_using_lsblk() instead
	public static Gee.ArrayList<Device> get_block_devices_using_blkid(string dev_name = ""){

		/* Returns list of mounted partitions using 'blkid' command
		   Populates device, type, uuid, label */

		var list = new Gee.ArrayList<Device>();

		string std_out;
		string std_err;
		string cmd;
		int ret_val;
		Regex rex;
		MatchInfo match;

		cmd = "/sbin/blkid" + ((dev_name.length > 0) ? " " + dev_name: "");

		if (LOG_DEBUG){
			log_debug(cmd);
		}
		
		ret_val = exec_script_sync(cmd, out std_out, out std_err);
		if (ret_val != 0){
			var msg = "blkid: " + _("Failed to get partition list");
			msg += (dev_name.length > 0) ? ": " + dev_name : "";
			log_error(msg);
			return list; //return empty list
		}

		/*
		sample output
		-----------------
		/dev/sda1: LABEL="System Reserved" UUID="F476B08076B04560" TYPE="ntfs"
		/dev/sda2: LABEL="windows" UUID="BE00B6DB00B69A3B" TYPE="ntfs"
		/dev/sda3: UUID="03f3f35d-71fa-4dff-b740-9cca19e7555f" TYPE="ext4"
		*/

		//parse output and build filesystem map -------------

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

			Device pi = new Device();

			pi.device = line.split(":")[0].strip();

			if (pi.device.length == 0) { continue; }

			//exclude non-standard devices --------------------

			if (!pi.device.has_prefix("/dev/")){
				continue;
			}

			if (pi.device.has_prefix("/dev/sd") || pi.device.has_prefix("/dev/hd") || pi.device.has_prefix("/dev/mapper/") || pi.device.has_prefix("/dev/dm")) {
				//ok
			}
			else if (pi.device.has_prefix("/dev/disk/by-uuid/")){
				//ok, get uuid
				pi.uuid = pi.device.replace("/dev/disk/by-uuid/","");
			}
			else{
				continue; //skip
			}

			//parse & populate fields ------------------

			try{
				rex = new Regex("""LABEL=\"([^\"]*)\"""");
				if (rex.match (line, 0, out match)){
					pi.label = match.fetch(1); // do not strip - labels can have leading or trailing spaces
				}

				rex = new Regex("""UUID=\"([^\"]*)\"""");
				if (rex.match (line, 0, out match)){
					pi.uuid = match.fetch(1).strip();
				}

				rex = new Regex("""TYPE=\"([^\"]*)\"""");
				if (rex.match (line, 0, out match)){
					pi.fstype = match.fetch(1).strip();
				}
			}
			catch(Error e){
				log_error (e.message);
			}

			//add to map -------------------------

			if (pi.uuid.length > 0){
				list.add(pi);
			}
		}

		log_debug("Device: get_block_devices_using_blkid(): %d".printf(list.size));
		
		return list;
	}

	public static Gee.ArrayList<Device> get_disk_space_using_df(string dev_name_or_mount_point = ""){

		/*
		Returns list of mounted partitions using 'df' command
		Populates device, type, size, used and mount_point_list
		*/

		var list = new Gee.ArrayList<Device>();

		string std_out;
		string std_err;
		string cmd;
		int ret_val;

		cmd = "df -T -B1";

		if (dev_name_or_mount_point.length > 0){
			cmd += " '%s'".printf(escape_single_quote(dev_name_or_mount_point));
		}

		if (LOG_DEBUG){
			log_debug(cmd);
		}

		ret_val = exec_sync(cmd, out std_out, out std_err);
		//ret_val is not reliable, no need to check

		/*
		sample output
		-----------------
		Filesystem     Type     1M-blocks    Used Available Use% Mounted on
		/dev/sda3      ext4        25070M  19508M     4282M  83% /
		none           tmpfs           1M      0M        1M   0% /sys/fs/cgroup
		udev           devtmpfs     3903M      1M     3903M   1% /dev
		tmpfs          tmpfs         789M      1M      788M   1% /run
		none           tmpfs           5M      0M        5M   0% /run/lock
		/dev/sda3      ext4        25070M  19508M     4282M  83% /mnt/timeshift
		*/

		string[] lines = std_out.split("\n");

		int line_num = 0;
		foreach(string line in lines){

			if (++line_num == 1) { continue; }
			if (line.strip().length == 0) { continue; }

			Device pi = new Device();

			//parse & populate fields ------------------

			int k = 1;
			foreach(string val in line.split(" ")){

				if (val.strip().length == 0){ continue; }

				switch(k++){
					case 1:
						pi.device = val.strip();
						break;
					case 2:
						pi.fstype = val.strip();
						break;
					case 3:
						pi.size_bytes = uint64.parse(val.strip());
						break;
					case 4:
						pi.used_bytes = uint64.parse(val.strip());
						break;
					case 5:
						pi.available_bytes = uint64.parse(val.strip());
						break;
					case 6:
						pi.used_percent = val.strip();
						break;
					case 7:
						//string mount_point = val.strip();
						//if (!pi.mount_point_list.contains(mount_point)){
						//	pi.mount_point_list.add(mount_point);
						//}
						break;
				}
			}

			/* Note:
			 * The mount points displayed by 'df' are not reliable.
			 * For example, if same device is mounted at 2 locations, 'df' displays only the first location.
			 * Hence, we will not populate the 'mount_points' field in Device object
			 * Use get_mounted_filesystems_using_mtab() if mount info is required
			 * */

			// resolve device name --------------------

			pi.device = resolve_device_name(pi.device);
			
			// get uuid ---------------------------

			pi.uuid = get_device_uuid(pi.device);

			// add to map -------------------------

			if (pi.uuid.length > 0){
				list.add(pi);
			}
		}
		
		log_debug("Device: get_disk_space_using_df(): %d".printf(list.size));

		return list;
	}

	public static Gee.ArrayList<Device> get_mounted_filesystems_using_mtab(){

		/* Returns list of mounted partitions by reading /proc/mounts
		   Populates device, type and mount_point_list */

		var list = new Gee.ArrayList<Device>();

		string mtab_path = "/etc/mtab";
		string mtab_lines = "";

		File f;

		// find mtab file -----------

		mtab_path = "/proc/mounts";
		f = File.new_for_path(mtab_path);
		if(!f.query_exists()){
			mtab_path = "/proc/self/mounts";
			f = File.new_for_path(mtab_path);
			if(!f.query_exists()){
				mtab_path = "/etc/mtab";
				f = File.new_for_path(mtab_path);
				if(!f.query_exists()){
					return list; //empty list
				}
			}
		}

		/* Note:
		 * /etc/mtab represents what 'mount' passed to the kernel
		 * whereas /proc/mounts shows the data as seen inside the kernel
		 * Hence /proc/mounts is always up-to-date whereas /etc/mtab might not be
		 * */

		//read -----------

		mtab_lines = file_read(mtab_path);

		/*
		sample mtab
		-----------------
		/dev/sda3 / ext4 rw,errors=remount-ro 0 0
		proc /proc proc rw,noexec,nosuid,nodev 0 0
		sysfs /sys sysfs rw,noexec,nosuid,nodev 0 0
		none /sys/fs/cgroup tmpfs rw 0 0
		none /sys/fs/fuse/connections fusectl rw 0 0
		none /sys/kernel/debug debugfs rw 0 0
		none /sys/kernel/security securityfs rw 0 0
		udev /dev devtmpfs rw,mode=0755 0 0

		device - the device or remote filesystem that is mounted.
		mountpoint - the place in the filesystem the device was mounted.
		filesystemtype - the type of filesystem mounted.
		options - the mount options for the filesystem
		dump - used by dump to decide if the filesystem needs dumping.
		fsckorder - used by fsck to determine the fsck pass to use.
		*/

		/* Note:
		 * We are interested only in the last device that was mounted at a given mount point
		 * Hence the lines must be parsed in reverse order (from last to first)
		 * */

		//parse ------------

		string[] lines = mtab_lines.split("\n");
		var mount_list = new Gee.ArrayList<string>();

		for (int i = lines.length - 1; i >= 0; i--){

			bool ignoreEntry = false;

			string line = lines[i].strip();
			if (line.length == 0) { continue; }

			var pi = new Device();

			var mp = new MountEntry(pi,"","");

			//parse & populate fields ------------------

			int k = 1;
			foreach(string val in line.split(" ")){
				if (val.strip().length == 0){ continue; }
				if (ignoreEntry){ break; }
				switch(k++){
					case 1: //device
						pi.device = val.strip();
						break;
					case 2: //mountpoint
						mp.mount_point = val.strip().replace("""\040"""," "); // replace space. TODO: other chars?

						// HACK: ignore Docker mounting(?) rootfs on /var/lib/docker
						if (mp.mount_point.contains("/docker")){
							ignoreEntry = true;
							break;
						}

						if (!mount_list.contains(mp.mount_point)){
							mount_list.add(mp.mount_point);
							pi.mount_points.add(mp);
						}
						break;
					case 3: //filesystemtype
						pi.fstype = val.strip();
						break;
					case 4: //options
						mp.mount_options = val.strip();
						break;
					default:
						//ignore
						break;
				}
			}

			if (ignoreEntry) { continue; }

			// resolve device names ----------------

			pi.device = resolve_device_name(pi.device);

			// get uuid ---------------------------

			pi.uuid = get_device_uuid(pi.device);

			// add to map -------------------------

			if (pi.uuid.length > 0){
				var dev = find_device_in_list(list, pi.uuid);
				if (dev == null){
					list.add(pi);
				}
				else{
					// add mount points to existing device
					foreach(var item in pi.mount_points){
						dev.mount_points.add(item);
					}
				}
			}
		}

		log_debug("Device: get_mounted_filesystems_using_mtab(): %d".printf(list.size));
		
		return list;
	}

	// helpers ----------------------------------

	public static Device? get_device_by_uuid(string uuid){

		return find_device_in_list(device_list, uuid);
	}

	public static Device? get_device_by_name(string file_name){

		return find_device_in_list(device_list, file_name);
	}

	public static Device? get_device_by_path(string path_to_check){
		
		var list = Device.get_disk_space_using_df(path_to_check);
		
		if (list.size > 0){
			return list[0];
		}
		
		return null;
	}
	
	public static string get_device_uuid(string device){
		
		if (device_list == null){
			device_list = get_block_devices_using_lsblk();
		}
		foreach(Device dev in device_list){
			if (dev.device == device){
				return dev.uuid;
			}
		}
		
		return "";
	}

	public static Gee.ArrayList<MountEntry> get_device_mount_points(string dev_name_or_uuid){
		string device = "";
		string uuid = "";

		if (dev_name_or_uuid.has_prefix("/dev")){
			device = dev_name_or_uuid;
			uuid = get_device_uuid(dev_name_or_uuid);
		}
		else{
			uuid = dev_name_or_uuid;
			device = "/dev/disk/by-uuid/%s".printf(uuid);
			device = resolve_device_name(device);
		}

		var list_mtab = get_mounted_filesystems_using_mtab();
		
		var dev = find_device_in_list(list_mtab, uuid);

		if (dev != null){
			return dev.mount_points;
		}
		else{
			return (new Gee.ArrayList<MountEntry>());
		}
	}

	public static bool device_is_mounted(string dev_name_or_uuid){

		var mps = Device.get_device_mount_points(dev_name_or_uuid);
		if (mps.size > 0){
			return true;
		}

		return false;
	}

	public static bool mount_point_in_use(string mount_point){
		var list = Device.get_mounted_filesystems_using_mtab();
		foreach (var dev in list){
			foreach(var mp in dev.mount_points){
				if (mp.mount_point.has_prefix(mount_point)){
					// check for any mount point at or under the given mount_point
					return true;
				}
			}
		}
		return false;
	}

	public static string resolve_device_name(string dev_alias){

		var dev = find_device_in_list(device_list, dev_alias);

		if (dev != null){
			return dev.device;
		}
		else{
			return dev_alias;
		}
	}

	public static Device? find_device_in_list(Gee.ArrayList<Device> list, string _dev_alias){

		string dev_alias = _dev_alias;
		
		if (dev_alias.down().has_prefix("uuid=")){
			
			dev_alias = dev_alias.split("=",2)[1].strip().down();
		}
		else if (file_exists(dev_alias) && file_is_symlink(dev_alias)){

			var link_path = file_get_symlink_target(dev_alias);
			
			dev_alias = link_path.replace("../../../","/dev/").replace("../../","/dev/").replace("../","/dev/");
		}

		foreach(var dev in list){
			
			if (dev.device == dev_alias){
				return dev;
			}
			else if (dev.uuid == dev_alias){
				return dev;
			}
			else if (dev.label == dev_alias){
				return dev;
			}
			else if (dev.partuuid == dev_alias){
				return dev;
			}
			else if (dev.partlabel == dev_alias){
				return dev;
			}
			else if (dev.device_by_uuid == dev_alias){
				return dev;
			}
			else if (dev.device_by_label == dev_alias){
				return dev;
			}
			else if (dev.device_by_partuuid == dev_alias){
				return dev;
			}
			else if (dev.device_by_partlabel == dev_alias){
				return dev;
			}
			else if (dev.device_mapper == dev_alias){
				return dev;
			}
			else if (dev.mapped_name == dev_alias){ // check last
				return dev;
			}
		}

		return null;
	}
	
	public void copy_fields_from(Device dev2){
		
		this.device = dev2.device;
		this.name = dev2.name;
		this.kname = dev2.kname;
		this.pkname = dev2.pkname;
		this.label = dev2.label;
		this.uuid = dev2.uuid;
		this.mapped_name = dev2.mapped_name;
		
		this.type = dev2.type;
		this.fstype = dev2.fstype;

		this.size_bytes = dev2.size_bytes;
		this.used_bytes = dev2.used_bytes;
		this.available_bytes = dev2.available_bytes;
		
		this.mount_points = dev2.mount_points;
		this.symlinks = dev2.symlinks;
		this.parent = dev2.parent;
		this.children = dev2.children;

		this.vendor = dev2.vendor;
		this.model = dev2.model;
		this.serial = dev2.serial;
		this.revision = dev2.revision;

		this.removable = dev2.removable;
		this.read_only = dev2.read_only;
	}

	public Device? query_changes(){
		
		Device dev_new = null;
		
		foreach (var dev in get_block_devices_using_lsblk()){
			if (uuid.length > 0){
				if (dev.uuid == uuid){
					dev_new = dev;
					break;
				}
			}
			else{
				if (dev.device == device){
					dev_new = dev;
					break;
				}
			}
		}
		
		return dev_new;
	}
	
	public void query_disk_space(){

		/* Updates disk space info and returns the given Device object */

		var list_df = get_disk_space_using_df(device);
		
		var dev_df = find_device_in_list(list_df, uuid);
		
		if (dev_df != null){
			// update dev fields
			size_bytes = dev_df.size_bytes;
			used_bytes = dev_df.used_bytes;
			available_bytes = dev_df.available_bytes;
			used_percent = dev_df.used_percent;
		}
	}

	// mounting ---------------------------------
	
	public static bool automount_udisks(string dev_name_or_uuid, Gtk.Window? parent_window){
		
		if (dev_name_or_uuid.length == 0){
			log_error(_("Device name is empty!"));
			return false;
		}
		
		var cmd = "udisksctl mount -b '%s'".printf(dev_name_or_uuid);
		log_debug(cmd);
		int status = Posix.system(cmd);

		if (status != 0){
			if (parent_window != null){
				string msg = "Failed to mount: %s".printf(dev_name_or_uuid);
				gtk_messagebox("Error", msg, parent_window, true);
			}
		}

		return (status == 0);
	}

	public static bool automount_udisks_iso(string iso_file_path, out string loop_device, Gtk.Window? parent_window){

		loop_device = "";

		if (!file_exists(iso_file_path)){
			string msg = "%s: %s".printf(_("Could not find file"), iso_file_path);
			log_error(msg);
			return false;
		}

		var cmd = "udisksctl loop-setup -r -f '%s'".printf(
			escape_single_quote(iso_file_path));
			
		log_debug(cmd);
		string std_out, std_err;
		int exit_code = exec_sync(cmd, out std_out, out std_err);
		
		if (exit_code == 0){
			log_msg("%s".printf(std_out));
			//log_msg("%s".printf(std_err));

			if (!std_out.contains(" as ")){
				log_error("Could not determine loop device");
				return false;
			}

			loop_device = std_out.split(" as ")[1].replace(".","").strip();
			log_msg("loop_device: %s".printf(loop_device));
		
			var list = get_block_devices_using_lsblk();
			foreach(var dev in list){
				if ((dev.pkname == loop_device.replace("/dev/","")) && (dev.fstype == "iso9660")){
					loop_device = dev.device;
					return automount_udisks(dev.device, parent_window);
				}
			}
		}
		
		return false;
	}

	public static bool unmount_udisks(string dev_name_or_uuid, Gtk.Window? parent_window){

		if (dev_name_or_uuid.length == 0){
			log_error(_("Device name is empty!"));
			return false;
		}
		
		var cmd = "udisksctl unmount -b '%s'".printf(dev_name_or_uuid);
		log_debug(cmd);
		int status = Posix.system(cmd);

		if (status != 0){
			if (parent_window != null){
				string msg = "Failed to unmount: %s".printf(dev_name_or_uuid);
				gtk_messagebox("Error", msg, parent_window, true);
			}
		}
		
		return (status == 0);
	}

	public static Device? luks_unlock(
		Device luks_device, string mapped_name, string passphrase, Gtk.Window? parent_window, 
		out string message, out string details){

		/* Unlocks a LUKS device using provided passphrase.
		 * Prompts the user for passphrase if empty.
		 * Displays a GTK prompt if parent_window is not null
		 * Otherwise prompts user on terminal with a timeout of 20 secsonds
		 * */
		 
		Device unlocked_device = null;
		string std_out = "", std_err = "";
		
		// check if not encrypted
		if (!luks_device.fstype.contains("luks") && !luks_device.fstype.contains("crypt")){
			message = _("This device is not encrypted");
			details = _("Failed to unlock device");
			return null;
		}

		// check if already unlocked
		var list = get_block_devices_using_lsblk();
		foreach(var part in list){
			if (part.pkname == luks_device.kname){
				unlocked_device = part;
				message = _("Device is unlocked");
				details = _("Unlocked device is mapped to '%s'").printf(part.mapped_name);
				return part; 
			}
		}

		string luks_pass = passphrase;
		string luks_name = mapped_name;

		if ((luks_name == null) || (luks_name.length == 0)){
			luks_name = "%s_crypt".printf(luks_device.kname);
		}

		if (parent_window == null){

			// console mode
			
			if ((luks_pass == null) || (luks_pass.length == 0)){

				// prompt user on terminal and unlock, else timeout after 20 secs
				
				var counter = new TimeoutCounter();
				counter.kill_process_on_timeout("cryptsetup", 20, true);
				string cmd = "cryptsetup luksOpen '%s' '%s'".printf(luks_device.device, luks_name);

				log_debug(cmd);
				Posix.system(cmd);
				counter.stop();
				log_msg("");
				
			}
			else{

				// use password to unlock

				var cmd = "echo -n -e '%s' | cryptsetup luksOpen --key-file - '%s' '%s'\n".printf(
					escape_single_quote(luks_pass), luks_device.device, luks_name);

				log_debug(cmd.replace(escape_single_quote(luks_pass), "**PASSWORD**"));
				
				int status = exec_script_sync(cmd, out std_out, out std_err, false, true);

				switch (status){
				case 512: // invalid passphrase
					message = _("Wrong password");
					details = _("Failed to unlock device");
					log_error(message);
					log_error(details);
					break;
				}
			}

		}
		else{

			// gui mode

			if ((luks_pass == null) || (luks_pass.length == 0)){

				// show input prompt

				log_debug("Prompting user for passphrase..");
				
				luks_pass = gtk_inputbox(
						_("Encrypted Device"),
						_("Enter passphrase to unlock '%s'").printf(luks_device.name),
						parent_window, true);

				if (luks_pass == null){
					// cancelled by user
					message = _("Failed to unlock device");
					details = _("User cancelled the password prompt");
					log_debug("User cancelled the password prompt");
					return null;
				}
			}

			if ((luks_pass != null) && (luks_pass.length > 0)){

				// use password to unlock

				var cmd = "echo -n -e '%s' | cryptsetup luksOpen --key-file - '%s' '%s'\n".printf(
					escape_single_quote(luks_pass), luks_device.device, luks_name);

				log_debug(cmd.replace(escape_single_quote(luks_pass), "**PASSWORD**"));
				
				int status = exec_script_sync(cmd, out std_out, out std_err, false, true);

				switch (status){
				case 512: // invalid passphrase
					message = _("Wrong password");
					details = _("Failed to unlock device");
					log_error(message);
					log_error(details);
					break;
				}
			}
			
		}

		// find unlocked device
		list = get_block_devices_using_lsblk();
		foreach(var part in list){
			if (part.pkname == luks_device.kname){
				unlocked_device = part;
				break; 
			}
		}

		bool is_error = false;
		if (unlocked_device == null){
			message = _("Failed to unlock device") + " '%s'".printf(luks_device.device);
			details = std_err;
			is_error = true;
		}
		else{
			message = _("Unlocked successfully");
			details = _("Unlocked device is mapped to '%s'").printf(unlocked_device.mapped_name);
		}

		if (parent_window != null){
			gtk_messagebox(message, details, parent_window, is_error);
		}

		return unlocked_device;
	}

	public static bool luks_lock(string kname, Gtk.Window? parent_window){
		
		var cmd = "cryptsetup luksClose %s".printf(kname);

		log_debug(cmd);

		string std_out, std_err;
		int status = exec_script_sync(cmd, out std_out, out std_err, false, true);
		log_msg(std_out);
		log_msg(std_err);
		
		if (status != 0){
			if (parent_window != null){
				string msg = "Failed to lock device: %s".printf(kname);
				gtk_messagebox("Error", msg, parent_window, true);
			}
		}
		
		return (status == 0);
		
		/*log_debug(cmd);
		
		if (bash_admin_shell != null){
			int status = bash_admin_shell.execute(cmd);
			return (status == 0);
		}
		else{
			int status = exec_script_sync(cmd,null,null,false,true);
			return (status == 0);
		}*/
	}

	public static bool mount(
		string dev_name_or_uuid, string mount_point, string mount_options = "", bool silent = false){

		/*
		 * Mounts specified device at specified mount point.
		 * 
		 * */

		string cmd = "";
		string std_out;
		string std_err;
		int ret_val;

		string device = "";
		string uuid = "";

		// resolve uuid and device name ----------
		
		if (dev_name_or_uuid.has_prefix("/dev")){
			device = dev_name_or_uuid;
			uuid = get_device_uuid(dev_name_or_uuid);
		}
		else{
			uuid = dev_name_or_uuid;
			device = "/dev/disk/by-uuid/%s".printf(uuid);
			device = resolve_device_name(device);
		}

		// check if already mounted --------------
		
		var mps = Device.get_device_mount_points(dev_name_or_uuid);

		log_debug("------------------");
		log_debug("arg=%s, device=%s".printf(dev_name_or_uuid, device));
		foreach(var mp in mps){
			log_debug(mp.mount_point);
		}
		log_debug("------------------");
		
		foreach(var mp in mps){
			if ((mp.mount_point == mount_point) && mp.mount_options.contains(mount_options)){
				if (!silent){
					var msg = "%s is mounted at: %s".printf(device, mount_point);
					if (mp.mount_options.length > 0){
						msg += ", options: %s".printf(mp.mount_options);
					}
					log_msg("\n%s\n".printf(msg));
				}
				return true;
			}
		}

		dir_create(mount_point);

		// unmount if any other device is mounted

		unmount(mount_point);

		// mount the device -------------------
		
		if (mount_options.length > 0){
			cmd = "mount -o %s \"%s\" \"%s\"".printf(mount_options, device, mount_point);
		}
		else{
			cmd = "mount \"%s\" \"%s\"".printf(device, mount_point);
		}

		ret_val = exec_sync(cmd, out std_out, out std_err);

		if (ret_val != 0){
			log_error ("Failed to mount device '%s' at mount point '%s'".printf(device, mount_point));
			log_error (std_err);
			return false;
		}
		else{
			if (!silent){
				Device dev = get_device_by_name(device);
				log_msg ("Mounted '%s'%s at '%s'".printf(
					(dev == null) ? device : dev.device_name_with_parent,
					(mount_options.length > 0) ? " (%s)".printf(mount_options) : "",
					mount_point));
			}
			return true;
		}
			
		// check if mounted successfully ------------------

		/*mps = Device.get_device_mount_points(dev_name_or_uuid);
		if (mps.contains(mount_point)){
			log_msg("Device '%s' is mounted at '%s'".printf(dev_name_or_uuid, mount_point));
			return true;
		}
		else{
			return false;
		}*/
	}

	public static string automount(
		string dev_name_or_uuid, string mount_options = "", string mount_prefix = "/run"){

		/* Returns the mount point of specified device.
		 * If unmounted, mounts the device to /run/<uuid> and returns the mount point.
		 * */

		string device = "";
		string uuid = "";

		// get uuid -----------------------------

		if (dev_name_or_uuid.has_prefix("/dev")){
			device = dev_name_or_uuid;
			uuid = Device.get_device_uuid(dev_name_or_uuid);
		}
		else{
			uuid = dev_name_or_uuid;
			device = "/dev/disk/by-uuid/%s".printf(uuid);
			device = resolve_device_name(device);
		}

		// check if already mounted and return mount point -------------

		var list = Device.get_block_devices_using_lsblk();
		var dev = find_device_in_list(list, uuid);
		if (dev != null){
			return dev.mount_points[0].mount_point;
		}

		// check and create mount point -------------------

		string mount_point = "%s/%s".printf(mount_prefix, uuid);

		try{
			File file = File.new_for_path(mount_point);
			if (!file.query_exists()){
				file.make_directory_with_parents();
			}
		}
		catch(Error e){
			log_error (e.message);
			return "";
		}

		// mount the device and return mount_point --------------------

		if (mount(uuid, mount_point, mount_options)){
			return mount_point;
		}
		else{
			return "";
		}
	}

	public static bool unmount(string mount_point){

		/* Recursively unmounts all devices at given mount_point and subdirectories
		 * */

		string cmd = "";
		string std_out;
		string std_err;
		int ret_val;

		// check if mount point is in use
		if (!Device.mount_point_in_use(mount_point)) {
			return true;
		}

		// try to unmount ------------------

		try{

			string cmd_unmount = "cat /proc/mounts | awk '{print $2}' | grep '%s' | sort -r | xargs umount".printf(mount_point);

			log_debug(_("Unmounting from") + ": '%s'".printf(mount_point));

			//sync before unmount
			cmd = "sync";
			Process.spawn_command_line_sync(cmd, out std_out, out std_err, out ret_val);
			//ignore success/failure

			//unmount
			ret_val = exec_script_sync(cmd_unmount, out std_out, out std_err);

			if (ret_val != 0){
				log_error (_("Failed to unmount"));
				log_error (std_err);
			}
		}
		catch(Error e){
			log_error (e.message);
			return false;
		}

		// check if mount point is in use
		if (!Device.mount_point_in_use(mount_point)) {
			return true;
		}
		else{
			return false;
		}
	}

	// description helpers

	public string full_name_with_alias{
		owned get{
			string text = device;
			if (mapped_name.length > 0){
				text += " (%s)".printf(mapped_name);
			}
			return text;
		}
	}

	public string full_name_with_parent{
		owned get{
			return device_name_with_parent;
		}
	}

	public string short_name_with_alias{
		owned get{
			string text = kname;
			if (mapped_name.length > 0){
				text += " (%s)".printf(mapped_name);
			}
			return text;
		}
	}

	public string short_name_with_parent{
		owned get{
			string text = kname;

			if (has_parent() && (parent.type == "part")){
				text += " (%s)".printf(pkname);
			}
			
			return text;
		}
	}

	public string device_name_with_parent{
		owned get{
			string text = device;

			if (has_parent() && (parent.type == "part")){
				text += " (%s)".printf(parent.kname);
			}
			
			return text;
		}
	}

	public string description(){
		return description_formatted().replace("<b>","").replace("</b>","");
	}

	public string description_formatted(){
		string s = "";

		if (type == "disk"){
			s += "<b>" + kname + "</b> ~";
			if (vendor.length > 0){
				s += " " + vendor;
			}
			if (model.length > 0){
				s += " " + model;
			}
			if (size_bytes > 0) {
				s += " (%s)".printf(format_file_size(size_bytes, false, "", true, 0));
			}
		}
		else{
			s += "<b>" + short_name_with_parent + "</b>" ;
			s += (label.length > 0) ? " (" + label + ")": "";
			s += (fstype.length > 0) ? " ~ " + fstype : "";
			if (size_bytes > 0) {
				s += " (%s)".printf(format_file_size(size_bytes, false, "", true, 0));
			}
		}

		return s.strip();
	}
	
	public string description_simple(){
		return description_simple_formatted().replace("<b>","").replace("</b>","");
	}
	
	public string description_simple_formatted(){
		
		string s = "";

		if (type == "disk"){
			if (vendor.length > 0){
				s += " " + vendor;
			}
			if (model.length > 0){
				s += " " + model;
			}
			if (size_bytes > 0) {
				if (s.strip().length == 0){
					s += "%s Device".printf(format_file_size(size_bytes, false, "", true, 0));
				}
				else{
					s += " (%s)".printf(format_file_size(size_bytes, false, "", true, 0));
				}
			}
		}
		else{
			s += "<b>" + short_name_with_parent + "</b>" ;
			s += (label.length > 0) ? " (" + label + ")": "";
			s += (fstype.length > 0) ? " ~ " + fstype : "";
			if (size_bytes > 0) {
				s += " (%s)".printf(format_file_size(size_bytes, false, "", true, 0));
			}
		}

		return s.strip();
	}

	public string description_full_free(){
		string s = "";

		if (type == "disk"){
			s += "%s %s".printf(model, vendor).strip();
			if (s.length == 0){
				s = "%s Disk".printf(format_file_size(size_bytes));
			}
			else{
				s += " (%s Disk)".printf(format_file_size(size_bytes));
			}
		}
		else{
			s += kname;
			if (label.length > 0){
				s += " (%s)".printf(label);
			}
			if (fstype.length > 0){
				s += " ~ %s".printf(fstype);
			}
			if (free_bytes > 0){
				s += " ~ %s".printf(description_free());
			}
		}

		return s;
	}

	public string description_full(){
		string s = "";
		s += device;
		s += (label.length > 0) ? " (" + label + ")": "";
		s += (uuid.length > 0) ? " ~ " + uuid : "";
		s += (fstype.length > 0) ? " ~ " + fstype : "";
		s += (used.length > 0) ? " ~ " + used + " / " + size + " GB used (" + used_percent + ")" : "";
		
		return s;
	}

	public string description_usage(){
		if (used.length > 0){
			return used + " / " + size + " used (" + used_percent + ")";
		}
		else{
			return "";
		}
	}

	public string description_free(){
		if (used.length > 0){
			return format_file_size(free_bytes, false, "g", false)
				+ " / " + format_file_size(size_bytes, false, "g", true) + " free";
		}
		else{
			return "";
		}
	}

	public string tooltip_text(){
		string tt = "";

		if (type == "disk"){
			tt += "%-15s: %s\n".printf(_("Device"), device);
			tt += "%-15s: %s\n".printf(_("Vendor"), vendor);
			tt += "%-15s: %s\n".printf(_("Model"), model);
			tt += "%-15s: %s\n".printf(_("Serial"), serial);
			tt += "%-15s: %s\n".printf(_("Revision"), revision);

			tt += "%-15s: %s\n".printf( _("Size"),
				(size_bytes > 0) ? format_file_size(size_bytes) : "N/A");
		}
		else{
			tt += "%-15s: %s\n".printf(_("Device"),
				(mapped_name.length > 0) ? "%s → %s".printf(device, mapped_name) : device);
				
			if (has_parent()){
				tt += "%-15s: %s\n".printf(_("Parent Device"), parent.device);
			}
			tt += "%-15s: %s\n".printf(_("UUID"),uuid);
			tt += "%-15s: %s\n".printf(_("Type"),type);
			tt += "%-15s: %s\n".printf(_("Filesystem"),fstype);
			tt += "%-15s: %s\n".printf(_("Label"),label);
			
			tt += "%-15s: %s\n".printf(_("Size"),
				(size_bytes > 0) ? format_file_size(size_bytes) : "N/A");
				
			tt += "%-15s: %s\n".printf(_("Used"),
				(used_bytes > 0) ? format_file_size(used_bytes) : "N/A");

			tt += "%-15s: %s\n".printf(_("System"),dist_info);
		}

		return "<tt>%s</tt>".printf(tt);
	}

	// testing -----------------------------------

	public static void test_all(){
		
		var list = get_block_devices_using_lsblk();
		log_msg("\n> get_block_devices_using_lsblk()");
		print_device_list(list);

		log_msg("");
		
		//list = get_block_devices_using_blkid();
		//log_msg("\nget_block_devices_using_blkid()\n");
		//print_device_list(list);

		list = get_mounted_filesystems_using_mtab();
		log_msg("\n> get_mounted_filesystems_using_mtab()");
		print_device_mounts(list);

		log_msg("");

		list = get_disk_space_using_df();
		log_msg("\n> get_disk_space_using_df()");
		print_device_disk_space(list);

		log_msg("");

		list = get_filesystems();
		log_msg("\n> get_filesystems()");
		print_device_list(list);
		print_device_mounts(list);
		print_device_disk_space(list);
		
		log_msg("");
	}

	public static void print_device_list(Gee.ArrayList<Device> list){

		log_debug("");
		
		log_debug("%-12s ,%-5s ,%-5s ,%-36s ,%s".printf(
			"device",
			"pkname",
			"kname",
			"uuid",
			"mapped_name"));

		log_debug(string.nfill(100, '-'));

		foreach(var dev in list){
			log_debug("%-12s ,%-5s ,%-5s ,%-36s ,%s".printf(
				dev.device ,
				dev.pkname,
				dev.kname,
				dev.uuid,
				dev.mapped_name
				));
		}

		log_debug("");
		
		/*
		log_debug("%-20s %-20s %s %s %s %s".printf(
			"device",
			"label",
			"vendor",
			"model",
			"serial",
			"rev"));

		log_debug(string.nfill(100, '-'));

		foreach(var dev in list){
			log_debug("%-20s %-20s %s %s %s %s".printf(
				dev.device,
				dev.label,
				dev.vendor,
				dev.model,
				dev.serial,
				dev.revision
				));
		}

		log_debug("");
		*/
		
		/*log_debug("%-20s %-10s %-15s %-3s %-3s %15s %15s".printf(
			"device",
			"type",
			"fstype",
			"REM",
			"RO",
			"size",
			"used"));

		log_debug(string.nfill(100, '-'));

		foreach(var dev in list){
			log_debug("%-20s %-10s %-15s %-3s %-3s %15s %15s".printf(
				dev.device,
				dev.type,
				dev.fstype,
				dev.removable ? "1" : "0",
				dev.read_only ? "1" : "0",
				format_file_size(dev.size_bytes, true),
				format_file_size(dev.used_bytes, true)
				));
		}*/

		//log_debug("");
	}

	public static void print_device_mounts(Gee.ArrayList<Device> list){

		log_debug("");
		
		log_debug("%-15s %s".printf(
			"device",
			//"fstype",
			"> mount_points (mount_options)"
		));

		log_debug(string.nfill(100, '-'));

		foreach(var dev in list){

			string mps = "";
			foreach(var mp in dev.mount_points){
				mps += "\n    %s -> ".printf(mp.mount_point);
				if (mp.mount_options.length > 0){
					mps += " %s".printf(mp.mount_options);
				}
			}

			log_debug("%-15s %s".printf(
				dev.device,
				//dev.fstype,
				mps
			));
			
		}

		log_debug("");
	}

	public static void print_device_disk_space(Gee.ArrayList<Device> list){
		log_debug("");
		
		log_debug("%-15s %-12s %15s %15s %15s %10s".printf(
			"device",
			"fstype",
			"size",
			"used",
			"available",
			"used_percent"
		));

		log_debug(string.nfill(100, '-'));

		foreach(var dev in list){
			log_debug("%-15s %-12s %15s %15s %15s %10s".printf(
				dev.device,
				dev.fstype,
				format_file_size(dev.size_bytes, true),
				format_file_size(dev.used_bytes, true),
				format_file_size(dev.available_bytes, true),
				dev.used_percent
			));
		}

		log_debug("");
	}
}







