# Part of the A-A-P project: Action execution module

# Copyright (C) 2002 Stichting NLnet Labs
# Permission to copy and use this file is specified in the file COPYING.
# If this file is missing you can find it here: http://www.a-a-p.org/COPYING

#
# This module selects commands to be executed for an action on a file 
#
# EXTERNAL INTERFACE:
#
# action_run(recdict, action, dict, fname)
#				Detects the type of file "fname", selects
#				the application to do "action" and runs it.
#				"dict" has attributes for customization.
#
# action_add(rpstack, recdict, args, commands)
#				Add a connection between action-filetype to
#				commands
#

import string
import os
import os.path

from Util import *
from RecPos import RecPos
from Dictlist import listitem2str, str2dictlist, dictlist2str
from ParsePos import ParsePos
from Process import Process
from Filetype import filetype_root
from Commands import expand
from DoBuild import get_build_recdict

# All actions are stored in this dictionary.  Use this as:
#    _action_dict[action][in-filetype][out-filetype] = Action()
_action_dict = {}

class Action:
    """Class to store the commands and rpstack for an action/filetype."""
    def __init__(self, rpstack, recdict, attributes, commands):
	self.rpstack = rpstack
	self.buildrecdict = recdict
	self.attributes = attributes
	self.commands = commands

    def __str__(self):
	return self.commands


def action_add(rpstack, recdict, arglist, commands):
    """Add "commands" for an action-filetype combination defined by dictlist
       "arglist".
       "rpstack" is used for an error message when executing the commdands."""
    global _action_dict

    if len(arglist) == 2:
	outtypes = "default"
	intypes = arglist[1]["name"]
    elif len(arglist) == 3:
	outtypes = arglist[1]["name"]
	intypes = arglist[2]["name"]
    else:
	from Process import recipe_error
	recipe_error(rpstack, _(':action must have two or three arguments'))

    act = Action(rpstack, recdict, arglist[0], commands)
    for action in string.split(arglist[0]["name"], ','):
	for intype in string.split(intypes, ','):
	    for outtype in string.split(outtypes, ','):
		if not _action_dict.has_key(action):
		    _action_dict[action] = {}
		if not _action_dict[action].has_key(intype):
		    _action_dict[action][intype] = {}
		_action_dict[action][intype][outtype] = act


def action_ftype(recdict, action, dict):
    """Decide what filetype to use for "action" for the file specified with
       dictionary "dict"."""
    if dict.has_key("filetype"):
	return dict["filetype"]

    from Work import getwork
    fname = dict["name"]
    node = getwork(recdict).find_node(fname)
    if node and node.attributes.has_key("filetype"):
	return node.attributes["filetype"]

    # For viewing a remote file the filetype doesn't really matter, need to
    # use a browser anyway.
    # TODO: expand list of methods
    i = string.find(fname, "://")
    if (i > 0 and fname[:i] in [ "http", "https", "ftp" ]
						     and action == "view"):
	return "html"

    # Detect the filetype
    from Filetype import ft_detect
    return ft_detect(fname, 1, recdict)


def has_action(recdict, action, dict):
    """Return non-zero if "action" is defined for the file specified with
       "dict".  Does not use the default."""
    if not _action_dict.has_key(action):
	return 0
    return _action_dict[action].has_key(action_ftype(recdict, action, dict))


def find_depend_action(ftype):
    """Find the depend action for a source file with type "ftype"."""
    if not _action_dict.has_key("depend"):
	return None

    if not _action_dict["depend"].has_key(ftype):
	root = filetype_root(ftype)
	if _action_dict["depend"].has_key(root):
	    ftype = root
	elif _action_dict["depend"].has_key("default"):
	    ftype = "default"
	else:
	    return None
    if _action_dict["depend"][ftype].has_key("default"):
	return _action_dict["depend"][ftype]["default"]
    return None


def find_out_ftype(recdict, action, in_ftype, out_ftype, msg):
    """Find the output filetype to be used for "action" and "in_ftype".  Use
       the first one of "out_ftype", its root or "default" that is defined.
       return the output filetype and non-zero if found, otherwise anything and
       zero."""
    if out_ftype and _action_dict[action][in_ftype].has_key(out_ftype):
	return out_ftype, 1

    if out_ftype:
	root = filetype_root(out_ftype)
	if _action_dict[action][in_ftype].has_key(root):
	    return root, 1

    if _action_dict[action][in_ftype].has_key("default"):
	if msg:
	    msg_extra(recdict, _('Using default for %s %s from %s')
					       % (action, out_ftype, in_ftype))
	return "default", 1

    return out_ftype, 0


def action_find(recdict, action, in_ftype, out_ftype, msg):
    """Find the action to be used for "action", with detected input filetype
    "in_ftype" and detected output filetype "out_ftype".
    When "msg" is non-zero give a message about using a default type."""
    # Try three input filetypes and three output filetypes for each of them.
    # If no commands defined for the detected filetype, use the root filetype.
    # If still no commands defined, use the default action.
    use_in_ftype = in_ftype
    use_out_ftype = out_ftype
    found = 0
    if _action_dict[action].has_key(in_ftype):
	use_out_ftype, found = find_out_ftype(recdict, action,
						      in_ftype, out_ftype, msg)

    if not found:
	root = filetype_root(in_ftype)
	if _action_dict[action].has_key(root):
	    use_in_ftype = root
	    use_out_ftype, found = find_out_ftype(recdict, action,
							  root, out_ftype, msg)

    if not found and _action_dict[action].has_key("default"):
	use_in_ftype = "default"
	use_out_ftype, found = find_out_ftype(recdict, action,
						  use_in_ftype, out_ftype, msg)
	if msg and found and use_out_ftype != "default":
	    msg_extra(recdict, _('Using default for %s from %s')
							  % (action, in_ftype))

    if not found:
	return None, None
    return use_in_ftype, use_out_ftype


def action_run(recdict, args):
    """Run the associated program for action "args[0]" on a list of files
       "args[1:]".  "args" is a dictlist.
       Use the attributes in "args[0]" to customize the action.
       When the "filetype" attribute isn't specified, detect it from args[1].
       Returns None for success, an error message for failure."""
    action = args[0]["name"]
    if not _action_dict.has_key(action):
	return _("Unknown action: %s") % action

    in_ftype = action_ftype(recdict, action, args[1])
    if not in_ftype:
	msg_warning(recdict,
	      _('Filetype not recognized for "%s", using "default"')
							     % args[1]["name"])
	in_ftype = "default"

    out_ftype = None
    if args[0].has_key("target"):
	t = args[0]["target"]
    elif recdict.has_key("target"):
	t = recdict["target"]
    else:
	t = None
    if t:
	try:
	    out_ftype = action_ftype(recdict, action, str2dictlist([], t)[0])
	except UserError, e:
	    return _('Error parsing target attribute: ') + str(e)

    use_in_ftype, use_out_ftype = action_find(recdict, action, in_ftype,
								  out_ftype, 1)

    if not use_in_ftype:
	return (_("No commands defined for %s %s from %s")
					       % (action, out_ftype, in_ftype))
	
    msg_extra(recdict, _('Do %s %s') % (action, use_in_ftype))
    act = _action_dict[action][use_in_ftype][use_out_ftype]

    # Make a copy of the recdict to avoid the commands modify them.
    # Take care of local and build command variables.
    new_recdict = get_build_recdict(recdict, act.buildrecdict)

    # $source is the whole list of filenames.  Need to use quotes around items
    # with white space.
    # $fname is the first file name
    new_recdict["source"] = dictlist2str(args[1:])
    new_recdict["fname"] = listitem2str(args[1])

    # $filetype is the detected input filetype
    # $targettype is the detected output filetype
    # $action is the name of the action
    new_recdict["filetype"] = in_ftype
    new_recdict["targettype"] = out_ftype
    new_recdict["action"] = action

    # Turn the attributes on the action into variables.
    for k in args[0].keys():
	new_recdict[k] = args[0][k]

    # Turn the "var_" attributes of the sources into variables.
    for s in args[1:]:
	for k in s.keys():
	    if len(k) > 4 and k[:4] == "var_":
		new_recdict[k[4:]] = s[k]

    # Create a ParsePos object to contain the parse position in the string.
    fp = ParsePos(act.rpstack, string = act.commands)

    #
    # Parse and execute the commands.
    #
    try:
	Process(fp, new_recdict)
    except UserError, e:
	return ((_('Error executing commands for %s %s: ')
					    % (action, use_in_ftype)) + str(e))

    # Move the exported variables to the recdict of the current recipe and the
    # recdict of the recipe where the action was defined.
    exports = new_recdict["_exports"]
    for e in exports.keys():
	recdict[e] = exports[e]
	if act.buildrecdict and act.buildrecdict != recdict:
	    act.buildrecdict[e] = exports[e]

    return None


def action_expand_do(recdict, commands, targetlist, sourcelist):
    """Expand ":do" commands in "commands" to the build commands they stand
       for.  Used for computing the checksum, not for executing the commands!.
       The filetype is sometimes guessed."""
    # Return quickly when there is no ":do" command.
    if string.find(commands, ":do") < 0:
	return commands

    # Remember the end of an expanded action.  It is not allowed to expand
    # again before this position, it would mean an action invokes itself and
    # causes an endless loop.
    exp_action_end = {}

    # Locate each ":do" command.  When we can find the commands of the action,
    # replace the ":do" command with them.
    idx = 0
    while 1:
	# Find the next ":do" command.
	do = string.find(commands, ":do", idx)
	if do < 0:
	    break

	# Get the arguments of the ":do" command; advance "idx" to the end.
	i = skip_white(commands, do + 3)
	idx = string.find(commands, '\n', i)
	args = str2dictlist([], commands[i:idx])
	if len(args) >= 2:
	    # The action is the first argument.
	    action = args[0]["name"]

	    if _action_dict.has_key(action):
		# Guess the input filetype to be used:
		# 1. an explicitly defined filetype after the action.
		# 2. if the first filename doesn't contain a $, get the
		#    filetype from it.
		# 3. use the filetype from the first source item
		if args[1].has_key("filetype"):
		    in_ftype = args[1]["filetype"]
		elif not '$' in args[1]["name"]:
		    in_ftype = action_ftype(recdict, action, args[1])
		else:
		    in_ftype = None
		if not in_ftype and sourcelist:
		    # Get the filetype of the first source item.
		    in_ftype = action_ftype(recdict, action, sourcelist[0])
		if not in_ftype:
		    in_ftype = "default"

		# Guess the output filetype to be used:
		# 1. an explicitly defined filetype after the target.
		# 2. if the first filename doesn't contain a $, get the
		#    filetype from it.
		# 3. use the filetype from the first source item
		tt = recdict.get("target")
		try:
		    recdict["target"] = targetlist[0]["name"]
		    target = str2dictlist([], expand(0, recdict,
			    args[0]["target"], Expand(1, Expand.quote_aap)))[0]
		except:
		    target = targetlist[0]
		if recdict.has_key("target"):
		    if tt is None:
			del recdict["target"]
		    else:
			recdict["target"] = tt
		if target.has_key("filetype"):
		    out_ftype = target["filetype"]
		elif not '$' in target["name"]:
		    out_ftype = action_ftype(recdict, action, target)
		else:
		    out_ftype = None
		if not out_ftype:
		    out_ftype = "default"

		# Find the action to be used for these filetypes, falling back
		# to "default" when necessary.
		in_ftype, out_ftype = action_find(recdict, action,
							in_ftype, out_ftype, 0)
					
		if in_ftype:
		    # Check if this action isn't used recursively.
		    act = _action_dict[action][in_ftype][out_ftype]
		    cmd = act.commands
		    key = action + '@' + in_ftype + '@' + out_ftype
		    if (exp_action_end.has_key(key)
					     and exp_action_end[key] > do):
			# :do command starts before expanded commands.
			act = None

		    if act:
			# Correct the end for the already expanded actions.
			for k in exp_action_end.keys():
			    if exp_action_end[k] > idx:
				exp_action_end[k] = (exp_action_end[k]
						   + len(cmd) - (idx - do))

			# Set the end of the currently expanded action.
			exp_action_end[key] = do + len(cmd)

			# Replace the ":do" command with the commands of
			# the action
			commands = commands[:do] + cmd + commands[idx:]

			# Continue at the start of the replaced commands,
			# so that it works recursively.
			idx = do

    return commands


# vim: set sw=4 sts=4 tw=79 fo+=l:
