#!/usr/bin/env python
#coding: iso-8859-15
#
# uptodate helps you to keep your system uptodate
#
# Copyright (C) 2005  Gatan Lehmann <gaetan.lehmann@jouy.inra.fr>
# 
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#

VERSION = "0.1"


import urllib
import re
import __builtin__
import sys
import os.path
from optparse import OptionParser, IndentedHelpFormatter


# error codes
ERROR_INVALID_NB_OF_ARGS = 1
ERROR_NO_VERSION_FOUND = 2
ERROR_MODULE_NOT_FOUND = 3
ERROR_PROPERTY_NOT_FOUND = 4
ERROR_MODULE_EXISTS = 5
ERROR_UNKNOWN_COMMAND = 5

ERRORS = {
	ERROR_INVALID_NB_OF_ARGS: "Invalid number of arguments",
	ERROR_NO_VERSION_FOUND: "No version found",
	ERROR_MODULE_NOT_FOUND: "Module not found",
	ERROR_PROPERTY_NOT_FOUND: "Property not found",
	ERROR_MODULE_EXISTS: "Module already exists",
	ERROR_UNKNOWN_COMMAND: "Unknown command",
}
# following functions are commands

def commandAdd(options, conf, out) :
	"Add a new module"
	parser = OptionParser(usage="uptodate [options] add [options] name url regexp [comment]\n", description=commandAdd.__doc__)
	parser.add_option("-v", "--verbose", action="store_true", dest="verbose", help="display added module")
	parser.add_option("--list-option", action="store_true", dest="listOption", help="list avaible options and commands")
	opts, args = parser.parse_args(options.argv)

	if opts.listOption :
		for opt in parser.option_list :
			print opt.get_opt_string()
		return 0
    
	if len(args) < 3 :
		parser.print_usage(sys.stderr)
		return ERROR_INVALID_NB_OF_ARGS

	module = args[0]
	url = args[1]
	regexp = args[2]
	comment = " ".join(args[3:])
	
	if conf.has_section(module) :
		print >> sys.stderr, "Module %s already exists." % module
		return ERROR_MODULE_EXISTS
	
	current = getVersions(module, url, regexp)
	
	# test if a version can be found
	if len(current) != 0 :
		conf.add_section(module)
		conf.set(module, 'url', url)
		conf.set(module, 'regexp', regexp)
		conf.set(module, 'comment', comment)
		conf.set(module, 'current', repr(current))
		
		if opts.verbose :
			printModule(conf, module, out, True)
	else :
		# no version found... exit
		print >> sys.stderr, "Error: no version found."
		return ERROR_NO_VERSION_FOUND


def commandAuto(options, conf, out) :
	"Search for a regexp and a new module"
	parser = OptionParser(usage="uptodate [options] auto [options] name url version [comment]""", description=commandAdd.__doc__)
	parser.add_option("-i", "--interactive", action="store_true", dest="choose", help="choose a regexp")
	parser.add_option("-v", "--verbose", action="store_true", dest="verbose", help="display added module")
	parser.add_option("--list-option", action="store_true", dest="listOption", help="list avaible options and commands")
	opts, args = parser.parse_args(options.argv)

	if opts.listOption :
		for opt in parser.option_list :
			print opt.get_opt_string()
		return 0
    
	if len(args) < 3 :
		parser.print_usage(sys.stderr)
		return ERROR_INVALID_NB_OF_ARGS

	module = args[0]
	url = args[1]
	comment = " ".join(args[3:])
	version = args[2]
	
	if conf.has_section(module) :
		print >> sys.stderr, "Module %s already exists." % module
		return ERROR_MODULE_EXISTS
	
	urlFile = urllib.urlopen(url)
	urlData = urlFile.read()
	urlFile.close()

	borders = set(re.findall(r'([>"\'][^>"\']*)%s([^>"]{0,15}[<"\'])' % re.escape(version), urlData))
	
	if len(borders) == 0 :
		print >> sys.stderr, "Error: no version found."
		return ERROR_NO_VERSION_FOUND
	
	candidates = []
	regexp = ""
	for border in borders :
		lBorder, rBorder = border
		
		# remove part of border if version is found in it
		if version in lBorder :
			lBorder = lBorder[lBorder.find(version)+len(version):]
		if version in rBorder :
			rBorder = rBorder[:lBorder.find(version)]
			
		# escape characters so they can be used in regexp
		lBorder = re.escape(lBorder)
		rBorder = re.escape(rBorder)
		
		# create regexp candidate
		candidate = r'%s([^<>\n]{1,30})%s' % (lBorder, rBorder)
	
		# select regexp... actually, the best is the largest
		if len(candidate) > len(regexp) :
			regexp = candidate
			
		candidates.append(candidate)
	
	candidates.sort()

	if opts.choose :
		print >> sys.stderr, "Avaible candidates: "
		for i, candidate in enumerate(candidates) :
			print >> sys.stderr, "%i. %s" % (i, candidate)
			print >> sys.stderr, '    %s' % repr(re.findall(candidate, urlData))
			print >> sys.stderr
		# get number
		selectRegExp = -1
		while selectRegExp == -1 :
			print >> sys.stderr, "Choose a regexp candidate: ",
			try :
				selectRegExp = int(sys.stdin.readline())
				if selectRegExp < 0 or selectRegExp >= len(candidates) :
					selectRegExp = -1
			except ValueError :
				pass
		regexp = candidates[selectRegExp]

	current = list(set(re.findall(regexp, urlData)))

	# test if a version can be found
	if len(current) != 0 :
		conf.add_section(module)
		conf.set(module, 'url', url)
		conf.set(module, 'regexp', regexp)
		conf.set(module, 'comment', comment)
		conf.set(module, 'current', repr(current))
		
		if opts.verbose :
			printModule(conf, module, out, True)
	else :
		# no version found... exit
		print >> sys.stderr, "Error: no version found."
		return ERROR_NO_VERSION_FOUND


def commandCheck(options, conf, out) :
	"Check version"
	parser = OptionParser(usage="uptodate [options] check [options] [name] ...", description=commandCheck.__doc__)
	parser.add_option("-a", "--all", action="store_true", dest="all", help="check all modules")
	parser.add_option("-r", "--removed", action="store_true", dest="removed", help="display removed versions")
	parser.add_option("-A", "--added", action="store_true", dest="added", help="display added versions")
	parser.add_option("-v", "--verbose", action="store_true", dest="verbose", help="display what is done")
	parser.add_option("--list-option", action="store_true", dest="listOption", help="display what is done")
	opts, args = parser.parse_args(options.argv)

	if opts.listOption :
		for opt in parser.option_list :
			print opt.get_opt_string()
		for module in conf.sections() :
			print module
		return 0
    
	if not opts.added and not opts.removed :
		# by default, both removed and added are displayed
		opts.added = True
		opts.removed = True
	
	modules = args
	if opts.all :
		modules = sorted(conf.sections())
	else :
		wrongModules = set(modules) - set(conf.sections())
		if wrongModules :
			print >> sys.stderr, "Error: %s doesn't exist." % ", ".join(wrongModules)
			return ERROR_MODULE_NOT_FOUND

	if len(modules) == 0 :
		# no module to check... print usage and exit
		parser.print_usage(sys.stderr)
		return
	
	for module in modules :
		if opts.verbose :
			print >> sys.stderr, "Checking %s" % module
		# get module properties
		url = conf.get(module, 'url')
		regexp = conf.get(module, 'regexp').replace('\\\\', '\\')
		currentVersions = eval(conf.get(module, 'current'))
		# get new versions
		newVersions = getVersions(module, url, regexp)
		# test if a version can be found
		if len(newVersions) != 0 :
			conf.set(module, 'current', repr(newVersions))
			
			added = set(newVersions) - set(currentVersions)
			removed = set(currentVersions) - set(newVersions)
			if added and opts.added :
				print >> out, "%s: %s added." % (module, ", ".join(map(repr, added)))
			if removed and opts.removed :
				print >> out, "%s: %s removed." % (module, ", ".join(map(repr, removed)))
		else :
			# no version found ... exit
			print >> sys.stderr, "Warning, no version found for %s. Nothing will be stored." % module
			# return
		

def commandEdit(options, conf, out) :
	"Edit module properties"
	parser = OptionParser(usage="uptodate [options] edit [options] name property value", description=commandEdit.__doc__)
	parser.add_option("-v", "--verbose", action="store_true", dest="verbose", help="display modified module")
	parser.add_option("--list-option", action="store_true", dest="listOption", help="list avaible options and commands")
	opts, args = parser.parse_args(options.argv)

	if opts.listOption :
		for opt in parser.option_list :
			print opt.get_opt_string()
		for module in conf.sections() :
			print module
		return 0
    
	if len(args) != 3 :
		parser.print_usage(sys.stderr)
		return ERROR_INVALID_NB_OF_ARGS

	module, prop, value = args
	if module not in conf.sections() :
		print >> sys.stderr, "Error: %s doesn't exist." % module
		return ERROR_MODULE_NOT_FOUND
	
	if conf.has_option(module, prop) :
		conf.set(module, prop, value)
		if opts.verbose :
			printModule(conf, module, out, True)
	else :
		print >> sys.stderr, "Error: %s property doesn't exist." % prop
		return ERROR_PROPERTY_NOT_FOUND
		
	if prop in ["regexp", "url"] :
		current = getVersions(module, conf.get(module, 'url'), conf.get(module, 'regexp'))
		if len(current) == 0 :
			print >> sys.stderr, "Error: no version found."
			return ERROR_NO_VERSION_FOUND
		conf.set(module, 'current', repr(current))

def commandRemove(options, conf, out) :
	"Remove a module"
	parser = OptionParser(usage="uptodate [options] remove [options] [name] ...", description=commandRemove.__doc__)
	parser.add_option("-a", "--all", action="store_true", dest="all", help="remove all modules")
	parser.add_option("-v", "--verbose", action="store_true", dest="verbose", help="display what is done")
	parser.add_option("--list-option", action="store_true", dest="listOption", help="list avaible options and commands")
	opts, args = parser.parse_args(options.argv)

	if opts.listOption :
		for opt in parser.option_list :
			print opt.get_opt_string()
		for module in conf.sections() :
			print module
		return 0
    
	modules = args
	if opts.all :
		modules = sorted(conf.sections())
	else :
		wrongModules = set(modules) - set(conf.sections())
		if wrongModules :
			print >> sys.stderr, "Error: %s doesn't exist." % ", ".join(wrongModules)
			return ERROR_MODULE_NOT_FOUND


	if len(modules) == 0 :
		parser.print_usage(sys.stderr)
		return ERROR_INVALID_NB_OF_ARGS
	
	for module in modules :
		if opts.verbose :
			printModule(conf, module, out, True)
		conf.remove_section(module)
	

def commandDisplay(options, conf, out) :
	"Display configured modules"
	parser = OptionParser(usage="uptodate [options] display [options] [name] ...", description=commandDisplay.__doc__)
	parser.add_option("-a", "--all", action="store_true", dest="all", help="display all modules")
	parser.add_option("-v", "--verbose", action="store_true", dest="verbose", help="display more info")
	parser.add_option("--list-option", action="store_true", dest="listOption", help="list avaible options and commands")
	opts, args = parser.parse_args(options.argv)

	if opts.listOption :
		for opt in parser.option_list :
			print opt.get_opt_string()
		for module in conf.sections() :
			print module
		return 0
    
	modules = args
	if opts.all :
		modules = sorted(conf.sections())
	else :
		wrongModules = set(modules) - set(conf.sections())
		if wrongModules :
			print >> sys.stderr, "Error: %s doesn't exist." % ", ".join(wrongModules)
			return ERROR_MODULE_NOT_FOUND

	if len(modules) == 0 :
		parser.print_usage(sys.stderr)
		return ERROR_INVALID_NB_OF_ARGS
	
	for module in modules :
		printModule(conf, module, out, opts.verbose)


def commandCopy(options, conf, out) :
	"Copy a module"
	parser = OptionParser(usage="uptodate [options] copy name name", description=commandCopy.__doc__)
	parser.add_option("-v", "--verbose", action="store_true", dest="verbose", help="display copied module")
	parser.add_option("--list-option", action="store_true", dest="listOption", help="list avaible options and commands")
	opts, args = parser.parse_args(options.argv)

	if opts.listOption :
		for opt in parser.option_list :
			print opt.get_opt_string()
		for module in conf.sections() :
			print module
		return 0
    
	if len(args) != 2 :
		parser.print_usage(sys.stderr)
		return ERROR_INVALID_NB_OF_ARGS

	module, new = args
	if module not in conf.sections() :
		print >> sys.stderr, "Error: %s doesn't exist." % module
		return ERROR_MODULE_NOT_FOUND
	
	if not conf.has_section(new) :
		conf.add_section(new)
		for prop in conf.options(module) :
			conf.set(new, prop, conf.get(module, prop))
		if opts.verbose :
			printModule(conf, new, out, True)
	else :
		print >> sys.stderr, "Module %s already exists." % new
		return ERROR_MODULE_EXISTS

	
	
def commandRename(options, conf, out) :
	"Rename a module"
	parser = OptionParser(usage="uptodate [options] rename name newName", description=commandRename.__doc__)
	parser.add_option("-v", "--verbose", action="store_true", dest="verbose", help="display renamed module")
	parser.add_option("--list-option", action="store_true", dest="listOption", help="list avaible options and commands")
	opts, args = parser.parse_args(options.argv)

	if opts.listOption :
		for opt in parser.option_list :
			print opt.get_opt_string()
		for module in conf.sections() :
			print module
		return 0
    
	if len(args) != 2 :
		parser.print_usage(sys.stderr)
		return ERROR_INVALID_NB_OF_ARGS

	module, new = args
	if module not in conf.sections() :
		print >> sys.stderr, "Error: %s doesn't exist." % module
		return ERROR_MODULE_NOT_FOUND
	
	if not conf.has_section(new) :
		conf.add_section(new)
		for prop in conf.options(module) :
			conf.set(new, prop, conf.get(module, prop))
		conf.remove_section(module)
		if opts.verbose :
			printModule(conf, new, out, True)
	else :
		print >> sys.stderr, "Module %s already exists." % new
		return ERROR_MODULE_EXISTS

	
	
def printModule(conf, module, out, verbose=False) :
	"""
	printModule(conf, module, out, verbose)

	print module info to out file. Infos comes from conf object.
	"""
	url = conf.get(module, 'url')
	regexp = conf.get(module, 'regexp').replace('\\\\', '\\')
	current = conf.get(module, 'current')
	comment = conf.get(module, 'comment')
	
	if verbose :
		print >> out, module
		print >> out, "\tcomment= "+comment
		print >> out, "\turl= "+url
		print >> out, "\tregexp= "+regexp
		print >> out, "\tcurrent= "+current
	else :
		s = module
		if comment :
			s += " (%s)" % comment
		print >> out, s
		


USAGE="uptodate [options] command [options] [arguments]"

COMMANDS = {'add': commandAdd,
	    'check': commandCheck,
	    'edit': commandEdit,
	    'remove': commandRemove,
	    'display': commandDisplay,
	    'copy': commandCopy,
	    'rename': commandRename,
	    'auto': commandAuto,
	    }

comString = ""
fill = max([len(c) for c in COMMANDS.keys()])
for command in sorted(COMMANDS.keys()) :
	comString += "  %s  %s\n" % (command.ljust(fill), COMMANDS[command].__doc__.splitlines()[0].strip())

errString = ""
fill = max([len(str(c)) for c in ERRORS.keys()])
for error in sorted(ERRORS.keys()) :
	errString += "  %s  %s\n" % (str(error).ljust(fill), ERRORS[error])

DESCRIPTION='''Commands:
%s
Run "uptodate command --help" for more information.

Exit codes:
%s
''' % (comString, errString)


def parse_options(argv):
    parser = OptionParser(usage=USAGE,
                          description=DESCRIPTION,
                          version="uptodate %s" % VERSION,
			  formatter=UptodateHelpFormatter())
    parser.disable_interspersed_args()
    parser.add_option("-o", "--output", dest="outputPath", default="-", metavar="FILE", help="write output to FILE")
    parser.add_option("-c", "--config-file", dest="configPath", default=os.path.expanduser("~/.uptodate"), metavar="FILE", help="configuration file")
    parser.add_option("-d", "--dry-run", action="store_true", dest="dryRun", help="do not modify config file")
    parser.add_option("--list-option", action="store_true", dest="listOption", help="list avaible options and commands")
    opts, args = parser.parse_args()
    
    if opts.listOption :
    	for opt in parser.option_list :
		print opt.get_opt_string()
	for command in COMMANDS :
		print command
	raise Exception(0)
    
    if args and args[0] in COMMANDS.keys() :
        opts.command = args[0]
        opts.argv = args[1:]
    else:
        opts.command = None
        opts.argv = []
    if not opts.command:
        parser.print_usage()
        raise Exception(ERROR_UNKNOWN_COMMAND)
    return opts



def getVersions(module, url, regexp) :
    urlFile = urllib.urlopen(url)
    urlData = urlFile.read()
    urlFile.close()
    new = re.findall(regexp, urlData)
    return list(set(new))



def file(fName, output=False, notNone=False, append=False) :
        """Renvoi un fichier en fonction du nom de fichier pass en paramtre.
        
        fName est le chemin d'acces complet a un fichier.
        fName = "-" est interprt comme l'entre (ou la sortie, si output=True) standard.
        output = True indique que le ficher doit etre ouvert en ecriture
        notNone = True provoque le renvoi d'un fichier ouvert sur /dev/null plutot que None quand fName = ""
        append provoque l'ouverture du ficheir en mode append. Doit etre utilis avec output = True
        """
        if append :
                wMode = "a"
        else :
                wMode = "w"
        
        #le nom de fichier est vide...
        if fName == "" :
                if notNone :
                        if output :
                                return file("/dev/null", wMode)
                        else :
                                return file("/dev/null")
                else :
                        return None
        
        elif fName == "-" :
                #la sortie ou l'entree standard
                if output :
                        return sys.stdout
                else :
                        return sys.stdin
        else :
                # un fichier normal
                if output :
                        return __builtin__.file(fName, wMode)
                else :
                        return __builtin__.file(fName)



class UptodateHelpFormatter(IndentedHelpFormatter) :
	def format_description(self, description):
		return description.strip()



def main(argv) :
	try :
		options = parse_options(argv)
	except Exception, e :
		return e.args[0]
	
	# load config in a file to be able to load it from stdin
	import ConfigParser
	conf = ConfigParser.SafeConfigParser()
	try :
		# try to load config
		confFile = file(options.configPath)
	except :
		# load fail... try to create it
		file(options.configPath, True).close()
		confFile = file(options.configPath)
	conf.readfp(confFile)
	confFile.close()

	# set output file
	out = file(options.outputPath, True)
	
	# execute command
	retVal = COMMANDS[options.command](options, conf, out)
	
	if retVal :
		return retVal
	else :
		# save config file, if needed
		if not options.dryRun :
			confFile = file(options.configPath, 'w')
			conf.write(confFile)
			confFile.close()
		return 0






if __name__ == '__main__':
	import sys
	try :
		sys.exit(main(sys.argv[1:]))

	except IOError, e:
		sys.stderr.write(sys.argv[0]+": ")
		if e.filename :
			sys.stderr.write(e.filename+": ")
		sys.stderr.write(str(e.strerror)+"\n")
		sys.exit(e.errno)

	except KeyboardInterrupt, e :
		sys.stderr.write(str(e)+"\n")
		sys.exit(130)
		




