"""
Jython wrapper for invoking Ant tasks
"""
from org.apache.tools.ant import IntrospectionHelper, RuntimeConfigurable, BuildException, Target, Location, Project #@UnresolvedImport
from types import * #@UnusedWildImport
import inspect

ant_attr_map={"property":["name", "value"], "echo":["message"], "mkdir":["dir"]}

# internal variables
_target_function={}
_func_name_target={}
_target_counter=0

"""
Current Ant project. Initialized by jython Ant task
"""
project=None
ant=None

"""
This could be used in python 2.1 instead of dict
"""
def nested(**attrs):
    return attrs


class Ant:
    """Responsible for creating Ant tasks""" 
    
    
    def __init__(self, project):
        self.project=project
        self.proj=project
        self.antProject=project
        
    def __getattr__(self, method):
        def process_task(*args, **attrs):
            return self.exec_task(method, attrs, args)
        
        return process_task
            
    def exec_task( self, task_name, attrs, args=None):
        t=self.declare_task(task_name, attrs, args)
        try:
            t.execute()
        except BuildException, be:
            # This will get rid of Java stack trace
            #print be
            raise be.toString()

    def declare_task( self, task_name, attrs, args=None):
        # ant_ can be added to avoid conflicts
        name_prefix="ant_"
        if task_name.startswith(name_prefix):
            task_name=task_name[len(name_prefix):]
        
        if args:
            extra_attrs=self.__map_unnamed(task_name, args)
            attrs.update(extra_attrs)
            
        t=self.__create_task(task_name, attrs)
        return t
        

    def __map_unnamed(self, task_name, args):
        task_mapping=ant_attr_map.get(task_name.lower())
        if not task_mapping:
            raise "You provided unnamed parameters for task '%s'. \
We don't know how to map them to Ant attributes" % task_name
        if len(task_mapping)<len(args):
            raise "You provided %d unnamed parameters for task '%s'. \
But only %d of them are mapped to Ant attributes in PAnt" % (len(args), task_name, len(task_mapping))
        
        attrs={}
        i=0
        for arg in args:
            name=task_mapping[i]
            attrs[name]=arg
            i+=1
        return attrs
    
    def __create_task( self, task_name, attrs):

        t=self.project.createTask(task_name)
        if not t:
            raise "Ant project was not configured/created correctly, can't create task '%s'" % task_name
        conf=t.getRuntimeConfigurableWrapper()
        self.__configure_element(t, conf, attrs)
        t.maybeConfigure()

        return t

    def __create_nested_element(self, parent, elt_name, attrs):

        name=self.__convert_to_ant_name(elt_name)
        ih=IntrospectionHelper.getHelper(self.project, parent.getClass());
        # this will also invoke "add" method
        child = ih.createElement(self.project, parent, name)
        conf=RuntimeConfigurable(child, name)
        
        self.__configure_element(child, conf, attrs)
        conf.maybeConfigure(self.project)
        # invoke addConfigured method
        ih.storeElement(self.project, parent, child, name)

        return conf


    """
    In order to be able to specify a repetitive groups in a dictionary, 
    users can add suffixes to make each nested element unique, e.g., 
    env_1, env_2.
    PAnt gets rid of that suffix, incl. the underscore.
    """
    def __convert_to_ant_name( self, name ):
        antName=name
        underscoreI = name.rfind("_")
        if underscoreI>=0:
            antName=name[0:underscoreI]
        return antName


    def __configure_element(self, owner, conf, attrs):
        
        for attrName, attrVal in attrs.iteritems():
            if type(attrVal) is DictType:
                nConf=self.__create_nested_element(owner, attrName, attrVal)
                conf.addChild(nConf)
            elif type(attrVal) is ListType:
                for nested_elt in attrVal:
                    nConf=self.__create_nested_element(owner, attrName, nested_elt)
                    conf.addChild(nConf)
            else:
#                print "Setting attr %s=%s" % (attrName, attrVal) 
                conf.setAttribute(attrName, attrVal)


    """
    Helper method for converting python dict  to Ant properties
    """
    def dict_to_ant_props(self, dict):
        
        for propName, propVal in dict.iteritems():
            self.project.setNewProperty(propName, propVal)
            



"""
Represents Ant target, also handles creation of targets
Do not use directly, use antarget() or @target instead
"""
class AnTarget:
    def __init__(self, *arguments, **keywords ):
        if len(arguments)>0:
            if len(arguments)>1:
                raise "You can only pass a single argument to PTarget which is a dictionary"
            # assume dict was passed
            attrs=arguments[0] 
            if not type(attrs) is DictType:
                raise "You can only pass a dictionary with 'name', 'depends' and other attributes"
        else:
            attrs=keywords
        self.target=self.__create_target(attrs)

    def __create_target(self, attrs):
        """ Create the target with the project prefix (if avail) and 
        main target if needed. We replicate Ant logic here"""
        
        # determine name
        name=attrs.get("name")
        func=attrs.get("func")
        if not name:
            if not func:
                raise "You must provide a name or a function to define a target. You provided: "+str(attrs)
            name=func.func_name

        # determine target prefix
        # prefix could be supplied using "project_name" parameter 
        prefix=attrs.get("project_name")
        if prefix is None:
            # prefix supplied golobally to pimport
            prefix=_pimport_project_name
            
        if prefix is None and func:
            # use module name as prefix
            prefix=self.__get_module_name(func)
        
        if prefix:
            prefix=prefix.strip()
        
        # Do not register unless "" project name is supplied
        if project.getTargets().containsKey(name) and not(prefix and prefix==""):
            project.log("Already defined in main or a previous import, ignore "
                        + name, Project.MSG_VERBOSE);
        else:
            target=Target()
            target.setName(name)
            self.__init_target(target, attrs)
            project.addOrReplaceTarget(target.getName(), target)
        
        
        if prefix and prefix!="":
            target=Target()
            target.setName(prefix+"."+name)
            self.__init_target(target, attrs)
            project.addOrReplaceTarget(target.getName(), target)
        # register target w/o prefix if need be
        
    def __get_module_name(self, f):
        name=""
        m=inspect.getmodule(f)
        if m:
            name=m.__name__
        return name
    
    def __init_target(self, target, attrs):

        func=attrs.get("func")
        
        self.__set_location(target)
        
        depends=attrs.get("depends")
        if depends:
            if inspect.isfunction(depends):
                depends_target=_func_name_target.get(depends.func_name)
                if not depends_target:
                    raise "Function '%s' that you provided in 'depends' is not mapped to a target" % depends.func_name
            else:
                depends_target=depends
            target.setDepends(depends_target)

        val=attrs.get("description")
        if not val:
            val=attrs.get("desc")
        if not val and func:
            val=func.__doc__
        if val:
            target.setDescription(val)
        
        val=attrs.get("if_")
        if not val:
            val=attrs.get("if")
        if val:
            target.setIf(val)

        val=attrs.get("unless")
        if val:
            target.setUnless(val)
        func=attrs.get("func")
        code=attrs.get("code")
        if code and func:
            raise "You are not allowed to provide both 'func' and 'code'. You provided: "+str(attrs)
        
        if not code and not func:
            raise "You must provide 'func' or 'code'. You provided: "+str(attrs)
        if func:
            self.set_func(target, func)
        if code:
            self.set_code(target, code)

        
    def __set_location(self, target):
        """ Set fake location. Note that the location must be unique,
        otherwise Ant does not display targets in -projecthelp """
        global _target_counter
        location= Location("python", _target_counter, 0);
        _target_counter+=1
        target.setLocation(location);

    def set_code(self, target, code):
        self.__create_jython_task(target, code)
        
    def set_func(self, target, func):
        global _func_name_target, _target_function
        if not self.__isnoargfunction(func):
            raise "Function '%s' has arguments. Only functions without arguments can be used as targets" % func.func_name 
        
        func_key=target.getName()+"!"+func.func_name
        _target_function[func_key]=func
        calling_code="""
import pant.pant as pant
f=pant._target_function['%s']
f()
""" % func_key
        self.__create_jython_task(target, calling_code)
        # store so we can get to it later
        _func_name_target[func.func_name]=target.getName()

    def __create_jython_task(self, target, code):
        t=ant.declare_task("jython", {"exec":code} )
        target.addTask(t)
        
    def __isnoargfunction(self, f):
        """Returns True if the function does not have arguments"""
        args=inspect.getargspec(f)[0]
        return len(args)==0 or args[0]=="self"
    
        
    def __str__(self):
        if self.target:
            return "Target: name=%s description=%s" % (self.target.getName(),
                                                         self.target.getDescription())
        else:
            return None

_pimport_project_name=None
def antimport(module_name, project_name=None):
    """ Import python module into Ant. The module must use decorators to specify targets """
    global _pimport_project_name
    _pimport_project_name=None
    if project_name:
        _pimport_project_name=project_name
    __import__(module_name)
    _pimport_project_name=None



def antarget(func=None, **keywords):
    """Creates Ant target"""
    attrs=keywords.copy()
    if func:
        attrs["func"]=func
    project_name=attrs.get("project_name")
    if not project_name:
        attrs["project_name"]=""
    return AnTarget(attrs)

def target(*arguments, **keywords):
    """
    Allows to specify target using function decorator 
    """
    if len(arguments)>0:
        # if there are no arguments, python passes function to the decorator
        f=arguments[0] 
        if not inspect.isfunction(f):
            raise "You can't pass positional arguments to 'target' decorator"
        AnTarget(func=f)
        return f
    else:
        # if a decorator was provided with 
        # this function will be called at the decoration time
        def wrap_target(f):
            target_attrs=keywords.copy()
            target_attrs["func"]=f
            AnTarget(target_attrs)
            return f
        return wrap_target

        