#!/usr/bin/python3
import glob
import hashlib
import os
import shutil
import subprocess
import sys
import threading

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Pango, Gdk, GLib

MOUNT_PATH = '/mnt/pw_ramdisk'
PWD_FILE_PATH = '/home/dsorber/Desktop/network'

ENC_FILE_PATH = '/home/dsorber/Documents/chicken_soup.enc'
SCRATCH_FILE_PATH = '/mnt/pw_ramdisk/chicken_soup.txt'
ENC_TMP_FILE_PATH = '/home/dsorber/Documents/chicken_soup.enc.tmp'
KEY_FILE_PATH = '/mnt/pw_ramdisk/.f05d11ef-000c-45c2-ae14-56b56c23e1c5'

VERSION = '0.0.8'

class SearchDialog(Gtk.Dialog):

    def __init__(self, parent):
        Gtk.Dialog.__init__(self, 'Search', parent,
            Gtk.DialogFlags.MODAL, buttons=(
            Gtk.STOCK_FIND, Gtk.ResponseType.OK,
            Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL))

        box = self.get_content_area()

        label = Gtk.Label('Insert text you want to search for:')
        box.add(label)

        self.entry = Gtk.Entry()
        self.entry.set_activates_default(True)
        box.add(self.entry)

        # Make hitting ENTER trigger the OK button
        ok_button = self.get_widget_for_response(response_id=Gtk.ResponseType.OK)
        ok_button.set_can_default(True)
        ok_button.grab_default()

        self.show_all()
        
        
class WarningDialog(Gtk.MessageDialog):
    
    def __init__(self, parent, title, message):
        Gtk.MessageDialog.__init__(self, parent,
                                   Gtk.DialogFlags.MODAL, 
                                   Gtk.MessageType.WARNING,
                                   Gtk.ButtonsType.OK_CANCEL,
                                   message)

        self.set_title(title)
        
        # Make hitting ENTER trigger the OK button
        ok_button = self.get_widget_for_response(response_id=Gtk.ResponseType.OK)
        ok_button.set_can_default(True)
        ok_button.grab_default()
        
        self.show_all()
        
def get_user_pw(parent, message, title=''):
    """
    Returns user input as a string or None
    If user does not input text it returns None, NOT AN EMPTY STRING.
    
    https://stackoverflow.com/questions/13970445/python-gtk3-how-to-add-a-gtk-entry-to-a-gtk-messagedialog
    """
    dialog = Gtk.MessageDialog(parent,
                               Gtk.DialogFlags.MODAL | 
                               Gtk.DialogFlags.DESTROY_WITH_PARENT,
                               Gtk.MessageType.QUESTION,
                               Gtk.ButtonsType.OK_CANCEL,
                               message)
    
    dialog.set_title(title)

    box = dialog.get_content_area()
    box.set_border_width(10)
    pw_entry = Gtk.Entry()
    pw_entry.set_visibility(False)
    pw_entry.set_invisible_char('*')
    pw_entry.set_size_request(200, 0)
    pw_entry.set_activates_default(True)
    
    box.add(pw_entry)
    #~ box.pack_end(pw_entry, False, False, 0)
    
    # Make hitting ENTER trigger the OK button
    ok_button = dialog.get_widget_for_response(response_id=Gtk.ResponseType.OK)
    ok_button.set_can_default(True)
    ok_button.grab_default()

    dialog.show_all()
    response = dialog.run()
    text = pw_entry.get_text() 
    dialog.destroy()
    
    if (response == Gtk.ResponseType.OK) and (text != ''):
        return text
    else:
        return None
        
def yield_gtk():
    while Gtk.events_pending():
            Gtk.main_iteration()
    
class ProgressBarWindow(Gtk.Window):

    def __init__(self, parent, title):
        Gtk.Window.__init__(self, title=title)
        self.set_border_width(10)

        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=3)
        self.add(vbox)

        self.progressbar = Gtk.ProgressBar()
        vbox.pack_start(self.progressbar, True, True, 0)

        self.progressbar.set_text('Zee text')
        self.progressbar.set_show_text(True)
        
        self.set_modal(True)
        #~ self.set_decorated(False)
        self.set_deletable(False)
        self.set_transient_for(parent)


    def update_text(self, text):
        self.progressbar.set_text(text)

    def update_complete(self, fraction):
        self.progressbar.set_fraction(fraction)


class TextViewWindow(Gtk.Window):

    def __init__(self):
        Gtk.Window.__init__(self, title='pwmgr')

        self.set_default_size(800, 600)

        self.grid = Gtk.Grid()
        self.add(self.grid)

        self.create_textview()
        self.create_toolbar()
        self.create_bottom()
        self.connect("key-press-event", self._key_press_event)
        
        self.mounted = False
        self.pass_data_loaded = False
        self.pwd_file_fd = None
        self.original_hash = None
        self.pbar_win = None

        # Open password prompt window at start
        self.open_pwd_file(self.grid)
        yield_gtk()

    def _key_press_event(self, widget, event):
        keyval = event.keyval
        keyval_name = Gdk.keyval_name(keyval)
        state = event.state
        
        ctrl = (state & Gdk.ModifierType.CONTROL_MASK)
        
        # Bind existing handlers to keys
        if ctrl and keyval_name == 'f':
            self.on_search_clicked(widget)
        elif ctrl and keyval_name == 'o':
            self.open_pwd_file(widget)
        elif ctrl and keyval_name == 's':
            self.save_pwd_file(widget)
        elif ctrl and keyval_name == 'q':
            self.close_pwd_file(widget, None)
        else:
            return False
        return True

    def create_toolbar(self):
        toolbar = Gtk.Toolbar()
        self.grid.attach(toolbar, 0, 0, 3, 1)

        button_open = Gtk.ToolButton()
        button_open.set_icon_name('document-open')
        toolbar.insert(button_open, 0)        
        button_open.connect('clicked', self.open_pwd_file)
        
        button_save = Gtk.ToolButton()
        button_save.set_icon_name('document-save')
        toolbar.insert(button_save, 1)
        button_save.connect('clicked', self.save_pwd_file)
        
        button_close = Gtk.ToolButton()
        button_close.set_icon_name('window-close')
        toolbar.insert(button_close, 2)
        button_close.connect('clicked', self.close_pwd_file, None)

        toolbar.insert(Gtk.SeparatorToolItem(), 3)

        button_clear = Gtk.ToolButton()
        button_clear.set_icon_name('edit-clear-symbolic')
        button_clear.connect('clicked', self.on_clear_clicked)
        toolbar.insert(button_clear, 4)

        toolbar.insert(Gtk.SeparatorToolItem(), 5)

        button_search = Gtk.ToolButton()
        button_search.set_icon_name('system-search-symbolic')
        button_search.connect('clicked', self.on_search_clicked)
        toolbar.insert(button_search, 6)

    def create_textview(self):
        scrolledwindow = Gtk.ScrolledWindow()
        scrolledwindow.set_hexpand(True)
        scrolledwindow.set_vexpand(True)
        self.grid.attach(scrolledwindow, 0, 1, 3, 1)

        self.textview = Gtk.TextView()
        self.textview.set_editable(False)
        self.textview.set_cursor_visible(False)
        self.textbuffer = self.textview.get_buffer()
        scrolledwindow.add(self.textview)

        self.tag_bold = self.textbuffer.create_tag("bold",
            weight=Pango.Weight.BOLD)
        self.tag_italic = self.textbuffer.create_tag("italic",
            style=Pango.Style.ITALIC)
        self.tag_underline = self.textbuffer.create_tag("underline",
            underline=Pango.Underline.SINGLE)
        self.tag_found = self.textbuffer.create_tag('found', 
                                                    background='orange',
                                                    weight=Pango.Weight.BOLD)

    def create_bottom(self):
        
        label = Gtk.Label()
        label.set_text('version: {:s}'.format(VERSION))
        label.set_justify(Gtk.Justification.RIGHT)
        
        self.grid.attach(label, 0, 2, 3, 1)

    def open_pwd_file(self, widget):
        """ 
        Open handler
        """

        # Get password
        if not self.pass_data_loaded:
            passwd = get_user_pw(self, 'Please enter your password', 'Password')
            if passwd is None:
                return
        
        if not self.mounted:
            os.makedirs(MOUNT_PATH, 0o600, exist_ok=True)
            
            # Mount the tmpfs
            mnt_cmd = 'mount -t tmpfs -o size=16m tmpfs {:s}'
            mnt_cmd = mnt_cmd.format(MOUNT_PATH)
            subprocess.call(mnt_cmd, shell=True, timeout=5)
            self.mounted = True
            
            # Store password
            with open(KEY_FILE_PATH, 'w') as pass_wd_file:
                pass_wd_file.write(passwd)
            os.chmod(KEY_FILE_PATH, 0o600)
            
            # Decrypt password store
            cmd = 'openssl enc -aes-256-cbc -d -in {:s} -out {:s} -pass file:{:s}'
            cmd = cmd.format(ENC_FILE_PATH, SCRATCH_FILE_PATH, KEY_FILE_PATH)
            subprocess.call(cmd, shell=True, timeout=5)
            
            # Open and load file; make textview editable
            with open(SCRATCH_FILE_PATH, 'r') as pwd_file_fd:
                self.textbuffer.set_text(pwd_file_fd.read())
            self.textview.set_editable(True)
            self.textview.set_cursor_visible(True)
            
            # Store hash of the original content which is used to detect changes
            self.original_hash = self._hash_textview()
            
            # Mark state that pasword data is loaded
            self.pass_data_loaded = True
        
    def save_pwd_file(self, widget):
        """
        Save handler
        """
        
        if self.mounted and self.pass_data_loaded:
            # Check if changes have been made
            current_hash = self._hash_textview()
            if self.original_hash.hexdigest() != current_hash.hexdigest():
                # Changes were made, must save data
                # Write the password store data to the scratch file
                with open(SCRATCH_FILE_PATH, 'w') as pwd_file_fd:
                    pwd_file_fd.write(
                        self.textbuffer.get_text(self.textbuffer.get_start_iter(),
                                                 self.textbuffer.get_end_iter(), 
                                                 True))
                print('Write complete')
                
                # Encrypt the password store
                cmd = 'openssl enc -aes-256-cbc -salt -in {:s} -out {:s} -pass'\
                      ' file:{:s}'
                cmd = cmd.format(SCRATCH_FILE_PATH, ENC_TMP_FILE_PATH, 
                                 KEY_FILE_PATH)
                subprocess.call(cmd, shell=True, timeout=5)
                print('Enc complete')
                
                # Remove the key file, then copy over the encrypted file with 
                # the just created temp one
                os.remove(KEY_FILE_PATH)
                os.remove(ENC_FILE_PATH)
                os.rename(ENC_TMP_FILE_PATH, ENC_FILE_PATH)
            
            # Clear the buffer and disable the textview
            self.textbuffer.set_text('')
            self.textview.set_editable(False)
            self.textview.set_cursor_visible(False)
            
            # Mark password data as not loaded
            self.pass_data_loaded = False
    
    def _remove_mount_files(self):
        # Remove files in the mount path
        for file_object in os.listdir(MOUNT_PATH):
            file_object_path = os.path.join(MOUNT_PATH, file_object)
            if os.path.isfile(file_object_path):
                os.unlink(file_object_path)
            else:
                shutil.rmtree(file_object_path)
    
    def _hash_textview(self):
        passwd_text = self.textbuffer.get_text(self.textbuffer.get_start_iter(),
                                               self.textbuffer.get_end_iter(), 
                                               True)
        return hashlib.sha256(bytearray(passwd_text, 'utf-8'))
    
    def do_mount_cleanup(self):
        """
        Clean up ramdisk by overwriting, then unmount
        """
        self.pbar_win.update_text('Starting ramdisk cleanup')
        yield_gtk()
        
        # Build shell commands to write random and zero data (respectively) to
        # the file
        rand_write_cmd = 'dd if=/dev/urandom of={:s} bs=16M count=1 status=none'
        rand_write_cmd = rand_write_cmd.format(os.path.join(MOUNT_PATH, 'data'))
        
        zero_write_cmd = 'dd if=/dev/zero of={:s} bs=16M count=1 status=none'
        zero_write_cmd = zero_write_cmd.format(os.path.join(MOUNT_PATH, 'data'))
        
        self._remove_mount_files()
            
        # Overwrite the ramdisk a few times
        for ctr in range(10):
            print(ctr)
            message = 'Overwrite {:d} of 10'.format(ctr +  1)
            self.pbar_win.update_text(message)
            yield_gtk()
            
            # Do one pass, write random data then write zeros
            subprocess.call(rand_write_cmd, shell=True, timeout=5)
            self._remove_mount_files()
            subprocess.call(zero_write_cmd, shell=True, timeout=5)
            self._remove_mount_files()
            
            # Update progress bar
            self.pbar_win.update_complete((ctr + 1) / 10)
            yield_gtk()
        
        self.pbar_win.update_text('Unmounting ramdisk')
        yield_gtk()
        
        # Unmount the ramdisk
        umount_cmd = 'umount {:s}'.format(MOUNT_PATH)
        subprocess.call(umount_cmd, shell=True, timeout=5)

        self.pbar_win.update_text('Complete')
        yield_gtk()
    
    def close_pwd_file(self, widget, other):
        """
        Close handler
        """
        if self.pass_data_loaded:
            # Hash the current text to see if any changes have been made
            current_hash = self._hash_textview()
            if current_hash.hexdigest() != self.original_hash.hexdigest():
                # Changes have been made, ask the user to save
                dialog = WarningDialog(self, 'Save changes?', 
                                       'Save changes to password data?')
                response = dialog.run()
                dialog.destroy()
        
                if response == Gtk.ResponseType.OK:
                    self.save_pwd_file(widget)
        
        # Clear the buffer and disable the textview
        self.textbuffer.set_text('')
        self.textview.set_editable(False)
        self.textview.set_cursor_visible(False)
        
        if self.mounted:
            # Create the progress window
            self.pbar_win = ProgressBarWindow(self, 'Closing...')
            self.pbar_win.show_all()
            yield_gtk()
            
            self.do_mount_cleanup()
            
        # Quit GTK app
        Gtk.main_quit()
        print('CLOSE')

    def on_clear_clicked(self, widget):
        start = self.textbuffer.get_start_iter()
        end = self.textbuffer.get_end_iter()
        self.textbuffer.remove_all_tags(start, end)

    def on_search_clicked(self, widget):
        dialog = SearchDialog(self)
        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            cursor_mark = self.textbuffer.get_insert()
            start = self.textbuffer.get_iter_at_mark(cursor_mark)
            if start.get_offset() == self.textbuffer.get_char_count():
                start = self.textbuffer.get_start_iter()

            self.search_and_mark(dialog.entry.get_text(), start)

        dialog.destroy()

    def search_and_mark(self, text, start):
        # Remove existing marks before starting new search
        self.textbuffer.remove_all_tags(self.textbuffer.get_start_iter(), 
                                        self.textbuffer.get_end_iter())
        
        end = self.textbuffer.get_end_iter()
        match = start.forward_search(text, Gtk.TextSearchFlags.CASE_INSENSITIVE, end)

        if match != None:
            match_start, match_end = match
            self.textbuffer.apply_tag(self.tag_found, match_start, match_end)
            
            # Scroll so the match appears in the middle of the screen
            self.textview.scroll_to_iter(match_start, 0.1, True, 0.0, 0.5)
            #~ self.search_and_mark(text, match_end)

def main():
    """
    NOTE: this *must* be called using sudo or gksudo
    """
    win = TextViewWindow()
    win.connect('delete-event', win.close_pwd_file)
    win.show_all()
    Gtk.main()
    
if __name__ == '__main__':
    sys.exit(main())
