#!/usr/local/bin/python2.7
# -*- coding: utf-8 -*-

#
# Copyright (c) 2016 vincent_delft@yahoo.com
#       All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#
# THIS SOFTWARE IS PROVIDED BY Berkeley Software Design, Inc. ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL Berkeley Software Design, Inc. BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#

import sys
import xdg, xdg.BaseDirectory, xdg.DesktopEntry, xdg.IconTheme
import os.path, glob

import gettext
gettext.install('openboxMenuCreate', './po', unicode=True)

#TODO: i18n must be finalized

#######################
# configuration items #
#######################

THEME="Adwaita" #verify your have such package on your system (/usr/local/share/icons/Awaita)
CONFIG_FILE="~/.config/openbox/menu.xml" #this is the default output file
TERM = "xterm" #to start some .desktop applications
FAVORITE_APPS=[{"exec":"firefox"}, {"name":"_Xterm","exec":"xterm"}, {"exec":"pcmanfm"}] #name is optional, "_" is for shortcodes 

#the sequence is important because some .desktop file have several categories. 
#we take the first one matching
menuCategories = [[_('multimedia'), ('AudioVideo','Audio','Video')],
              (_('development'), ('Development','TextEditor')),
              (_('office'),('Office',)),
              (_('education'), ('Education',)),
              (_('games'), ('Game',)),
              (_('graphics'), ('Graphics',)),
              (_('network'), ('Network',)),
              (_('system'),('System',)),
              (_('accessories'),('Settings','Utility')),
              (_('others'),()),
              ]

#layout proposed for the global menus
#parameters are: favorites, commands, applications, system
XML_LAYOUT = """<openbox_menu>
  <menu id="root-menu" label="OpenBox 3">
    <separator label="Favorites"/>
      %(favorites)s
    <separator label="Commands" />
      %(commands)s
    <separator label="Applications"/>
      %(applications)s
    <separator label="System"/>
      %(system)s
    <separator/>
      %(exit)s
  </menu>
</openbox_menu>
"""

##############################
# end of configuration items #
##############################

#parameters are: label, icon, command
ITEM_tmpl="""
<item label="%(label)s" icon="%(icon)s">
 <action name="Execute">
   <command>%(command)s</command>
 </action>
</item>"""

#parameters are: id, label, content
MENU_tmpl="""
<menu id="%(id)s" label="%(label)s">
  %(content)s
</menu>""" 


#parameters are: id, label, command
PIPE_tmpl="""
<menu id="%(id)s" label="%(label)s"  execute="%(command)s" />"""



#############################################
# methods to collect the .desktop available #
# on the machine                            #
#############################################

def reformatIcon(icon):
    """ in some .desktop the icon entry is not correctly formated. we remove the extension"""
    ret = icon
    #remove the extension
    if icon and (icon[0] not in ["/","."]):
        ret = os.path.splitext(icon)[0]
    return ret

 
def getDesktopFolders():
    "return a list of all folders containing .desktop file"
    desktopDirs=[]
    for elem in xdg.BaseDirectory.xdg_data_dirs:
        folder = elem.rstrip("/") + "/applications"
        if folder not in desktopDirs:
            if os.path.isdir(folder):
                desktopDirs.append(folder + "/*.desktop")
    print "we perform the following search:", desktopDirs
    return desktopDirs

def getDesktopItems(filename):
    """return a dictionary containing the .desktop data"""
    xdgData={}
    if os.path.isfile(filename):
        entryObject = xdg.DesktopEntry.DesktopEntry(filename = filename)
        xdgData['name'] = entryObject.getName()
        icon = reformatIcon(entryObject.getIcon())
        fpicon = xdg.IconTheme.getIconPath(icon, theme=THEME)
        if fpicon == None:
            fpicon = xdg.IconTheme.getIconPath("application-x-executable", theme=THEME)
        xdgData['icon'] = fpicon
        xdgData['hidden'] = entryObject.getHidden()
        xdgData['nodisplay'] = entryObject.getNoDisplay() #boolean
        exe = entryObject.getExec() #remove %f, %u, ... and add termnial if required
        pos = exe.find("%")
        if pos >0: #we want to remove any %F, %f, %U, %u, ....
            exe = exe [:pos]
        if entryObject.getTerminal():
            exe = "%s -e \"%s\" " % (TERM, exe)
        xdgData['exec'] = exe            
        xdgData['path'] = entryObject.getPath() 
        xdgData['categories'] = entryObject.getCategories() #is a list
        xdgData['onlyshowin'] = entryObject.getOnlyShowIn() #is a list
        xdgData['noshowin'] = entryObject.getNotShowIn() #is a list
    #print "Filename: ", filename
    #print xdgData
    #print "-"*20
    return xdgData 

def getAllXDGData():
    """return a dictonary of the type:
    {<desktop full path file name>: { <desktop key>: <desktop value> # can be string or list }
    keys and values are coming from getDesktopItems
    """
    allData = {}
    for folder in getDesktopFolders():
        for elem in glob.glob(folder):
            allData[elem] = getDesktopItems(elem)
    return allData

def getMenuCategory(xdgData):
    """for xdgdata allowed, we return one of the know categories or others"""
    if ((len(xdgData['onlyshowin'])>0) and ("Openbox" not in xdgData['onlyshowin'])) or ("Openbox" in xdgData["noshowin"]) or ( xdgData["nodisplay"] == True):
        return None
    for menuName, categories in menuCategories:
        for cat in categories:
            if cat in xdgData['categories']:
                return menuName
    return "others"    


###############################################
# Objects to properly create the required xml #
###############################################


def pretty_xml(text, indent=0):
    res = ""
    for line in text.split('\n'):
        if line.strip():
            res += "  "*indent + line + "\n"
    return res

    


##############################################
# Methods to build our openbox menu.xml file #
##############################################


def menu_exit():
    "This return an XML with the exit possibilities"
    #TODO: i18n the different labels
    content ="""<item label="Logout">
        <action name="Exit">
          <prompt>yes</prompt>
        </action>
      </item>
      <item label="Reboot">
        <action name="Execute">
          <prompt>Are you sure you want to reboot?</prompt>
          <execute>
            doas /sbin/shutdown -r now
          </execute>
        </action>
      </item>
      <item label="Shutdown" icon="%(exit-icon)s">
        <action name="Execute">
          <prompt>Are you sure you want to shutdown?</prompt>
          <execute>
            doas /sbin/halt -p
          </execute>
        </action>
      </item>""" % {'exit-icon': xdg.IconTheme.getIconPath("application-exit", theme=THEME)}
    return content



def menu_applications(dataDict ):
    "return an xml with all applications found on your system"
    openboxElems = {}
    for filename in dataDict.keys():
        menuCategory = getMenuCategory(dataDict[filename])
        if menuCategory:
            openboxElems.setdefault(menuCategory, {})
            openboxElems[menuCategory]['name'] = menuCategory 
            openboxElems[menuCategory].setdefault('entries', [])
            openboxElems[menuCategory]['entries'].append([dataDict[filename]['name'], dataDict[filename]['exec'], dataDict[filename]['icon']])
    content = ""
    for menuName, categories in menuCategories:
        if openboxElems.has_key(menuName):
            menu_category = openboxElems[menuName]['name']
            items = ""
            for elems in openboxElems[menuName]['entries']:
                if elems[0] and elems[2]:
                    raw_xml = ITEM_tmpl % {'label':elems[0], 'icon':elems[2], 'command':elems[1]}
                    items += raw_xml
            if items:
                p_items = pretty_xml(items,1)
                content += MENU_tmpl % {'id':menu_category,'label':menu_category,'content':p_items.lstrip()}
    return content 

def menu_favoriteApplications(dataDict):
    "Build the xml with few favorites applications as specified in FAVORITE_APPS"
    
    #reorganise the data dictionary
    dataDictExecs = {}
    for fn in dataDict.keys():
         exe = dataDict[fn].get('exec','').strip()
         if exe:
             dataDictExecs[exe] = dataDict[fn]
    content = ""
    for app in FAVORITE_APPS:
        if app.get("exec","") and app.get("name","") and app['name']!="_":
            #we take what you propose in FAVORITE_APPS
            icon = app.get('icon','')
            #if you do not provide an icon in FAVORITA_APPS,we try to find one
            if not icon and dataDictExecs.get(app['exec'],''):
                icon = dataDictExecs[app["exec"]].get('icon','')
            raw_xml = ITEM_tmpl % {'label':app['name'],'icon':icon,'command': app["exec"]}
            content += raw_xml
        elif app.get('exec','') and dataDictExecs.get(app["exec"],'') and dataDictExecs[app["exec"]].get('name',''):
            #no "name" in FAVORIT_APPS (or name is just the underscore (for key-bindings)
            raw_xml = ITEM_tmpl % {'label':app.get('name','') + dataDictExecs[app["exec"]]["name"], 'icon':dataDictExecs[app["exec"]]['icon'], 'command':app["exec"]}
            content += raw_xml
        else:
            print """ERROR !!!!. The favorite is not correctly defined. It must have either "exec" either "name" and "exec" and optionaly "icon" """
            print "We have:", app
            execs = dataDictExecs.keys()
            execs.sort()
            print "Available exec are:", execs
    return content

def menu_commands():
    "Build an xml with few users commands, if they are present on the system"
    #the network part
    content = ""
    net_items = ""
    if os.path.isfile(os.path.expanduser("~/.config/openbox/wireless.py")):
        net_items = PIPE_tmpl % {'id':"wifi-list",'label':_("Wifi confidured"),'command':"~/.config/openbox/wireless.py --list"} 
        net_items += PIPE_tmpl % {'id':"wifi-scan",'label':_("Wifi scan"),'command':"~/.config/openbox/wireless.py --scan"} 
    if os.path.isfile(os.path.expanduser("~/.config/openbox/nfs_status.sh")):
        if net_items:
            net_items += "<separator />"
        net_items += PIPE_tmpl % {'id':"NFS-status", 'label':_("NFS status"), 'command':"~/.config/openbox/nfs_status.sh"}
        net_items += ITEM_tmpl % {'label':_("NFS toggle u/mount"),'icon':"",'command':"~/.config/openbox/toggle_mount_nas.sh"}
    if net_items:
        net_items = pretty_xml(net_items.lstrip(),1)
        content += MENU_tmpl % {'id':"network-commands",'label':_("Network"),'content':net_items.lstrip()}
    
    #the battery part
    if os.path.isfile(os.path.expanduser("~/.config/openbox/battery.sh")):
        content += PIPE_tmpl % {'id':"battery-menu", 'label':_("Battery status"),'command':"~/.config/openbox/battery.sh"}
    return content 


def menu_system():
    "we return an xml with some system commands we can find on the system"
    items = ""
    if os.path.isfile(os.path.expanduser("/usr/local/bin/pcmanfm")):
        items += ITEM_tmpl % {"label":_("Desktop preferences"), "icon": "", "command":"/usr/local/bin/pcmanfm --desktop-pref"}
    if os.path.isfile(os.path.expanduser("~/.config/openbox/wallpaper.sh")):
        items += ITEM_tmpl % {"label":_("Change wallpaper"), "icon":"", "command":"~/.config/openbox/wallpaper.sh"}
    if os.path.isfile(os.path.expanduser("~/.config/openbox/switchmonitor.sh")):
        items += ITEM_tmpl % {"label":_("Switch monitor"), "icon":"", "command":"~/.config/openbox/switchmonitor.sh"}
    if os.path.isfile(os.path.expanduser("~/.config/openbox/togglemonitor2.sh")):
        items += ITEM_tmpl % {"label":_("Toggle extra monitor"), "icon":"", "command":"~/.config/openbox/togglemonitor2.sh"}
    if os.path.isfile(os.path.expanduser("/usr/loca/bin/openboxMenuCreate")):
        items += ITEM_tmpl % {"label":_("Generate Openbox menus"),"icon":"", "command":"openboxMenuCreate"}
    if items:
        items += "<separator/>"
    if os.path.isfile(os.path.expanduser("/usr/local/bin/obconf")):
        items += ITEM_tmpl % {"label":_("Openbox configuration manager"),"icon":"", "command":"obconf"}
    items += """<item label="%s">\n  <action name="Reconfigure" />\n</item>""" % (_("Reload Openbox configs(W-r)"))
    content = MENU_tmpl % {'id':'system-commnds',"label":"system commands","content":pretty_xml(items,1).lstrip()}
    return content


def build_whole_xml(allData):
    "Concatenation of all elements in order to build a complete xml file for openbox"
    favorites = pretty_xml(menu_favoriteApplications(allData), 3)
    commands = pretty_xml(menu_commands().lstrip(),3)
    applications = pretty_xml(menu_applications(allData),3)
    system = pretty_xml(menu_system().lstrip(),3)
    exit = menu_exit()
    xml_content = XML_LAYOUT % {'favorites':favorites.lstrip(),'commands':commands.lstrip(), 'applications':applications.lstrip(), 'system':system.lstrip(),'exit':exit}
    
    return xml_content


def return_existing_categories(data):
    for filename in data.keys():
        menuCategory = getMenuCategory(data[filename])
        if menuCategory:
            print "%s\t%s\t%s" % (filename, data[filename]['exec'], menuCategory)

    
if __name__=="__main__": 
    import argparse
    parser = argparse.ArgumentParser(description=_("Create menus for openbox"))
    parser.add_argument('--dryrun','-d', action="store_true", help=_("dry run and do not overwrite the config file, but present the result on the standard output"))
    parser.add_argument('--config','-c', help=_("overwrite the default config file"))
    parser.add_argument('--categories','-C', action="store_true", help=_("present all application and their assocaited category"))
    args = parser.parse_args()
   
    allData = getAllXDGData()
    if args.categories:
        return_existing_categories(allData)
        sys.exit(0)
    
    xml_content = build_whole_xml(allData)
    if xml_content:
        if CONFIG_FILE:
            conf_file = os.path.expanduser(CONFIG_FILE)
        if args.config:
            conf_file = args.config
        if conf_file and not args.dryrun:
            try:
                fid = open(conf_file,"w")
                fid.write(xml_content.encode('utf8'))
                fid.close()
            except Exception, e:
                print "ERROR: failed to create the file:", conf_file
                raise
            else:
                print "The following menu file has been created:", conf_file
        if args.dryrun:
            print xml_content

