#!/usr/bin/env python

"""
Python indicator applet to show the status of several git repositories

Behaviour
---------
When first opened, all repo's in `~/.batchgit` are checked against the remote.
A label is assigned of the form ahead, diverged, behind, up-to-date or no-state
is recorded for each repo. The menu is then set-up in the following way: The
main menu icon displays some global information - either all okay, or action
required; each individual menu item is assigned a symbol to reflect its current
status. In addition each menu item is given a `+n` to indicate the `n` modified
files.

The indicator then periodically checks all repo's with a frequency set by
`update_period` (by default this is 100 seconds). On these periodic checks the
remote is *not* tested. To update against the remote a `manual update` entry
is provided in the menu itself.

"""

# Python 2.7.x standard library imports
import subprocess
import os
import sys
import argparse

sys.tracebacklimit = 2

try:
    import pygtk
    pygtk.require('2.0')
    import gobject
    import gtk
except ImportError:
    raise ImportError(
    "The python package gobject required for the batchgit indicator\n"
    "is not installed. To install you may use\n\n"
    "    apt-get install python-gobject\n")
except AssertionError:
    raise ImportError(
    "The python package gtk2 required for the batchgit indicator\n"
    "is not installed. To install you may use\n\n"
    "    apt-get install python-gtk2\n")

try:
    import appindicator
except ImportError:
    raise ImportError(
    "The python package appindicator required for the batchgit indicator\n"
    "is not installed. To install you may use\n\n"
    "    apt-get install python-appindicator\n")


class AppIndicator:
    def __init__(self, dirs, args):

        self.args = args
        self.dirs = dirs
        self.update_period = 100
        self.remote = False  # Flag to update the remote
        self.ind = appindicator.Indicator(
            "checkgit", "", appindicator.CATEGORY_APPLICATION_STATUS)

        self.ind.set_status(appindicator.STATUS_ACTIVE)
        self.ind.set_attention_icon("indicator-messages-new")

        self.IconMenuDictionary = {'ahead': gtk.STOCK_GO_UP,
                                   'diverged': gtk.STOCK_REFRESH,
                                   'behind': gtk.STOCK_GO_DOWN,
                                   'up-to-date': gtk.STOCK_YES,
                                   'no-state': gtk.STOCK_HELP
                                   }
        # create a menu
        menu = gtk.Menu()

        ManualCheck = gtk.MenuItem("Manually update")
        ManualCheck.show()
        ManualCheck.connect("activate", self.SetIconAndMenu_NoRemote)
        menu.append(ManualCheck)

        dirs_items = []
        for dir in self.dirs:
            item = gtk.ImageMenuItem(gtk.STOCK_YES, dir)
            item.show()
            item.set_always_show_image(True)
            menu.append(item)
            dirs_items.append(item)

        self.dirs_items = dirs_items

        quit = gtk.ImageMenuItem(gtk.STOCK_QUIT)
        quit.connect("activate", self.quit)
        quit.set_always_show_image(True)
        quit.show()
        menu.append(quit)

        self.ind.set_menu(menu)

        self.SetIconAndMenu(remote=True)  # Initialise the icon

        gobject.timeout_add_seconds(int(self.update_period),
                                    self.SetIconAndMenu)
        gtk.threads_init()

    def CheckState(self, path):
        """ Check the state information of path, if remote is true then it will
        also be compared to see if the path is behind """

        path = path.rstrip("/") + "/"

        if self.remote and not self.args.no_remote:
            # Check if there is a remote
            cmd_line = ("git --git-dir={}.git "
                        "--work-tree={} remote update".format(path, path))
            status = subprocess.check_output(cmd_line, shell=True)
            if "ERROR: Repository not found" in status:
                pass

        cmd_line = "git --git-dir={}.git --work-tree={} status".format(
            path, path)
        status = subprocess.check_output(cmd_line, shell=True)

        for state in ['ahead', 'diverged', 'behind', 'up-to-date']:
            if state in status:
                return state

        return 'no-state'

    def ModifiedCount(self, path):
        path = path.rstrip("/") + "/"
        cmd_line = "git --git-dir={}.git --work-tree={} ls-files -m".format(
            path, path)
        output = subprocess.check_output(cmd_line, shell=True)
        modified_files = output.split("\n")
        return len(modified_files) - 1

    def CheckAllDirStatus(self):
        stati = {}
        for (dir, list_item) in zip(self.dirs, self.dirs_items):
            status = {'state_to_origin': self.CheckState(dir),
                      'modified': self.ModifiedCount(dir),
                      }
            stati[dir] = status

        return stati

    def SetIconAndMenu(self, remote=False, *args):
        """ Sets the icon and menu items

        Parameters
        ----------
        remote : bool
            If true then update the remote for this time only

        Returns
        -------
        True
        """

        if remote:
            self.remote = True
        else:
            self.remote = False

        stati = self.CheckAllDirStatus()

        # Set the main icon
        if any([dic['state_to_origin'] in ['ahead', 'diverged', 'behind']
                for dic in stati.values()]):
            self.ind.set_icon(gtk.STOCK_DIALOG_WARNING)
        else:
            self.ind.set_icon(gtk.STOCK_YES)

        # Set individual menu items
        for i, dir_item in enumerate(self.dirs_items):
            dir = self.dirs[i]
            label = str(dir)
            status = stati[dir]

            if status['state_to_origin'] is not None:
                img = self.IconMenuDictionary[status['state_to_origin']]
                dir_item.get_image().set_from_stock(img,
                                                    gtk.ICON_SIZE_MENU)

                modified_count = stati[dir]['modified']
                if modified_count > 0:
                    label += " +{}".format(modified_count)

                dir_item.set_label(label)

        self.remote = False

        return True

    SetIconAndMenu_NoRemote = lambda self, args: self.SetIconAndMenu(self)

    def quit(self, widget, data=None):
        gtk.main_quit()


def _rcfile_help_messg(rcfile):
    mssg = """ The rcfile {} does not exist

Details:
checkgit uses the rcfile to load the paths of repo's that you want to check.
You can create an rcfile for it to use by creating a file `~/.batchgitrc` in
your home directory which contains lines with

/path/to/your/repo
"""
    return mssg

if __name__ == "__main__":

    # Set up the argparse
    parser = argparse.ArgumentParser(
        description=__doc__,
        formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument(
        '--no_remote', action='store_true',
        help='If set then do not check the remote ever')
    parser.add_argument(
        '-r', '--RCFILENAME', default=".batchgitrc",
        help='If set use supplied RCFILENAME otherwise use the default')

    args = parser.parse_args()

    RCFILENAME = args.RCFILENAME
    rcfile = os.path.expanduser("~") + "/" + RCFILENAME
    if os.path.isfile(rcfile):
        dirs = []
        with open(rcfile, "r") as f:
            for line in f:
                dirs.append(line.rstrip("\n"))

        indicator = AppIndicator(dirs, args),

        gtk.main()
    else:
        raise ValueError(_rcfile_help_messg(rcfile))
