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

# Copyright (C) 2002-2003 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
#
# action_get_list()             Return a dictionary that lists all available
#                               actions.
#

import string
import os
import os.path

from Util import *
from Dictlist import listitem2str, str2dictlist, dictlist2str
from ParsePos import ParsePos
from Process import Process, recipe_error
from Filetype import filetype_root, ft_known
from Commands import expand
from Scope import get_build_recdict
import RecPython

# All actions are stored in this dictionary.
# Key is the name of the action.  Value is a list of Action objects.
# When multiple actions match, the last action in the list is preferred over
# earlier actions.
# Use this as:
#    _action_dict[action].append(Action())
_action_dict = {}

class Action:
    """
    Class to store the commands and rpstack for an action/filetype.
    In a derived class get_in_types() and get_out_types() may be implemented
    differently.
    """
    def __init__(self, rpstack, recdict, attributes, commands,
                            outtypes = [], intypes = [],
                            defer_var_names = [],
                            outfilename = ''):
        self.rpstack = rpstack
        self.buildrecdict = recdict
        self.attributes = attributes
        self.commands = commands
        self.outtypes = outtypes
        self.intypes = intypes
        self.primary = attributes.get("primary", 0)
        self.defer_var_names = defer_var_names
        self.outfilename = outfilename

    def get_out_types(self, source = "", intype = None):
        """
        Return list of supported output filetypes.
        When "intype" is given, this is the required output type.
        """
        aname = self.defer_action_name(source = source)
        if aname:
            return get_ftypes(aname, 0, intype = intype)
        if intype and intype not in self.intypes:
            return []
        return self.outtypes

    def get_in_types(self, source = "", outtype = None):
        """
        Return list of supported input filetypes.
        When "outtype" is given, this is the required output type.
        """
        aname = self.defer_action_name(source = source)
        if aname:
            return get_ftypes(aname, 1, outtype = outtype)
        if outtype and outtype not in self.outtypes:
            return []
        return self.intypes

    def defer_action_name(self, source = ""):
        """Return name of action to defer the work to."""
        for n in self.defer_var_names:
            if len(n) > 2 and n[0] == '{' and n[-1] == '}':
                aname = RecPython.get_var_attr(source, n[1:-1])
            else:
                aname = get_var_val(0, Global.globals, "_no", n)
            if aname:
                return aname
        return None

    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.
    """

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

    # Check that the filetype names exist
    for i in intypes + outtypes:
        if i != "default" and not ft_known(i):
            msg_warning(recdict, _('unknown filetype "%s" in :action command; recipe %s line %d') % (i, rpstack[-1].name, rpstack[-1].line_nr))

    act = Action(rpstack, recdict, arglist[0], commands, outtypes, intypes)
    
    for action in string.split(arglist[0]["name"], ','):
        action_add_to_dict(action, act)


def action_add_to_dict(action, act):
    """
    Add action object "act" to the dictionary of actions.
    """
    global _action_dict
    if not _action_dict.has_key(action):
        _action_dict[action] = []
    _action_dict[action].append(act)


def find_action(action, intype, outtype):
    """
    Find the last action that supports intype and outtype.
    """
    act = None
    for a in _action_dict[action]:
        if intype in a.get_in_types(outtype = outtype) and outtype in a.get_out_types(intype = intype):
            act = a;
    return act


def find_action_route(in_ftype, out_ftype):
    """
    Find a route from "in_ftype" to "out_ftype" with primary actions.
    Returns a route object or None.
    """
    # Use a list of dictionaries.  Each dictionary holds info about one
    # incomplete route:
    #    type       : input for the next action to be found
    #    actions    : list of action lines of the steps found so far
    #    used_types : list of intermediate types used in the steps
    trylist = [ {"type" : in_ftype, "actions" : [], "used_types" : [[in_ftype]]} ]

    # Try up to ten steps deep before giving up.
    depth = 0
    while trylist and depth < 10:
        new_trylist = []
        for ent in trylist:
            # Find actions that uses the type of this entry as input
            for action in _action_dict.keys():
              for act in _action_dict[action]:
                if act.primary and ent["type"] in act.get_in_types():
                    for atype in act.get_out_types():
                        if atype == out_ftype:
                            # Found a route!
                            from Work import Route
                            from RecPos import RecPos

                            actions = ent["actions"] + [action]
                            return Route(Global.globals,
                                    [RecPos("route from primary actions")], 0,
                                    ent["used_types"] + [[out_ftype]],
                                    {},
                                    actions, map(lambda x: 0, actions))

                        # No direct route, may add to list for next try.
                        if atype != "default" and act.outfilename:
                            # Make the line look like what ":route" uses.
                            actions = ent["actions"] + [action + " " + act.outfilename]
                            new_trylist.append({"type" : atype,
                                        "actions" : actions,
                                        "used_types" : ent["used_types"] + [[atype]]})
        depth = depth + 1
        trylist = new_trylist

    return None


def find_primary_action(action):
    """
    Find the prinary action with name "action".
    """
    act = None
    for a in _action_dict[action]:
        if a.primary:
            act = a;
    return act


def get_ftypes(action, in_types, intype = None, outtype = None):
    """
    Return the list of filetypes supported by actions with name "action".
    If "intype" given this input type must be supported.
    If "outtype" given this output type must be supported.
    """
    retval = []
    if _action_dict.has_key(action):
        for act in _action_dict[action]:
            if in_types:
                l = act.get_in_types(outtype = outtype)
            else:
                l = act.get_out_types(intype = intype)
            for i in l:
                if i not in retval:
                    retval.append(i)
    return retval


def action_get_list():
    """
    Return the dictionary that lists all actions.
    This turns _action_dict[] into another kind of dictionary for backwards
    compatibility.
    The dictionary key is the action name, the item is the intype
    dictionary.
    The intype dictionary key is the input file type, the item is the outtype
    dictionary.
    The outtype dictionary key is the output file type, the item is an Action
    object.
    retval[action-name][input-filetype][output-filetype].
    """
    retval = {}
    for action in _action_dict.keys():
        retval[action] = {}
        for act in _action_dict[action]:
            for intype in act.get_in_types():
                if not retval[action].has_key(intype):
                    retval[action][intype] = {}
                for outtype in act.get_out_types():
                    retval[action][intype][outtype] = act

    return retval


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"]

    # A ":program", ":produce" etc. adds a "filetypehint" attribute that has a
    # lower priority than a "filetype" attribute the user specifies.
    if dict.has_key("filetypehint"):
        return dict["filetypehint"]

    # 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
    intype = action_ftype(recdict, action, dict)
    for act in _action_dict[action]:
        if intype in act.get_in_types():
            return 1
    return 0


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

    found_act = None
    for intype in [ftype, filetype_root(ftype), "default"]:
        for act in _action_dict["depend"]:
            if intype in act.get_in_types():
                found_act = act
        if found_act:
            break

    # TODO: Should we check for the "default" outtype in the loop above?
    if found_act and "default" in found_act.get_out_types():
        return found_act

    return None


def find_out_ftype(recdict, actlist, action, in_ftype, out_ftype, msg):
    """
    Find the output filetype to be used for "action" and "in_ftype".
    Use only the actions in the list "actlist".
    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:
        # Find an action that supports "out_ftype".
        for act in actlist:
            if out_ftype in act.get_out_types():
                return out_ftype, 1

        # Find an action that supports the root of "out_ftype".
        root = filetype_root(out_ftype)
        for act in actlist:
            if root in act.get_out_types():
                return root, 1

    # Find an action that supports "default".
    for act in actlist:
        if "default" in act.get_out_types():
            if msg:
                msg_extra(recdict, _('Using default for %s %s from %s')
                                               % (action, out_ftype, in_ftype))
            return "default", 1

    return out_ftype, 0

# For pychecker
find_type = None


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

    # Find_type must be global for this to work in Python 1.5.
    global find_type
    find_type = in_ftype
    actlist = filter(lambda act: find_type in act.get_in_types(),
                                                          _action_dict[action])
    if actlist:
        use_out_ftype, found = find_out_ftype(recdict, actlist,
                                              action, in_ftype, out_ftype, msg)

    if not found:
        find_type = filetype_root(in_ftype)
        actlist = filter(lambda act: find_type in act.get_in_types(),
                                                          _action_dict[action])
        if actlist:
            use_in_ftype = find_type
            use_out_ftype, found = find_out_ftype(recdict, actlist,
                                             action, find_type, out_ftype, msg)

    if not found:
        actlist = filter(lambda act: "default" in act.get_in_types(),
                                                          _action_dict[action])
        if actlist:
            use_in_ftype = "default"
            use_out_ftype, found = find_out_ftype(recdict, actlist, 
                                          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 get_vars_from_attr(fromdict, todict, savedict = None):
    """
    Set variables from attributes:
    - Take all the entries in "fromdict" that start with "var_" and copy the
      value to "todict".
    - Take all the entries in "fromdict" that start with "add_" and append the
      value to a variable in "todict".
    If "savedict" is given, save the old value in it.
    """
    for k in fromdict.keys():
        if len(k) > 4 and (k[:4] == "var_" or k[:4] == "add_"):
            varname = k[4:]
            val = fromdict[k]
            if k[0] == 'a':
                oldval = get_var_val(0, todict, "_no", varname)
                if oldval:
                    # If the value already appears, don't append it again.
                    if string.find(' ' + oldval + ' ', ' ' + val + ' ') >= 0:
                        val = oldval
                    else:
                        val = oldval + ' ' + val
            if savedict:
                savedict[varname] = todict.get(varname)
            todict[varname] = val


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_note(recdict,
              _('Warning; Filetype not recognized for "%s", using "default"')
                                                             % args[1]["name"])
        in_ftype = "default"

    out_ftype = args[0].get("filetype")
    if not out_ftype:
        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 -> %s')
                                       % (action, use_in_ftype, use_out_ftype))
    act = find_action(action, use_in_ftype, use_out_ftype)
    if not act:
        msg_error(recdict, _("Internal error: action_run() didn't find action"))

    # Make a new scope for the the command block.
    new_recdict = get_build_recdict(recdict, act.buildrecdict,
                                 keep_current_scope = 1, rpstack = act.rpstack,
                                 xscope = args[0].get("scope"))

    # Take care of local and build command variables.

    # $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]["name"])

    # $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

    # If the action is to be deferred to another action, set $DEFER_ACTION_NAME
    # to the name of that action.
    new_recdict["DEFER_ACTION_NAME"] = act.defer_action_name(source = new_recdict["source"])

    # Turn the attributes on the action into variables.
    # Both the full name and "var_" names.
    # Skip the "scope" attribute, it's handled above.
    for k in args[0].keys():
        if len(k) <= 4 or (k[:4] != "var_" and k[:4] != "add_"
                                 and k not in ["scope", "filetype", "remove"]):
            new_recdict[k] = args[0][k]
    get_vars_from_attr(args[0], new_recdict)

    from Work import getwork

    # First use "var_" and "add_" attributes of the node.
    # Turn the "var_" and "add_" attributes of the sources into variables.
    for s in args[1:]:
        node = getwork(recdict).find_node(s["name"])
        if node:
            get_vars_from_attr(node.attributes, new_recdict)
        get_vars_from_attr(s, new_recdict)

    # Also use "var_" and "add_" attributes from the target.
    if new_recdict.has_key("target"):
        for d in str2dictlist([], new_recdict["target"]):
            get_vars_from_attr(d, new_recdict)

    # 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, 0)
    except UserError, e:
        return ((_('Error executing commands for %s %s: ')
                                            % (action, use_in_ftype)) + str(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:
                    # Use the buildcheck attribute of the Action if present,
                    # use the commands otherwise.
                    act = find_action(action, in_ftype, out_ftype)
                    cmd = act.attributes.get("buildcheck")
                    if not cmd:
                        cmd = act.commands
                    # Check if this action isn't used recursively.
                    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 et sts=4 tw=79 fo+=l:
