
/* Gnome Encfs Manager (GEncfsM)
 * Copyright (C) 2011 Moritz Molch <mail@moritzmolch.de>
 
 * 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 Gee;
using Gtk;
using Encfs;
using GnomeEncfsManager;
using Posix;

namespace GnomeEncfsManager {
    public class Stash : GLib.Object {
        public string   cryptDir            {get; private set; }
        
        public string   _mountDir;
        public string mountDir {
        	owned get {
        	    if (_mountDir.length < 2) {
        	        if (_mountDir[0] == '/') {
        	            return _mountDir;
        	        } else {
        	            return Path.build_filename(get_mount_dir_base(), _mountDir);
        	        }
        	    }
        	    
        		if (_mountDir[0] == '/') {
        			return _mountDir;
        		} else if (_mountDir[0:2] == "~/") {
        		    if (_mountDir.length > 2) {
            		    return Path.build_filename(Environment.get_home_dir(), _mountDir[2:_mountDir.length]);
            		} else {
            		    return Environment.get_home_dir();
            		}
        		} else {
        			return Path.build_filename(get_mount_dir_base(), _mountDir);
        		}
        	}
        	private set {
        		if (value.has_prefix(get_mount_dir_base())) {
        			int idx = get_mount_dir_base().length +1;
        			_mountDir = value[idx:value.length];
        		} else {
	       			_mountDir = value;
	       		}
        	}
        }
        
        
        public bool     useKeyring          {get; set; }
        public bool     allowOther          {get; set; }
        public bool     autoMount           {get; set; }
        public bool     mountWhenAvailable  {get; set; }
        public bool     unmountWhenIdle     {get; set; }
        public int      idleTimeout         {get; set; }
        public bool     showInTrayMenu      {get; set; }
        public bool     useUnmountCommand   {get; set; }
        public string   encfs6config        {get; set; }
        
        public SimpleConf options;
        
        public bool     isMounted           {get; private set; }
        public bool     isMounting          {get; private set; }
        public bool     isAvailable         {get; private set; }

        private HashMap<string,bool> customCommandDisabled;
        private int[] atomics;
        private int busy_id;
        
        public string[] scriptenv = null;
                
        private int     _lock;
        private bool mount_removed;
        
        public TreeIter iter;
        
        private static VolumeMonitor volumemonitor;
        public static HashMap<string,int> scriptStatusCode;
        public static HashMap<string,int> commandSlots;
        
        public static Gtk.ListStore model;
        
        private File encfs_xml;
        
        private bool unmount_failed;
        
        public signal void mountResponse(bool success, string message);
        public signal void changePasswordResponse(bool success, string message);
        public signal void createResponse(bool success, string message);
        
        public static void init() {
            if (model != null)
                return;
                
            scriptStatusCode = new HashMap<string,int> ();
            scriptStatusCode["GENCFSM_CANCEL"] = 13;
            commandSlots = new HashMap<string,int> ();
            commandSlots["pre_mount_command"] = 0;
            commandSlots["post_mount_command"] = 1;
            commandSlots["pre_unmount_command"] = 2;
            commandSlots["post_unmount_command"] = 3;
            
            /* mountDir, isMounted, isAvailable, cryptDir */
            model = new Gtk.ListStore(5, typeof(string), typeof(bool), typeof(bool), typeof(string), typeof(Stash));
            
            volumemonitor = VolumeMonitor.get();
            volumemonitor.mount_added.connect(update_stash);
            volumemonitor.mount_removed.connect(mount_removed_update_stash);
            
            volumemonitor.mount_added.connect( (gmount) => {
            	GnomeEncfsManager.run_custom_command_async( "mount_added_command",
            												gmount.get_root().get_path(),
            												() => {} );
            });
            volumemonitor.mount_removed.connect( (gmount) => {
            	GnomeEncfsManager.run_custom_command_async( "mount_removed_command",
            												gmount.get_root().get_path(),
            												() => {} );
            });
            
            volumemonitor.mount_pre_unmount.connect( (gmount) => {
            	string mountpoint = gmount.get_root().get_path();
            	foreach (Stash s in stashes) {
		        	if (s.cryptDir.has_prefix(mountpoint)) {
		        		s.unmount();
						//print(s.cryptDir + "\n");
		        	}
            	}
            });
            
        }
        
        public static int commandSlotToInt( string cmd ) {
        	foreach( string s in commandSlots.keys ) {
        		if (s == cmd) {
        			return commandSlots[s];
        		}
        	}
        	return -1;
        }
        
        public static string get_mount_dir_base() {
        	string mount_dir_base = settings.get_string("mount-dir-base").strip();
        	if (mount_dir_base == "") {
        	    mount_dir_base = Environment.get_variable("HOME");
        	}
        	
        	if (mount_dir_base[mount_dir_base.length-1] == '/') {
        	    mount_dir_base = mount_dir_base[0:-1];
        	}
        	
            string[] vars = Environment.list_variables();
            foreach (string s in vars) {
            	mount_dir_base = mount_dir_base.replace("$"+s, Environment.get_variable(s));
            }
            return mount_dir_base;
        }
        
        public Stash(string cryptDir, string mountDir, string encfsconfig) {
            stashes_mutex.lock();
            mount_removed = false;
            unmount_failed = false;
            this.cryptDir = cryptDir;
            this.mountDir = mountDir;
            if (encfsconfig == "") {
	            this.encfs6config = ".encfs6.xml";
            } else {
	            this.encfs6config = encfsconfig;
            }
            
            customCommandDisabled = new HashMap<string,bool>();
            
            atomics = new int[commandSlots.size];
            for( int i = 0; i < commandSlots.size; i++ ) {
            	atomics[i] = 1;
            }
            options = new SimpleConf();
            
            allowOther          = false;
            autoMount           = false;
            mountWhenAvailable  = false;
            unmountWhenIdle     = false;
            idleTimeout         = 5;
            useKeyring          = false;
            
            showInTrayMenu      = true;

            isMounting = false;
            isMounted = Encfs.is_mounted(this.mountDir);
            
            encfs_xml = File.new_for_path( cryptDir );
            
            isAvailable = isAvailableReal();
            
            model.append(out iter);

            if (!isAvailable && isMounted) {
                unmount();
            }

            model.set(iter, 0, this.mountDir, 1, isMounted, 2, isAvailable, 3, cryptDir, 4, this, -1);
            stashes_mutex.unlock();
        }
        
        public void after_loading() {
            if (!isMounted) {
		        if (!options.get_bool("keep_mount_directory")) {
		            if (FileUtils.test(mountDir, FileTest.IS_DIR)) {
    		            Posix.rmdir(mountDir);
    		        }
		        } else {
		            if (!FileUtils.test(mountDir, FileTest.IS_DIR)) {
                        try {
                            string exit_stderr;
                            int exit_status;
	                        Process.spawn_command_line_sync( "mkdir -m 700 -p \""+mountDir+"\"" , null, out exit_stderr, out exit_status );
	                    }
	                    catch (Error e) {}
    		        }
		        }
            }
        }
        
        private bool lock() {
            if (AtomicInt.get(ref _lock) == 1) {
                return false;
            }
            AtomicInt.set(ref _lock, 1);
            return true;
        }
        
        private void unlock() {
            AtomicInt.set(ref _lock, 0);
        }
        
        //private bool is_locked() {
        //   return (AtomicInt.get(ref _lock) == 1);
        //}
        
        public static void update_all() {
            //GLib.stdout.printf("update_all\n");
        	//Timeout.add (500, () => {
		        foreach (Stash s in stashes) {
		            s.update();
		        }
		        TrayIcon.update();
		        Manager.update();
		        MountDialog.update();
		    //    return false;
		    //});
        }
        
        public static void update_stash(Mount mount) {
        	string dir = mount.get_root().get_path();
	        foreach (Stash s in stashes) {
	            if ( s.mountDir.has_prefix(dir) || s.cryptDir.has_prefix(dir) ) {
	            	s.update();
	            }
	        }
	        TrayIcon.update();
	        Manager.update();
	        MountDialog.update();
        }
        
        public void update() {
            isMounted = Encfs.is_mounted(mountDir);
            //stdout.printf(mountDir+": " + (isMounted?"true":"false")+"\n");
            //stdout.flush();
            isAvailable = isAvailableReal();
            bool available_pre;
            model.get(iter, 2, out available_pre, -1);
            
            if (isMounted && !isAvailable) {
                if (!unmount_failed) {
                    unmount();
                    return;
                }
            }
            
            else if (mountWhenAvailable && isAvailable) {
            	if (isAvailable != available_pre) {
            		MountDialog.mount(this);
            	}
            }
            
            unmount_failed = false;
            model.set(iter, 1, isMounted, 2, isAvailable, -1);
        }
        
        public static void mount_removed_update_stash(Mount mount) {
            //GLib.stdout.printf("mount_removed_update_stash\n");
        	string dir = mount.get_root().get_path();
	        foreach (Stash s in stashes) {
	            if (s.cryptDir.has_prefix(dir)) {
	            	s.mount_removed_update();
	            } else if (s.mountDir == dir) {
	                s.mount_removed_update_mountDir();
	            }
	        }
	        TrayIcon.update();
	        Manager.update();
	        MountDialog.update();
        }
        
        public void mount_removed_update_mountDir() {
            isMounted = false;
            model.set(iter, 1, isMounted, -1);
	        if (!options.get_bool("keep_mount_directory")) {
	            Posix.rmdir(mountDir);
	        }
            run_custom_command_async("post_unmount_command", ()=>{});
        }
        
        public void mount_removed_update() {
            isMounted = Encfs.is_mounted(mountDir);
            //stdout.printf(mountDir+": " + (isMounted?"true":"false")+"\n");
            //stdout.flush();
            isAvailable = isAvailableReal();
            bool available_pre;
            model.get(iter, 2, out available_pre, -1);
            
            if (isMounted && !isAvailable) {
                unmount();
                isMounted = Encfs.is_mounted(mountDir);
            }
            
            else if (mountWhenAvailable && isAvailable) {
            	if (isAvailable != available_pre) {
            		MountDialog.mount(this);
            	}
            }
            
            model.set(iter, 1, isMounted, 2, isAvailable, -1);
        }
        
        // reads in mtab. use deliberately
        public bool isMountedReal() {
        	return Encfs.is_mounted(mountDir);
        }
        
        public static Gtk.ListStore get_model() {
            return (Gtk.ListStore)model;
        }

        public TreeIter get_iter() {
            return iter;
        }
        
        public void mount(string password) {
            if (!this.lock())
                return;
            if (isMounted) {
                this.unlock();
                return;
            }
            
            isMounting = true;
            EncfsWrapper2.mount(this, password, (success, message) => {
                if (success) {
                    model.set(iter, 1, isMounted, -1);
                    //TrayIcon.update();
                    //ConfigureStash.update();
                    //Manager.update();
                    this.unlock();
                    update();
                    TrayIcon.update();
			        Manager.update();
			        MountDialog.update();
			        isMounting = false;
                    mountResponse(true, message);
                }
                else {
                    this.unlock();
                    isMounting = false;
                    mountResponse(false, message);
                }
            });

            return;

        }
        
        public void mount_cancel() {
        	EncfsWrapper2.mount_cancel();
        }

        public bool _unmount() {
        	if (!isMountedReal() || MountDialog.queue.contains(this)) {
		    	unmount_counter--;
		    	if (unmount_counter >= 1) {
		    		_unmount();
		    	}
        		return true;
        	}

			run_custom_command_async("pre_unmount_command", (status) => {
				if (status == scriptStatusCode["GENCFSM_CANCEL"]) {
					scriptenv = null;
					TrayIcon.update();
					unmount_counter--;
					if (unmount_counter >= 1) {
						_unmount();
					}
					return;
				}
				
		    	if (!isMountedReal() || MountDialog.queue.contains(this)) {
					unmount_counter--;
					if (unmount_counter >= 1) {
						_unmount();
					}
		    	}
				
		        busy_id = busy(Path.get_basename(mountDir) + ": " + _("Unmounting..."));
		        unmount_failed = !EncfsWrapper2.unmount(this);
		        if (!unmount_failed) {
		            if (!options.get_bool("keep_mount_directory")) {
		                Posix.rmdir(mountDir);
		            }
		        }

		        update();
		        TrayIcon.update();
			    Manager.update();
			    MountDialog.update();

		        busy(busy_id.to_string());
		        run_custom_command_async("post_unmount_command", ()=>{});
		        scriptenv = null;
		    	unmount_counter--;
		    	if (unmount_counter >= 1) {
		    		_unmount();
		    	}
		        
			});
            return true;
        }
        
        int unmount_counter = 0;
        public void unmount() {
            update();
            if (!isMounted) return;
        	if (unmount_counter >=2) return;
        	unmount_counter++;
        	if (unmount_counter == 1) {
        		_unmount();
        	}
        }
        
        /* return true when success, return false and place error message in output when error*/
        public void changePassword(string old_password, string new_password) {
            EncfsWrapper2.change_password(this, old_password, new_password, (status, msg) => {
            	if (status > 0) {
		            string output = _("An error occured. Wrong password?");
		            changePasswordResponse( false, output );
		        }
		        else {
		            changePasswordResponse( true, "" );
		        }
            });
        }
        
        public string getInfo() {
            return EncfsWrapper2.get_info(this);
        }
        
        
        public void create(string password, bool disable_filename_encryption) {
        	EncfsWrapper2.create( this, password, disable_filename_encryption, (status, message) => {
        		if (status == 0) {
		            createResponse(true, message);
        		} else {
	        		createResponse(false, message);
	        	}
        	});
        }
        
        public bool changeMountDir(string newMountDir, out string output) {
            output = "";
            if (isMounted) {
                output = _("Cannot change the mount directory while the stash is mounted");
                return false;
            }
            
            mountDir = newMountDir;
            model.set(iter, 0, mountDir, -1);
            return true;
        }
        
        public bool isAvailableReal() {
            if (mount_removed) {
                mount_removed = false;
                return false;
            }
            
            if (encfs6config == "") {
                return true;
            }
        
            if (encfs_xml.query_file_type(FileQueryInfoFlags.NONE, null) != FileType.DIRECTORY) {
            	return false;
            }
            
            if (encfs6config == ".encfs5") {
                return true;
            }
            
			try {
				string contents = null;

				if (encfs6config[0] == '/' || encfs6config[0] == '~') {
					FileUtils.get_contents(encfs6config, out contents);
				} else {
					FileUtils.get_contents(Path.build_filename(cryptDir, encfs6config), out contents);
				}
		    	
				if (!(contents.split("<").length == contents.split(">").length)) {
					return false;
				}
				
				return true;
				
			} catch (Error e) {
		        return false;
		    }
        }
        
        public string env_subst(string text) {
        	string output = GnomeEncfsManager.env_subst(text);
        	
        	return output;
        }
        
        public int run_custom_command_sync( string variable ) {
        	string command = options.get_string(variable);
        	if (command == "") {
        		return 0;
        	}

			int ret = 0;
        	ArrayList<string> env = new ArrayList<string> ();
			string[] vars = Environment.list_variables();

			foreach (string xvar in vars) {
				env.add(xvar+"="+Environment.get_variable(xvar));
				command = command.replace("$"+xvar, Environment.get_variable(xvar));
			}
        	env.add("MOUNT_DIR="+mountDir);
        	env.add("STASH_DIR="+cryptDir);
        	env.add("SCRIPT_TYPE="+variable);
        	foreach (var entry in scriptStatusCode.entries) {
        		env.add(entry.key+"="+entry.value.to_string());
        	}
			command = command.replace("$MOUNT_DIR", mountDir);
			command = command.replace("$STASH_DIR", cryptDir);
        	
        	GLib.stdout.printf("\n=== Command: %s\n", command);
        	try {
        		string[] argvp;
        		Shell.parse_argv( command, out argvp );
		    	Process.spawn_sync(null, argvp, arrayList2Array(env),
		    						SpawnFlags.SEARCH_PATH,
		    						null, null, null, out ret);
        	} catch (Error e) {
        		GLib.stderr.printf("%s\n", e.message);
        	}
        	return Process.exit_status(ret);
        }

        public delegate void custom_command_callback(int status);
        public void run_custom_command_async( string variable, custom_command_callback callback ) {
        	string command = options.get_string(variable);
        	
        	if (options.get_bool("open_file_manager") && variable == "post_mount_command") {
        		if (command == "") {
        			command = GnomeEncfsManager.options.get_string("file_manager_command");
        			if (command == "") {
        				command = "xdg-open \"$MOUNT_DIR\"";
        			}
        		}
        	}
        	
        	if (command == "") {
        		callback(0);
        		return;
        	}
        	if (!AtomicInt.dec_and_test( ref atomics[commandSlotToInt(variable)] )) {
        		AtomicInt.inc( ref atomics[commandSlotToInt(variable)] );
        		callback(0);
        		return;
        	}
			
        	ArrayList<string> env = new ArrayList<string> ();
			string[] vars = Environment.list_variables();

			foreach (string xvar in vars) {
				env.add(xvar+"="+Environment.get_variable(xvar));
				command = command.replace("$"+xvar, Environment.get_variable(xvar));
			}
			
			if (scriptenv != null) {
				foreach (string xvar in scriptenv) {
					env.add(xvar);
				}
			}
			
        	env.add("MOUNT_DIR="+mountDir);
        	env.add("STASH_DIR="+cryptDir);
        	env.add("SCRIPT_TYPE="+variable);
        	foreach (var entry in scriptStatusCode.entries) {
        		env.add(entry.key+"="+entry.value.to_string());
        	}
			command = command.replace("$MOUNT_DIR", mountDir);
			command = command.replace("$STASH_DIR", cryptDir);
        	
        	GLib.stdout.printf("\n=== Command: %s\n", command);
        	Pid pid;
        	try {
        		string[] argvp;
        		Shell.parse_argv( command, out argvp );
		    	Process.spawn_async(null, argvp, arrayList2Array(env),
		    						SpawnFlags.SEARCH_PATH | SpawnFlags.DO_NOT_REAP_CHILD,
		    						null, out pid);
		    						
		    	ChildWatch.add(pid, (pid, status) => {
		    		AtomicInt.inc( ref atomics[commandSlotToInt(variable)] );
		    		if (callback != null)
		    			callback(Process.exit_status(status));
		    	});
        	} catch (Error e) {
        		GLib.stderr.printf("%s\n", e.message);
				AtomicInt.inc( ref atomics[commandSlotToInt(variable)] );
				if (callback != null)
					callback(0);
        	}
        }
        
        public void destroy() {
            if (!this.lock())
                return;
            stashes_mutex.lock();
            #if VALA_0_36
                model.remove(ref iter);
            #else
                model.remove(iter);
            #endif
            stashes_mutex.unlock();
            this.unlock();
        }
    }
}

