#!/usr/bin/env python3
# -*- coding: utf-8 -*-

#
# Author: Serg Kolo , contact: 1047481448@qq.com
# Date: November 19 , 2016
# Purpose: appindicator for accessing files and folders
# Tested on: Ubuntu 16.04 LTS
#
#
# Licensed under The MIT License (MIT).
# See included LICENSE file or the notice below.
#
# Copyright © 2016 Sergiy Kolodyazhnyy
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import gi
gi.require_version('AyatanaAppIndicator3', '0.1')
gi.require_version('Notify', '0.7')
gi.require_version('GdkPixbuf', '2.0')
from gi.repository import GLib as glib
from gi.repository import AyatanaAppIndicator3 as appindicator
from gi.repository import Gtk as gtk
from gi.repository import Gio
from gi.repository import Notify
from gi.repository.GdkPixbuf import Pixbuf as gdkPixbuf
from collections import OrderedDict
import urllib.parse
import signal
import subprocess
import copy
import shutil
import dbus
import math
import json
import os

class FilesIndicator(object):

    def __init__(self):
        self.app = appindicator.Indicator.new(
            'files-indicator', "document-open-recent",
            appindicator.IndicatorCategory.HARDWARE
        )
        self.user_home = os.path.expanduser('~')
        filename = '.pinned_files.json'
        self.pinned_list = os.path.join(self.user_home,filename)

        self.config = os.path.join(self.user_home,'.files_indicator.json')
        self.read_config()

        self.app.set_status(appindicator.IndicatorStatus.ACTIVE)

        self.cached_files = self.get_recent_files()
        self.make_menu()
        self.update()

    def set_defaults(self,*args):
        self.max_items = 15
        self.name_length = 20
        self.recent_files_in_main_menu = False
        
    def read_config(self,*args):
        self.set_defaults()
        config = {}
        try:
            with open(self.config) as f:
                 config = json.load(f)

        except FileNotFoundError:
            print('>>> ',self.config,' not found.Creating one')
            f = open(self.config,'w')
            config = {'max_items':self.max_items,
                      'name_length':self.name_length,
                      'recent_files_in_main_menu':self.recent_files_in_main_menu
            }
            json.dump(config,f,indent=4)
            f.close()
        except json.JSONDecodeError:
            print(">>> Can't read ",self.pinned_list,',may be corrupt')
            return None
        else:
            self.max_items = config['max_items']
            self.name_length = config['name_length']
            self.recent_files_in_main_menu =  config['recent_files_in_main_menu']
            # TODO: check for missing config keywords in already existing file
            #    (throws KeyError exception)

    def add_menu_item(self, menu_obj, item_type, image, label, action, args):
        """ dynamic function that can add menu items depending on
            the item type and other arguments"""
        menu_item, icon = None, None
        if item_type is gtk.ImageMenuItem and label:
            menu_item = gtk.ImageMenuItem.new_with_label(label)
            menu_item.set_always_show_image(True)
            if isinstance(image, gdkPixbuf)  :
                icon = gtk.Image.new_from_pixbuf(image)
            elif '/' in image:
                icon = gtk.Image.new_from_file(image)
            else:
                icon = gtk.Image.new_from_icon_name(image, 48)
            menu_item.set_image(icon)
        elif item_type is gtk.ImageMenuItem and not label:
            menu_item = gtk.ImageMenuItem()
            menu_item.set_always_show_image(True)
            if isinstance(image, gdkPixbuf) :
                icon = gtk.Image.new_from_pixbuf(image)
            elif '/' in image:
                icon = gtk.Image.new_from_file(image)
            else:
                icon = gtk.Image.new_from_icon_name(image, 16)
            menu_item.set_image(icon)
        elif item_type is gtk.MenuItem:
            menu_item = gtk.MenuItem(label)
        elif item_type is gtk.SeparatorMenuItem:
            menu_item = gtk.SeparatorMenuItem()
        if action:
            menu_item.connect('activate', action, *args)

        menu_obj.append(menu_item)
        menu_item.show()

    def get_user_dirs(self,*args):
        user_dirs = []
        for index,val in glib.UserDirectory.__members__.items():
            print (index,val)
            if index == 8: continue
            dir = glib.get_user_special_dir(val)
            if dir: user_dirs.append(dir)
        return user_dirs

    def get_file_icon(self,*args):
        if not os.path.exists(args[-1]):
            return 'error'

        if args[-1].endswith('.desktop'):
            desk_file = Gio.DesktopAppInfo.new_from_filename(args[-1])
            icon = desk_file.get_icon()
            if not icon:  return 'gtk-file'
            if type(icon) == Gio.ThemedIcon:
                print('Gio.ThemedIcon')
                themed_name = icon.get_names()[0]
                theme = gtk.IconTheme.get_default()
                name = theme.lookup_icon(themed_name, 48, 0).get_filename()
            if type(icon) == Gio.FileIcon:
                print('Gio.FileIcon')
                name = icon.get_file().get_uri()
            icon_url= urllib.parse.unquote(name).replace('file://','') 
            return icon_url

        file = Gio.File.new_for_path(args[-1])
        file_info = file.query_info("standard::*",0)
        icon_string = file_info.get_icon().to_string()
        if 'folder-' in icon_string:
            return icon_string.split()[-2]
        return icon_string.split()[-1]

    def get_recent_files(self,*args):
        manager = gtk.RecentManager.get_default()
        recent_files = [ [item.get_short_name(), urllib.parse.unquote(item.get_uri().replace('file://','')), item.get_icon(16), item.get_modified() ] for item in manager.get_items() if item.exists() ]
        recent_files.sort(key=lambda x: x[3])
            #sort by "modified" attribute to get most recent files
        recent_files = recent_files[-self.max_items:]
        self.sort_menu( recent_files, column_name="basename" )
			# TODO: "basename" is hardcoded as sort criteria, should be set by a config option
        return recent_files
        
    def sort_menu(self, menu_data, column_name="", column_number=255):
        columns = { "basename": 0, "path": 1, "modification time": 3 }
        try:
            sort_column = columns[column_name]
        except:
            logging.debug(self.__name__ + ": sorting - illegal column name: %s", column_name)
            sort_column = column_number
                #use column_number as fallback
                
        try:
            menu_data = menu_data.sort(key=lambda x: x[sort_column].lower())
                # .lower() to make sorting case-insensitive
        except:
            pass
                # leave unsorted
    
    def callback(self,*args):
        self.update()

    def update(self,*args):
        current_files = self.get_recent_files()
        if current_files != self.cached_files:
             self.make_menu()
             self.cached_files = current_files
        glib.timeout_add_seconds(3,self.callback)
    
    def add_submenu(self,top_menu,label):
        menuitem = gtk.MenuItem(label)
        submenu = gtk.Menu()
        menuitem.set_submenu(submenu)
        top_menu.append(menuitem)
        menuitem.show()
        return submenu
    
    def make_menu(self):
        if hasattr(self, 'app_menu'):
            for item in self.app_menu.get_children():
                    self.app_menu.remove(item)
        else:
            self.app_menu = gtk.Menu()

        if self.recent_files_in_main_menu :
            recent = self.app_menu
        else:
            recent = self.add_submenu(self.app_menu,'Recent Files')

        recent_dict = self.get_recent_files()
        
        content = [recent,gtk.ImageMenuItem,'gtk-add',
                   'Add to Recent Files',self.add_recent,[None]
        ]      
        self.add_menu_item(*content) 

        content = [recent,gtk.ImageMenuItem,'user-trash',
                   'Clear recent files list',self.clear_recent,[None]
        ]      
        self.add_menu_item(*content)

        content = [recent,gtk.SeparatorMenuItem,
                   None,None,
                   None,[None]
        ]
        self.add_menu_item(*content)
        
        recent_list = self.get_recent_files()
        if not recent_list :
            content = [recent,gtk.MenuItem,None,
                       'No items',None,None
            ]
            self.add_menu_item(*content)
            last = None
            for i in recent.get_children():
                last = i
            last.set_sensitive(False)
        else:
            for [name, uri, icon, last_modified] in recent_list :
                content = [recent, gtk.ImageMenuItem,
                           icon, name[:self.name_length],
                           self.open_item, [uri]
                ]
                self.add_menu_item(*content)
        
        # if self.recent_files_in_main_menu :
            # content = [recent,gtk.SeparatorMenuItem,
                   # None,None,
                   # None,[None]
            # ]
            # self.add_menu_item(*content)
        

        # Pinned files
        bookmarks = self.add_submenu(self.app_menu,'Pinned Files')
        content = [bookmarks,gtk.ImageMenuItem,
                   'bookmark_add','Pin a file',
                   self.pin_file,[bookmarks,None]
        ]
        self.add_menu_item(*content)

        content = [bookmarks,gtk.ImageMenuItem,
                   'remove','Remove item',
                   self.remove_pinned,['files']
        ]
        self.add_menu_item(*content)

        content = [bookmarks,gtk.ImageMenuItem,
                   'user-trash','Remove All',
                   self.remove_all_pinned,['files']
        ]
        self.add_menu_item(*content)
        content = [bookmarks,gtk.SeparatorMenuItem,
                   None,None,
                   None,[None]
        ]
        self.add_menu_item(*content)
        self.add_menu_item(*content) 

        pinned_files = self.get_pinned()

        if (pinned_files and 
            'files' in pinned_files.keys() and
            pinned_files['files']):
            for filepath in pinned_files['files']:
                icon = self.get_file_icon(filepath) 
                content = [bookmarks,gtk.ImageMenuItem,
                           icon,os.path.basename(filepath),
                           self.open_item,[filepath]
                ]
                self.add_menu_item(*content)
        else:
            content = [bookmarks,gtk.MenuItem,None,
                       'No items',None,None
            ]
            self.add_menu_item(*content)
            last = None
            for i in bookmarks.get_children():
                last = i
            last.set_sensitive(False)

        places = self.add_submenu(self.app_menu,'Places')
        content = [places,gtk.ImageMenuItem,'add',
                   'Pin Directory',self.pin_dir,[None]
        ]
        
        self.add_menu_item(*content)

        content = [places,gtk.ImageMenuItem,
                   'remove','Remove Pinned',
                   self.remove_pinned,['dirs']
        ]
        self.add_menu_item(*content)

        content = [places,gtk.SeparatorMenuItem,
                   None,None,
                   None,[None]
        ]
        self.add_menu_item(*content)

        content = [places,gtk.MenuItem,None,
                   'Standard Dirs',None,None
        ]
        self.add_menu_item(*content)
        last = None
        for i in places.get_children():
            last = i
        last.set_sensitive(False)
        for dir in self.get_user_dirs():
            icon = self.get_file_icon(dir)
            content = [places,gtk.ImageMenuItem,icon,
                       os.path.basename(dir),self.open_item,[dir]
                       
            ]
            self.add_menu_item(*content)

        content = [places,gtk.SeparatorMenuItem,
                   None,None,
                   None,[None]
        ]
        self.add_menu_item(*content)

        content = [places,gtk.MenuItem,None,
                   'Pinned Dirs',None,None
        ]
        self.add_menu_item(*content)
        last = None
        for i in places.get_children():
            last = i
        last.set_sensitive(False)

        if (pinned_files and 
           'dirs' in pinned_files.keys() and
           pinned_files['dirs']):
            for dir in pinned_files['dirs']:
                icon = self.get_file_icon(dir)
                #print(icon)
                content = [places,gtk.ImageMenuItem,icon,
                           os.path.basename(dir),self.open_item,[dir]
                           
                ]
                self.add_menu_item(*content)
        else:
            content = [places,gtk.MenuItem,None,
                       'No items',None,None
            ]
            self.add_menu_item(*content)
            last = None
            for i in places.get_children():
                last = i
            last.set_sensitive(False)

        # LINKS
        
        links_menu = self.add_submenu(self.app_menu,'Links')
        #emblem-symbolic-link
        content = [links_menu,gtk.ImageMenuItem,'add',
                   'Add link',self.pin_link,[None]
        ]
        
        self.add_menu_item(*content)

        content = [links_menu,gtk.ImageMenuItem,
                   'remove','Remove link',
                   self.remove_link,[None]
        ]
        self.add_menu_item(*content)

        content = [links_menu,gtk.ImageMenuItem,
                   'user-trash','Remove all links',
                   self.remove_all_pinned,['links']
        ]
        self.add_menu_item(*content)

        content = [links_menu,gtk.SeparatorMenuItem,
                   None,None,
                   None,[None]
        ]
        self.add_menu_item(*content)

        if (pinned_files and 
           'links' in pinned_files.keys() and
           pinned_files['links']):
            for link in pinned_files['links']:
                print(link)
                icon = 'emblem-web'
                content = [links_menu,gtk.ImageMenuItem,icon,
                           link[:30] + "...",self.open_link,[link]
                ]
                self.add_menu_item(*content)
        else:
            content = [links_menu,gtk.MenuItem,None,
                       'No items',None,None
            ]
            self.add_menu_item(*content)
            last = None
            for i in links_menu.get_children():
                last = i
            last.set_sensitive(False)

        content = [self.app_menu,gtk.SeparatorMenuItem,
                   None,None,
                   None,[None]
        ]
        self.add_menu_item(*content)
        #---

        content = [self.app_menu,gtk.ImageMenuItem,'exit',
                   'quit',self.quit,[None]
        ]
        self.add_menu_item(*content)
        self.app.set_menu(self.app_menu)

    def check_directory(self,*args):
        current_set = set(os.listdir(args[-1]))
        return current_set - self.cached_set
        

    def get_pinned(self,*args):
        try:
            with open(self.pinned_list) as f:
                 things = json.load(f,object_pairs_hook=OrderedDict)
            new_things = OrderedDict()
            for key,pathlist in things.items():
                new_list = []
                if key == 'links':
                   new_list = pathlist
                   new_things[key] = new_list
                   continue
                for index,path in enumerate(pathlist):
                    if os.path.exists(path):
                       new_list.append(path) 
                new_things[key] = new_list
            return new_things
                 
        except FileNotFoundError:
            print('>>> ',self.pinned_list,' not found')
            return None
        except json.JSONDecodeError:
            print(">>> Can't read ",self.pinned_list,',may be corrupt')
            return None

    def pin_dir(self,*args):
        # TODO
        current_list = self.get_pinned()
        if not current_list:
            current_list = OrderedDict()
            current_list['dirs'] = []
            f = open(self.pinned_list,'w')
            f.write("")
            f.close()

        if not args[-1]:
            cmd = "zenity --file-selection --directory --separator || --multiple"
            dirs = self.run_cmd(cmd.split())
        else:
            dirs = args[-1]

        dir_list = []
        if not dirs: return None
        dir_list = dirs.decode().strip().split("||")
        if not 'dirs' in current_list.keys():
             current_list['dirs'] = []
        for f in dir_list:
            current_list['dirs'].append(f)

        with open(self.pinned_list,'w') as f:
            json.dump(current_list,f,indent=4)
        self.make_menu()

    #TODO: rewrite pin_link,pin_dirs,and pin_files as one function ?
    def pin_link(self,*args):
        current_list = self.get_pinned()
        if not current_list:
            current_list = OrderedDict()
            current_list['links'] = []
            f = open(self.pinned_list,'w')
            f.write("")
            f.close()
        if not args[-1]:
            cmd = ['zenity','--entry','--text','Insert URL']
            link = self.run_cmd(cmd)
        else:
            link = args[-1]

        if not link: return None
        link = link.decode().strip()

        if not 'links' in current_list.keys():
            current_list['links'] = []
        current_list['links'].append(link)

        with open(self.pinned_list,'w') as f:
                json.dump(current_list,f,indent=4)
        self.make_menu()

    def pin_file(self,*args):
        current_list = self.get_pinned()
        if not current_list:
            current_list = OrderedDict()
            current_list['files'] = []
            f = open(self.pinned_list,'w')
            f.write("")
            f.close()
        if not args[-1]:
            cmd = "zenity --file-selection --separator || --multiple "
            files = self.run_cmd(cmd.split())
        else:
            files = args[-1]

        file_list = []
        if not files: return None
        file_list = files.decode().strip().split("||")
        if not 'files' in current_list.keys():
            current_list['files'] = []
        for f in file_list:
                #icon = self.get_file_icon(f)
                current_list['files'].append(f)

        with open(self.pinned_list,'w') as f:
                json.dump(current_list,f,indent=4)
        self.make_menu()

    def remove_all_pinned(self,*args):
        try:
            with open(self.pinned_list) as f:
                pinned = json.load(f)
            pinned.pop(args[-1])       
            with open(self.pinned_list,'w') as f:
                    json.dump(pinned,f,indent=4)
        except:
            pass
        finally:
            self.make_menu()

    def remove_link(self,*args):
        key = 'links'
        pinned = self.get_pinned()
        if not pinned: return
        columns=[]
        for i in pinned[key]:
            columns.append("_")
            columns.append(i)
        print(columns)
        cmd=['zenity', '--title','', '--text','Remove Link', 
             '--list', '--checklist', '--column', '', 
             '--column','URL',*columns]
        selection = self.run_cmd(cmd)
        if not selection: return
        if isinstance(selection,bytes): selection = selection.decode()
        selection = selection.strip()
        index = pinned[key].index(selection)
        pinned[key].pop(index)
        with open(self.pinned_list,'w') as f:
            json.dump(pinned,f,indent=4)
        self.make_menu()
          

    def remove_pinned(self,*args):
        key = args[-1]
        pinned = self.get_pinned() 
        if not pinned: return
        cmd_str = "zenity --forms --add-combo Remove --combo-values"
        vals = "|".join(pinned[key])
        cmd = cmd_str.split() + [vals]
        item = self.run_cmd(cmd)
        if item: 
            path = item.decode().strip()
            index = pinned[key].index(path)
            pinned[key].pop(index)
            with open(self.pinned_list,'w') as f:
                json.dump(pinned,f,indent=4)
            self.make_menu()

    def add_recent(self,*args):
        cmd = "zenity --file-selection --separator || --multiple "
        files = self.run_cmd(cmd.split())
        file_list = []
        if not files: return
        file_list = files.decode().strip().split("||")
        items = ['file://' + f for f in file_list]
        for f in items: gtk.RecentManager().get_default().add_item(f)

    def clear_recent(self,*args):
        try:
            gtk.RecentManager.get_default().purge_items()
            self.make_menu()
        except:
            pass

    def open_link(self,*args):
        return self.run_cmd(['x-www-browser',args[-1]])

    def open_item(self,*args):
        if not os.path.exists(args[-1]):
            cmd = ['zenity','--error','--text',
                   'Path doesn\'t exist: ' + args[-1]
            ]
            self.run_cmd(cmd)
            self.make_menu()
            return

        if args[-1].endswith('.desktop'):
            desk_file = Gio.DesktopAppInfo.new_from_filename(args[-1])
            return desk_file.launch_uris()
        return subprocess.Popen(['xdg-open',args[-1]])

    def quit(self,*args):
        gtk.main_quit()

    def run_cmd(self, cmdlist):
        """ utility: reusable function for running external commands """
        try:
            stdout = subprocess.check_output(cmdlist) #env=new_env)
        except subprocess.CalledProcessError:
            pass
        else:
            if stdout:
                return stdout

    def run(self):
        """ Launches the indicator """
        try:
            gtk.main()
        except KeyboardInterrupt:
            pass

    def quit(self, *args):
        """ closes indicator """
        gtk.main_quit()

def main():
    """ defines program entry point """
    indicator = FilesIndicator()
    signal.signal(signal.SIGINT,signal.SIG_DFL)
    indicator.run()

if __name__ == '__main__':
    main()
