#
#  $Id: Application.py,v 1.1 1999/12/16 09:36:48 rob Exp $
#
#  Copyright 1999 Rob Tillotson <robt@debian.org>
#  All Rights Reserved
#
#  Permission to use, copy, modify, and distribute this software and
#  its documentation for any purpose and without fee or royalty is
#  hereby granted, provided that the above copyright notice appear in
#  all copies and that both the copyright notice and this permission
#  notice appear in supporting documentation or portions thereof,
#  including modifications, that you you make.
#
#  THE AUTHOR ROB TILLOTSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
#  THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
#  AND FITNESS.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
#  SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
#  RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
#  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
#  CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE!
#
"""Sulfur Applications

This module implements the Sulfur high-level application interface.
In some object-oriented languages (Java, for example) an application
is defined as a subclass of a standard application or applet object.
The superclass provides all of the common behavior (startup, argument
parsing, etc) and also provides an interface for operations common
to many applications.

This module follows that model by defining an Application class which
can be used to make Python application programming more consistent.
A Sulfur application defines an object which is a subclass of
Application, and provides custom code within the 'run' method.  Running
the application entails importing the subclass definition, instantiating
an object, and calling it, i.e. something like:

  #!/usr/bin/python
  import MyApp
  a = MyApp.MyAppClass()
  a()

The Application class will provide high-level functions for plugin
management, easily locating standard directories and files, and
so forth; the goal is to hide most of the implementation details from
the programmer.
"""

__version__ = '$Id: Application.py,v 1.1 1999/12/16 09:36:48 rob Exp $'

__copyright__ = 'Copyright 1999 Rob Tillotson <robt@debian.org>'

from types import ClassType
import sys, os, operator, string

from Sulfur import Plugin, Registry, Options, GUI, misc

from Sulfur.Options import O_NOCONFIG, O_NONINTERACTIVE, O_NOCMDLINE, O_MULTIPLE

# DEAD (move to AppContext)
class OptionParsingError(Exception):
    pass

# DEAD (move to AppContext)
def parse_cmd_options(argv, opt={}):
    values = {}
    
    # build a dict of aliases
    ali = {}
    for p, o in opt.values():
	if o.has_flag(O_NOCMDLINE):
	    continue
	if not o.cmd_names:
	    ali[o.name] = o.name
	else:
	    for a in o.cmd_names:
		ali[a] = o.name
    
    argv_out = []
    passall = 0
    long = 0
    while argv:
	a, argv = argv[0], argv[1:]
	if not a: continue

	if passall or a[0] != '-':
	    argv_out.append(a)
	else:
	    a = a[1:]
	    if not a: continue
	    if a[0] == '-':
		a = a[1:]
		if not a:
		    passall = 1
		    continue
		long = 1
	    else:
		long = 0

	    if long:
		if not ali.has_key(a): raise OptionParsingError, 'no such option --%s' % a
		p, o = opt[ali[a]]
		if o.type == Options.BOOLEAN: p.set_option(o.name, not o.default)
		else:
		    if not argv: raise OptionParsingError, 'option --%s requires a value' % a
		    a, argv = argv[0], argv[1:]
		    if o.has_flag(O_MULTIPLE):
			p.append_option(o.name, o.from_str(a))
		    else:
			p.set_option(o.name, o.from_str(a))
	    else:
		for aa in a:
		    if not ali.has_key(aa): raise OptionParsingError, 'no such option -%s' % a
		    p, o = opt[ali[aa]]
		    if o.type == Options.BOOLEAN: p.set_option(o.name, not o.default)
		    elif len(a) > 1:
			raise OptionParsingError, 'option -%s cannot be combined with other options' % aa
		    else:
			aa, argv = argv[0], argv[1:]
			if o.has_flag(O_MULTIPLE):
			    p.append_option(o.name, o.from_str(aa))
			else:
			    p.set_option(o.name, o.from_str(aa))
	    
    return argv_out

# DEAD (move to AppContext)
def process_help(d):
    k = d.keys()
    k.sort()
    l = []
    for ok in k:
	o = d[ok]
	# skip options that are not allowed on the command line
	if o.has_flag(O_NOCMDLINE):
	    continue
	n = ''
	if not o.cmd_names: cn = [o.name]
	else: cn = o.cmd_names
	for a in cn:
	    if not n:
		if len(a) == 1: n = '-'+a
		else: n = '--'+a
	    else:
		# cheesy cosmetic hack
		if len(a) == 1 and len(o.name) > 1:
		    n = '-'+a+', ' + n
		elif len(a) == 1:
		    n = n + ', -'+a
		else:
		    n = n + ', --'+a

	if o.type == Options.STRING: n = n + ' STR'
	elif o.type == Options.INTEGER or o.type == Options.FLOAT: n = n + ' NUM'
	elif o.type == Options.FILENAME: n = n + ' FILE'
	elif o.type == Options.CHOICE: n = n + ' OPT'

	if o.has_flag(O_MULTIPLE): n = n + '+'
	
	l.append( (n, o.title, o.default) )

    return l

def help_string(h, maxlen=40):
    l = []
    for n, t, d in h:
	l.append('  '+n)
	if len(n) <= maxlen: l.append(' '*(maxlen-len(n))+'  ')
	elif len(n) > maxlen: l.append('\n')

	l.append(t)
	if d:
	    l.append(' [%s]' % d)
	l.append('\n')
    return string.join(l, '')
	    

class Application(Plugin.Plugin):
    """A Sulfur application.

    The intent of this class is that applications wishing to use
    Sulfur will derive a subclass of this, in much the same way as
    apps in some other languages derive from a common 'Application'
    or 'Applet' superclass.  This class will thus provide both a
    base-level API as well as a place to hook in pre-defined
    initialization behavior, like finding the default user name
    and the like.
    """
    type = 'Application'
    name = 'DefaultApplication'
    extra_help = ''
    # DEAD
    auto_cmd_line = 1
    # DEAD
    auto_cmd_help = 1
    
    options = [
	Options.Boolean('help', 0, 'get help', None, (O_NOCONFIG, O_NONINTERACTIVE),
			['help', 'h']),
	]
    
    def __init__(self):
	Plugin.Plugin.__init__(self)

	self.config_path = self.name
	
	self.registry = None

	self.plugin_module_path = ['Sulfur']
	self.plugins = {}

    def prerun(self):
	if hasattr(self, 'get_config') and callable(self.get_config):
	    self.get_config()
    
    def postrun(self):
	pass

    def __call__(self, *a, **kw):
	import AppContext
	ctx = AppContext.CLIAppContext()
	ctx.app = self
	apply(ctx, a, kw)

    # DEAD
    def __old__call__(self, *a, **kw):
	self.registry = Registry.Registry()
	self.prerun()
	if self.auto_cmd_line:
	    try:
		argv = self.process_options(sys.argv[1:])
	    except OptionParsingError, err:
		print "error:", err
		print
		self.process_help()
		sys.exit(1)
	    apply(self.run, (argv,)+a, kw)
	else:
	    apply(self.run, a, kw)
	self.postrun()
	
    def run(self, *a, **kw):
	"""Run the application.
	This is just a stub, to be overridden in each subclass.
	It is called with whatever arguments were passed when the
	Application object was called.
	"""
	pass

    # And now, the API

    # First, some plugin support.
    def get_plugin(self, collection, name, *a, **kw):
	"""Find, load, and instantiate a plug-in.
	"""
	if self.context is not None and hasattr(self.context, 'get_plugin'):
	    return apply(self.context.get_plugin, (collection, name)+a, kw)
	# DEAD
	if not name: return None
	if collection: pname = collection + '.' + name
	else: pname = name
	if self.plugins.has_key(pname): return self.plugins[pname]
	
	m = None
	for pe in self.plugin_module_path:
	    if collection:
		if pe: pe = pe + '.' + collection
		else: pe = collection
	    elif not pe:
		# __import__ needs a module name as the first parameter, so
		# supporting '' in the path will require extra work below.
		# For now, the above code works fine to look for *collections*
		# in the current directory, and I'll just hope nobody looks
		# in the current directory for plugins that are outside a
		# collection ;-)
		continue
	    try:
		m = misc.import_module(pe+'.'+name, globals())
		#n = __import__(pe, globals(), locals(), [name])
		#m = n.__dict__[name]
		break
	    except:
		pass
	    
	if m is None: raise ImportError, 'Plugin: %s.%s' % (collection, name)

	for o in m.__dict__.values():
	    if type(o) == ClassType and hasattr(o, 'is_plugin'):
		p = apply(o, a, kw)
		p.set_context(self)
		p.configure(self.registry)
		self.plugins[pname] = p
		return p

    # DEAD
    def _new_get_plugin(self, collection, name, *a, **kw):
	pname = collection + '.' + name
	
	if self.plugins.has_key(pname): return self.plugins[pname]

	for pe in self.plugin_module_path:
	    if pe: pe = pe + '.' + collection
	    else: pe = collection

	    try:
		m = misc.import_module(pe+'.'+name, globals())
		for o in m.__dict__.values():
		    if type(o) == ClassType and hasattr(o, 'is_plugin'):
			p = apply(o, a, kw)
			p.set_context(self)
			p.configure(self.registry)
			self.plugins[pname] = p
			return p
	    except:
		pass
	raise ImportError, 'Plugin: %s.%s' % (collection, name)
    
	    
    def list_plugins(self, collection):
	"""List plugins in a particular collection.
	"""
	if self.context is not None and hasattr(self.context, 'list_plugins'):
	    return self.context.list_plugins(collection)
	# DEAD
	else:
	    return misc.list_plugins(self.plugin_module_path, collection)

    def list_plugin_info(self, collection):
	"""List information about plugins in a particular collection.
	"""
	if self.context is not None and hasattr(self.context, 'list_plugin_info'):
	    return self.context.list_plugin_info(collection)
	# DEAD
	else:
	    return misc.list_plugin_info(self.plugin_module_path, collection)
    
    # runtime (i.e. command line) options
#      # DEAD
#      def process_help(self, extra_plugins=[], extra_options=[]):
#  	hl = []
#  	l = []
#  	d = {}
#  	d.update(self.option_refs)
#  	for o in extra_options: d[o.name] = o
#  	hl.append( (self, process_help(d)) )
#  	for p in extra_plugins: hl.append( (p, process_help(p.option_refs)) )

#  	# who says Python can't be written obscurely?
#  	maxlen = max(reduce(operator.add,
#  			    map(lambda x: map(lambda y: len(y[0]), x),
#  				map(lambda x: x[1], hl)), []))

#  	if maxlen > 40: maxlen = 40

#  	for p, h in hl:
#  	    if p is not self: l.append('%s: ' % (p.type_name and p.type_name or p.type))
#  	    l.append('%s %s by %s\n' % (p.name, p.version, p.author))
#  	    l.append(p.description)
#  	    l.append('\n')
#  	    if h:
#  		l.append('\n')
#  		l.append(help_string(h, maxlen))
#  		l.append('\n')

#  	if self.extra_help: l.append(self.extra_help)
	
#  	print string.join(l, '')

#      # DEAD
#      def process_options(self, argv = [], extra_plugins=[]):
#  	"""Process runtime options.

#  	Constructs a set of available options from this object, plus
#  	the plugins listed in extra_plugins.  Obtains option values
#  	from the user, and then stores them in the Option objects
#  	already collected.

#  	How this function obtains option values from the user is
#  	system-dependent; the code here gets the options from the
#  	command line, but Tk-based code might be added later.

#  	Returns any extra information the user specified, generally
#  	a list of strings.
#  	"""
#  	opts = {}
#  	for k, v in self.option_refs.items(): opts[k] = (self, v)
#  	for p in extra_plugins:
#  	    for k, v in p.option_refs.items(): opts[k] = (p, v)
	    
#  	# command line
#  	argv = parse_cmd_options(argv, opts)

#  	if self.auto_cmd_help and self.has_option('help') \
#  	   and self.get_option('help'):
#  	    self.process_help(extra_plugins)
	    
#  	return argv


    # GUI support.
    def gui_systems(self):
	"""Return a list of GUI systems.

	Not all of these systems will necessarily be available; this
	is just a list of ones Sulfur has support for.

	If an application includes GUI code, it should override this
	to list only those systems which it supports.
	"""
	return GUI.system_names[:]

    def gui_pick_system(self, systems=None):
	"""Pick a GUI system.

	Checks a list of choices and returns the first one that matches.
	"""
	if systems is None: systems = self.gui_systems()
	return GUI.test_systems(systems)

    def gui_init(self, systems=None, *a, **kw):
	"""Initialize Sulfur GUI support.
	"""
	s = self.gui_pick_system(systems)
	if not s:
	    raise RuntimeError, 'no GUI system available'
	# GUIs are just plugins!
	self.gui = self.get_plugin('GUI',s)
			

