#!/usr/bin/python3
from typing import List
import gi; gi.require_version("Gtk", "4.0")
from gi.repository import Nautilus, GObject, Gio, Gtk
import subprocess
import os
import gettext; _ = gettext.gettext; gettext.textdomain("nautilus-encryption-plugin")

MOUNT_BASEDIR = os.getenv("HOME") + "/GCryFolder"
TERMINAL_CMDLINE = "kgx"

class NautilusFoldersExtension(GObject.GObject, Nautilus.MenuProvider):
    def ask_password(self, title: str, cb):
        self.password_dialog = Gtk.Window(title=title)
        self.password_dialog.set_icon_name("nautilus-encryption-plugin")
        box = Gtk.Box(orientation = Gtk.Orientation.VERTICAL, spacing=10)
        box.set_margin_start(10); box.set_margin_end(10); box.set_margin_top(10); box.set_margin_bottom(10)
        self.password_dialog.set_child(box)
        prompt_label = Gtk.Label(); prompt_label.set_markup(_("Enter password for") + " <span weight='bold'>" + self.path + "</span>")
        box.append(prompt_label)
        self.password_entry = Gtk.PasswordEntry()
        self.password_entry.set_show_peek_icon(True)
        self.password_entry.set_property("activates_default", True)
        self.password_entry.connect("changed", lambda e: self.password_error_label.set_text(""))
        box.append(self.password_entry)
        self.password_error_label = Gtk.Label(label = " ")
        box.append(self.password_error_label)
        buttons = Gtk.Box(orientation = Gtk.Orientation.HORIZONTAL, spacing=10)
        cancel_button = Gtk.Button(label=_("Cancel")); cancel_button.set_hexpand(True)
        cancel_button.connect("clicked", lambda bt: self.password_dialog.set_visible(False))
        buttons.append(cancel_button)
        ok_button = Gtk.Button(label=_("OK")); ok_button.set_hexpand(True)
        ok_button.connect("clicked",cb)
        buttons.append(ok_button)
        self.password_dialog.set_default_widget(ok_button)
        box.append(buttons)
        self.password_dialog.set_resizable(False)
        self.password_dialog.present()

    def ask_filename(self, cb):
        self.newfile_dialog = Gtk.Window(title=_("New file"))
        self.newfile_dialog.set_icon_name("nautilus-encryption-plugin")
        box = Gtk.Box(orientation = Gtk.Orientation.VERTICAL, spacing=10)
        box.set_margin_start(10); box.set_margin_end(10); box.set_margin_top(10); box.set_margin_bottom(10)
        self.newfile_dialog.set_child(box)
        prompt_label = Gtk.Label(label = _("Enter new file name"));
        box.append(prompt_label)
        self.filename_entry = Gtk.Entry()
        self.filename_entry.set_property("activates_default", True)
        box.append(self.filename_entry)
        self.fileexists_error_label = Gtk.Label(label = " ")
        box.append(self.fileexists_error_label)
        buttons = Gtk.Box(orientation = Gtk.Orientation.HORIZONTAL, spacing=10)
        cancel_button = Gtk.Button(label=_("Cancel")); cancel_button.set_hexpand(True)
        cancel_button.connect("clicked", lambda bt: self.newfile_dialog.set_visible(False))
        buttons.append(cancel_button)
        ok_button = Gtk.Button(label=_("OK")); ok_button.set_hexpand(True)
        ok_button.connect("clicked", cb)
        buttons.append(ok_button)
        self.newfile_dialog.set_default_widget(ok_button)
        box.append(buttons)
        self.newfile_dialog.set_resizable(False)
        self.newfile_dialog.present()
    
    def give_feedback(self, ret: int, msg: str):
        if ret == 0:
            self.password_dialog.set_visible(False)
            subprocess.Popen(["notify-send", "-t", "5000", msg])
        else:
            self.password_error_label.set_markup("<span foreground='red' weight='bold'>" + _("Wrong password.") + "</span>")



    def do_mount_folder_encfs(self, bt: Gtk.Button):
        stdin = self.password_entry.get_text()
        self.do_mkdir()
        mount = MOUNT_BASEDIR + "/" + os.path.basename(self.path)
        ps = subprocess.run(["encfs", "--stdinpass", self.path, mount], input=stdin, text=True, check=False) #FIXME: tryed in an other thread or process without success
        self.give_feedback(ps.returncode, _("Folder mounted in {}.").format(mount))

    def do_unlock_folder_fscrypt(self, bt: Gtk.Button):
        stdin = self.password_entry.get_text()
        protectors = self.get_fscrypt_protectors()
        ret = -1; i = 0
        while ret != 0 and i != len(protectors):
            ps = subprocess.Popen(["fscrypt", "unlock", self.path, "--unlock-with=" + protectors[i]], stdin=subprocess.PIPE)
            ps.communicate(input=(stdin + "\n").encode("utf-8")) #Ctrl+C = 0x03
            ret = ps.returncode; i += 1
        self.give_feedback(ps.returncode, _("Folder {} unlocked.").format(self.path))

    def do_encrypt_folder_encfs(self, bt: Gtk.Button):
        self.do_mkdir()
        mount = MOUNT_BASEDIR + "/" + os.path.basename(self.path)
        stdin = self.password_entry.get_text() + "\n" + self.password_entry.get_text()
        self.password_dialog.set_visible(False)
        subprocess.run(["encfs", "--standard", "--stdinpass", self.path, mount], input=stdin, text=True, check=False)
        subprocess.Popen(["notify-send", "-t", "5000", _("Folder {} encrypted.").format(self.path)])
    
    def do_encrypt_folder_fscrypt(self, bt: Gtk.Button):
        stdin = self.password_entry.get_text()
        name = self.path.replace("/", "_")
        self.password_dialog.set_visible(False)
        subprocess.run(["fscrypt", "encrypt", self.path, "--name=" + name, "--no-recovery", "--quiet"], input=stdin, text=True, check=False)
        subprocess.Popen(["notify-send", "-t", "5000", _("Folder {} encrypted.").format(self.path)])
    
    def get_fscrypt_protectors(self) -> List[str]:
        protectors = []
        lines = subprocess.run(["fscrypt", "status", self.path, "--verbose"], capture_output=True, text=True).stdout.split("\n")
        for line in lines:
            if line.find("/protectors/") != -1:
                line = line[line.find("\"")+1:-1]
                protectors.append(line[:line.find("/.fscrypt")] + ":" + line[line.find("/protectors/")+12:])
        return protectors        

    def do_destroy_folder_fscrypt(self):
        pass #TODO disabled: confirmation required
        name = self.path.replace("/", "_")
        search = subprocess.run(["grep", "-R", name, "/home/.fscrypt/protectors/"], capture_output=True, text=True).stdout #FIXME: /home
        protector = os.path.basename(search.split(":")[0])
        subprocess.run(["fscrypt", "metadata", "destroy", "--protector=/home:" + protector, "--force"])
        subprocess.Popen(["rm", "-rf", self.path])
    
    def do_mkdir(self):
        if not os.path.isdir(MOUNT_BASEDIR):
            os.mkdir(MOUNT_BASEDIR)
        mount = MOUNT_BASEDIR + "/" + os.path.basename(self.path)
        if not os.path.isdir(mount):
            os.mkdir(mount)
    
    def do_create_file(self, bt: Gtk.Button):
        filename = self.filename_entry.get_text()
        if not os.path.exists(self.viewpath + "/" + filename):
            mask = os.umask(0o000)
            os.mknod(self.viewpath + "/" + filename, mode = 0o666 - mask)
            os.umask(mask)
            self.newfile_dialog.set_visible(False)
        else:
            self.fileexists_error_label.set_markup("<span foreground='blue' weight='bold'>" + _("File already exists.") + "</span>")


    
    #def menu_activate_open_terminal(self, menu: Nautilus.MenuItem, file: Nautilus.FileInfo):
    #    subprocess.Popen(TERMINAL_CMDLINE.split(" ") + [self.viewpath])
    
    def menu_activate_new_file(self, menu: Nautilus.MenuItem, file: Nautilus.FileInfo):
        self.ask_filename(self.do_create_file)
        
    def menu_activate_mount_encfs(self, menu: Nautilus.MenuItem, file: Nautilus.FileInfo):
        self.ask_password(_("Open encrypted folder"), self.do_mount_folder_encfs)
    
    def menu_activate_unlock_fscrypt(self, menu: Nautilus.MenuItem, file: Nautilus.FileInfo):
        self.ask_password(_("Open encrypted folder"), self.do_unlock_folder_fscrypt)
    
    def menu_activate_lock_fscrypt(self, menu: Nautilus.MenuItem, file: Nautilus.FileInfo):
        subprocess.run(["fscrypt", "lock", self.path], check=False)
        subprocess.Popen(["notify-send", "-t", "5000", _("Folder {} locked.").format(self.path)])

    def menu_activate_create_encfs(self, menu: Nautilus.MenuItem, file: Nautilus.FileInfo):
        self.ask_password(_("Encrypt folder (encfs)"), self.do_encrypt_folder_encfs)
    
    def menu_activate_create_fscrypt(self, menu: Nautilus.MenuItem, file: Nautilus.FileInfo):
        self.ask_password(_("Encrypt folder (fscrypt)"), self.do_encrypt_folder_fscrypt)
    
    def menu_activate_destroy_fscrypt(self, menu: Nautilus.MenuItem, file: Nautilus.FileInfo):
        self.do_destroy_folder_fscrypt()



    def get_file_items(self, files: List[Nautilus.FileInfo]) -> List[Nautilus.MenuItem]:
        menu = []
        if len(files) == 1 and files[0].is_directory():
            directory = files[0]
            self.path = directory.get_location().get_path()
            if self.is_initialized():
                if not self.is_mounted():
                    if self.is_fscrypt():
                        item = Nautilus.MenuItem(name="Nautilus::unlock-fscrypt", label=_("Unlock encrypted folder"))
                        item.connect("activate", self.menu_activate_unlock_fscrypt, files[0])
                        menu.append(item)
                        #item = Nautilus.MenuItem(name="Nautilus::destroy-fscrypt", label=_("Destroy encrypted folder"))
                        #item.connect("activate", self.menu_activate_destroy_fscrypt, files[0])
                        #menu.append(item)
                    else:
                        item = Nautilus.MenuItem(name="Nautilus::mount-encfs", label=_("Mount encrypted folder"))
                        item.connect("activate", self.menu_activate_mount_encfs, files[0])
                        menu.append(item)
                else:
                    if self.is_fscrypt(): #mounted fscrypt
                        item = Nautilus.MenuItem(name="Nautilus::lock-fscrypt", label=_("Lock encrypted folder"))
                        item.connect("activate", self.menu_activate_lock_fscrypt, files[0])
                        menu.append(item)
                    #else: #encfs
                        #TODO ?: add fusermount -u action
            else:
                if self.is_empty():
                    item = Nautilus.MenuItem(name="Nautilus::encrypt-create-encfs", label=_("Encrypt folder (encfs)"))
                    item.connect("activate", self.menu_activate_create_encfs, files[0])
                    menu.append(item)
                    item = Nautilus.MenuItem(name="Nautilus::encrypt-create-fscrypt", label=_("Encrypt folder (fscrypt)"))
                    item.connect("activate", self.menu_activate_create_fscrypt, files[0])
                    menu.append(item)
        return menu

    def get_background_items(self, directory: Nautilus.FileInfo) -> List[Nautilus.MenuItem]:
        self.viewpath = directory.get_location().get_path()
        #item_terminal = Nautilus.MenuItem(name="Nautilus::open-terminal", label=_("Open terminal here"))
        #item_terminal.connect("activate", self.menu_activate_open_terminal, directory)
        item_new_file = Nautilus.MenuItem(name="Nautilus::new-file", label=_("New empty file"))
        item_new_file.connect("activate", self.menu_activate_new_file, directory)
        #return [ item_terminal, item_new_file ]
        return [ item_new_file ]



    def is_initialized(self) -> bool:
        return self.is_fscrypt() or self.is_encfs() 
    
    def is_fscrypt(self) -> bool:
        return subprocess.run(["fscrypt", "status", self.path]).returncode == 0
    
    def is_encfs(self) -> bool:
        return os.path.isfile(self.path + "/.encfs6.xml")
    
    def is_mounted(self) -> bool:
        if self.is_fscrypt():
            mounted = subprocess.run(["fscrypt", "status", self.path], capture_output=True, text=True).stdout.find("Unlocked: Yes") != -1
        else: #encfs
            mounted = False
            expected_mount_path = MOUNT_BASEDIR + "/" + os.path.basename(self.path)
            for mount in open("/proc/mounts").read().split("\n")[:-1]:
                mount_path = mount.split(" ")[1]
                if mount_path == expected_mount_path:
                    mounted = True
                    break
        return mounted
    
    def is_empty(self) -> bool:
        return len(os.listdir(self.path)) == 0

