#!/usr/bin/python
# Copyright (c) 2008 Bert Freudenberg
#
# 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.

# Changelog:
# Oct 14: Create a local mirror
# Aug 01: Deal with dot-less service names
# Jul 11: Rename Web to Browse
# Jun 11: test for user=olpc not uid=500
# May 14: modified for joyride
# Apr 01: do not automatically switch to olpc user
# Mar 28: install default activities if more than half are missing
# Mar 18: use /tmp/olpc-session-bus, exclude Journal, case-insensitive 
# Mar 15: provide dbus mainloop, create Activities dir, guess bundle name
# Mar 14: initial version

LATEST = "http://xs-dev.laptop.org/~cscott/repos/joyride/"
CURRENT = "/home/olpc/Activities/"
DEFAULT = ['AcousticMeasure', 'Analyze', 'Calculate', 'Chat',
  'Etoys', 'Log', 'Measure', 'Memorize', 'NewsReader', 'Paint',
  'Pippy', 'Read', 'Record', 'TamTamEdit', 'TamTamJam', 'TamTamMini',
  'TamTamSynthLab', 'Terminal', 'TurtleArt', 'Browse', 'Write']

import os, pwd, sys, re

def sanitize(string):
  """quote string for use in cmd line"""
  if re.match('^[-a-zA-Z0-9/.]+$', string) is None:
    return "'%s'" % string
  else:
    return string

### This only works if the script was downloaded to a directory readable
### by user olpc. But /root (the default) is not readable ...
#
#if (os.getuid() == 0):
#  print "Switching to user olpc ..."
#  cmdline = ' '.join([sanitize(arg) for arg in sys.argv])
#  os.execl("/bin/su", "/bin/su", "olpc", "-c", cmdline)
#  print "Failed."

from urllib import urlopen, urlretrieve, quote, unquote
from HTMLParser import HTMLParser
from glob import glob
from optparse import OptionParser
from ConfigParser import ConfigParser

parser = OptionParser(usage="usage: %prog [options] [ACTIVITIES]\n\n"+
  "If no ACTIVITIES are listed, operate on all activities")
parser.add_option("-u", "--upgrade", action="store_true", dest="upgrade",
  default=True, help="upgrade installed activities [default]") 
parser.add_option("-i", "--install", action="store_true", dest="install",
  default=False, help="install non-installed activities")
parser.add_option("-d", "--download", action="store_true", dest="download",
  default=False, help="only download, do not actually install or upgrade")
parser.add_option("-l", "--list", action="store_true", dest="listonly",
  default=False, help="only list, do not actually install or upgrade")
parser.add_option("-x", "--exclude", action="append", dest="exclude",
  metavar="ACTIVITY", help="exclude ACTIVITY (default: Journal)", 
  default=['Journal'])
parser.add_option("--mirror", action="store_true", dest="mirror",
                  default=False, help="download and create an activity mirror")
(options, activities) = parser.parse_args()

if options.mirror:
  CURRENT = "Activities-Mirror/"

if not options.mirror:
  user = pwd.getpwuid(os.getuid())[0]
  if (user != 'olpc'):
    print 'ERROR: You must log in as "olpc" to upgrade or install activities.'
    if (os.getuid() == 0):
      print '       Execute "su - olpc" and then try again.'
    exit()

if options.listonly:
  options.upgrade = False
  options.install = False

if activities:
  activities = [a.rsplit('-',1)[0].lower() for a in activities]
  print "Only considering " + ", ".join([sanitize(a) for a in activities])
  exclude = []
else:
  if options.install:
    print "To install you must list activities explicitly"
    exit()
  exclude = [a.rsplit('-',1)[0].lower() for a in options.exclude]
  print "Excluding " + ", ".join([sanitize(a) for a in exclude])

latest = {}
current = {}

class FindLatest(HTMLParser):
  def handle_starttag(self, tag, attrs):
    if tag == 'a':
      for (attr, value) in attrs:
        if attr == 'href' and value[-3:] == '.xo':
          (activity, version) = unquote(value[:-3]).rsplit('-',1)
          if activity.lower() in exclude:
            continue
          if activities and activity.lower() not in activities:
            continue
          if activity not in latest or latest[activity] < int(version):
            latest[activity] = int(version)

def find_latest():
  FindLatest().feed(urlopen(LATEST).read())

def find_current():
  for info in glob(CURRENT + "*.activity/activity/activity.info"):
    """There is no common naming convention for xo bundles. Sigh."""
    """Can't use ActivityBundle().get_name() because that is translated"""
    cp = ConfigParser()
    cp.read(info)
    guesses = []
    guesses.append(info[:-32].rsplit('/', 1)[-1])
    if cp.has_option('Activity', 'name'):
      guesses.append(cp.get('Activity', 'name'))
    if cp.has_option('Activity', 'bundle_id'):
      guesses.append(cp.get('Activity', 'bundle_id').rsplit('.',1)[-1])
    elif cp.has_option('Activity', 'service_name'):
      guesses.append(cp.get('Activity', 'service_name').rsplit('.',1)[-1])
    version = int(cp.get('Activity', 'activity_version'))
    for guess in guesses:
      current[guess] = version

def download(activity):
  name = "%s-%i.xo" % (activity, latest[activity])
  if os.access(CURRENT + name, os.F_OK):
    print "  already downloaded " + name
  else:
    print "  downloading " + name
    urlretrieve(LATEST + quote(name), CURRENT + name)
  return CURRENT + name

print "Finding latest versions ..."
find_latest()

if not latest:
  if activities: 
    print "No activities matching %s found." % " or ".join(
      [sanitize(a) for a in activities])
  else:
    print "Could not retrieve any activities."
  exit()

print "Finding current versions ..."
find_current()

to_install = []
to_upgrade = []
for activity in latest.keys():
  if activity not in current:
    to_install.append(activity)
  elif latest[activity] > current[activity]:
    to_upgrade.append(activity)

if not to_install and not to_upgrade:
  print "All activities are up-to-date. Nothing to do."
  exit()

"""Connect to running Sugar session"""
if not options.mirror:
  os.environ["DBUS_SESSION_BUS_ADDRESS"] = "unix:path=/tmp/olpc-session-bus"
  import dbus
  import dbus.mainloop
  dbus.set_default_main_loop(dbus.mainloop.NULL_MAIN_LOOP)

if not os.access(CURRENT, os.F_OK):
  print "Creating " + CURRENT
  os.mkdir(CURRENT)

errors = []

if not options.mirror:
  from sugar.bundle.activitybundle import ActivityBundle
 
if to_upgrade:
  to_upgrade.sort()
  print "To upgrade: " + ' '.join(
    [sanitize("%s-%i" % (a, latest[a])) for a in to_upgrade])
  if options.upgrade:
    for activity in to_upgrade:
      print "Upgrading %s from %i to %i:" % (
        activity, current[activity], latest[activity])
      try:
        xo = download(activity)
        if not options.mirror and not options.download:
          print "  upgrading %s" % activity
          ActivityBundle(xo).upgrade()
          os.remove(xo)
      except Exception, E:
	print "  ERROR: %s" % E
        errors.append("%s: %s" % (activity, E))
elif current:
  print "Installed activities are up-to-date"

if to_install:
  to_install.sort()
  default = [a for a in to_install if a in DEFAULT]
  if not activities and len(default) > len(DEFAULT)/2:
    to_install = default
    options.install = True

  print "To install: " + ' '.join([sanitize(a) for a in to_install])
  if options.install:
    for activity in to_install:
      print "%s ..." % activity
      try:
        xo = download(activity)
        if not options.mirror and not options.download:
          print "  installing %s" % activity
          ActivityBundle(xo).install()
          os.remove(xo)
      except Exception, E:
	print "  ERROR: %s" % E
        errors.append("%s: %s" % (activity, E))
  else:
    if not options.listonly:
      print "  (use -i to install)"

if errors:
  print "ERROR: something went wrong while installing:"
  print "\n".join(errors)

if options.mirror:
  print CURRENT
