#!/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:
# Jun 07: Changed the Path to be able to update to the latest Sucrose release 
# 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://dev.laptop.org/~erikos/bundles/0.81.4/"
CURRENT = "/home/olpc/Activities/"
DEFAULT = ['Calculate', 'Chat', 'Etoys', 'Log', 
  'Pippy', 'Read', 'Record', 'Terminal', 'Browse', 'Write']

import os, 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."

if (os.getuid() != 500):
  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()

from urllib import urlopen, urlretrieve, quote, unquote
from HTMLParser import HTMLParser
from glob import glob
from sugar.bundle.activitybundle import ActivityBundle
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'])
(options, activities) = parser.parse_args()

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+"index.html").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"""
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 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.download:
          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 "Installing %s ..." % activity
      try:
        xo = download(activity)
        if not options.download:
          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)

