#
# Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies).
# All rights reserved.
# This component and the accompanying materials are made available
# under the terms of "Eclipse Public License v1.0"
# which accompanies this distribution, and is available
# at the URL "http://www.eclipse.org/legal/epl-v10.html".
#
# Initial Contributors:
# Nokia Corporation - initial contribution.
#
# Contributors:
#
# Description:
#
## 
# @author <author>
'''
ConE plugin to run external applications/tools with given parameters in .commandml file. Notice that values can be also
fecthed from ConfML to maximize portability and minimize maintenance.
'''

import re
import os
import logging
import types
import pkg_resources

import subprocess

from cone.public import plugin,utils

def get_folder_set(folder):
    """
    Get a set object containing all files of given folder
    @param folder: the folder to create set for
    @return: a python set 
    """
    fileset = set()
    for (root, _, filenames) in os.walk(folder):
        for filename in filenames:
            fname = utils.relpath(os.path.join(root,filename), folder)
            fileset.add(fname)
    
    return fileset

class CommandImpl(plugin.ImplBase):
    """
    Plugin implementation class. 
    """
    
    IMPL_TYPE_ID = "commandml"
    
    
    def __init__(self,ref,configuration, reader):
        """
        Overloading the default constructor
        """
        plugin.ImplBase.__init__(self,ref,configuration)###3
        self.desc = ""
        self.logger = logging.getLogger('cone.commandml(%s)' % self.ref)
        self.reader = reader
        for element in self.reader.elements:
            element.set_logger(self.logger)
        

    def generate(self, context=None):
        """
        Generate the given implementation.
        """
        
        self.create_output(context)
        return 
    
    def generate_layers(self,layers):
        """
        Generate the given Configuration layers.
        """
        self.logger.info('Generating layers %s' % layers)
        self.create_output(layers)
        return 
    
    def create_output(self, context, layers=None):
        """
        Function to generate output files.
        """
        self.context = context
        tmpDict = self.__create_helper_variables()
        # Get the contents of output folder before the generation
        outset_before = get_folder_set(context.output)
        for element in self.reader.elements:
            #Element can be either command or condition.
            element.set_logger(self.logger)
            element.execute(context, tmpDict)        
        
        # Get the contents of output folder after the generation 
        # and get the new files created by the set difference.
        # NOTE! this does not recognize files outside output folder!
        outset_after = get_folder_set(context.output)
        outset = outset_after - outset_before
        for outfile in outset:
            context.add_file(outfile, implementation=self)
        return

    def __create_helper_variables(self):
        """
        Internal function to create dictionary containing most often used ConE "environment" variables.
        """        
        tmp = {}
        tmp["%CONE_OUT%"] = os.path.join(self.context.output, self.output).rstrip('\\')
        tmp["%CONE_OUT_ABSOLUTE%"] = os.path.abspath(os.path.join(self.context.output, self.output)).rstrip('\\')
        return tmp    
    
    def has_ref(self, refs):
        """
        @returns True if the implementation uses the given ref as input value.
        Otherwise return False.
        """
                
        # return true for now so that content copying is not filtered 
        return None
    
class CommandImplReader(plugin.ReaderBase):
    """
    Parses a single commandml file
    """ 
    NAMESPACE = 'http://www.s60.com/xml/commandml/1'
    NAMESPACE_ID = 'commandml'
    ROOT_ELEMENT_NAME = 'commandml'
    FILE_EXTENSIONS = ['commandml']
    
    def __init__(self):
        """
        Constructor
        """
        self.output_dir = None
        self.input_dir = None
        self.namespaces = [self.NAMESPACE]
        self.dview = None
        self.elements = []
        self.tags = None
    
    @classmethod
    def read_impl(cls, resource_ref, configuration, etree):
        reader = CommandImplReader()
        reader.set_default_view(configuration.get_default_view())
        reader.from_etree(etree)
        impl = CommandImpl(resource_ref, configuration, reader)
        if reader.tags:
            impl.set_tags(reader.tags)
        return impl
    
    @classmethod
    def get_schema_data(cls):
        return pkg_resources.resource_string('commandplugin', 'xsd/commandml.xsd')
    
    def set_default_view(self, dview):
        """
        Function to set default view that is needed when solving out ConfML reference information
        """        
        self.dview = dview
            
    def from_etree(self, etree):
        """
        Parser function for commandml element.
        """
        self.parse_tree(etree)

    def parse_tree(self, etree):
        """
        General parse function for condition and command elements.
        """        
        elements = list(etree)
        for element in elements:
            if element.tag == "{%s}condition" % self.namespaces[0]:
                self.elements.append(self.parse_condition(element))
            elif element.tag == "{%s}command" % self.namespaces[0]:
                self.elements.append(self.parse_command(element))
            else:
                pass
        self.tags = self.parse_tags(etree)

    def parse_condition(self, etree):
        """
        Parse function for condition element.
        """        
        condition = Condition()
        condition.set_condition(etree.get("value"))
        condition.set_commands(self.parse_commands(etree))
        condition.set_default_view(self.dview)
        return condition                                   
        
    def parse_commands(self,etree):
        """
        Parser function for commands.
        """
        commands = []
        for com_elem in etree.findall("{%s}command" % self.namespaces[0]):
            commands.append(self.parse_command(com_elem))
        return commands
         
    def parse_command(self,etree):
        """
        Parser function for single command.
        """
        cmd = Command()
        cmd.set_executable(etree.get("executable"))
        cmd.set_shell(etree.get("shell"))
        cmd.set_bufsize(etree.get("bufsize"))
        cmd.set_cwd(etree.get("cwd")) 
        cmd.set_all_envs(etree.get("env"))
        cmd.set_all_arguments(self.parse_arguments(etree))
        cmd.set_all_pipes(self.parse_pipes(etree))
        cmd.set_filters(self.parse_filters(etree))
        cmd.set_default_view(self.dview)
        return cmd
    
    def parse_arguments(self,etree):
        """
        Parser function for command's arguments.
        """
        arguments = []
        for argument in etree.findall("{%s}argument" % self.namespaces[0]):
            value = argument.get("value")
            if value:
                arguments.append(value)
        return arguments
    
    def parse_pipes(self,etree):
        """
        Parser function for command's pipes.
        """
        pipes = {}
        for argument in etree.findall("{%s}pipe" % self.namespaces[0]):
            name = argument.get("name")
            value = argument.get("value")
            if name:
                pipes[name] = value        
        return pipes

    def parse_filters(self,etree):
        """
        Parser function for command's filters.
        """
        filters = []
        for argument in etree.findall("{%s}filter" % self.namespaces[0]):
            f = Filter()
            f.set_severity(argument.get("severity"))
            f.set_condition(argument.get("condition"))
            f.set_input(argument.get("input"))
            f.set_formatter(argument.get("formatter"))
            filters.append(f)
        return filters
    
    def parse_tags(self,etree):
        tags = {}
        for tag in etree.getiterator("{%s}tag" % self.namespaces[0]):
            tagname = tag.get('name','')
            tagvalue = tag.get('value')
            values = tags.get(tagname,[])
            values.append(tagvalue)
            tags[tagname] = values
        return tags


class Condition(object):
    """
    Condition class is a simple wrapper class for commands so that commands are executed
    only if condition is True. Otherwise class does nothing. Class has similar interface 
    than Command class so that they can be used similar way from plugin perspective. 
    """
    
    def __init__(self):
        self.condition = None
        self.commands = []
        self.logger = None
        self.dview = None

    def set_condition(self, condition):
        self.condition = condition

    def set_default_view(self, dview):
        self.dview = dview

    def set_commands(self, commands):
        self.commands = commands

    def set_logger(self, logger):
        self.logger = logger
        for cmd in self.commands:
            cmd.set_logger(logger)        

    def add_command(self, command):
        self.commands.append(command)

    def execute(self, context, replaceDict=None):
        if self._solve_condition(self.condition, context):
            #Condition is true -> running command
            for command in self.commands:                
                command.execute(context, replaceDict)
        else:
            self.logger.info("Ignoring %s because it is evaluated as False." % self.condition)

    def _solve_condition(self, condition_str, context):
        """
        Internal function to handle condition
        """
        if condition_str != "":
            #Expanding ConfML information
            modstr = utils.expand_delimited_tokens(
                condition_str,
                lambda ref, index: repr(context.configuration.get_default_view().get_feature(ref).get_value()))
            return eval(modstr)
        else:
            #Empty condition is true always.
            return True

class Command(object):
    """
    Command is a class that executes actual commands. It provides ways to handle input, output and error 
    streams and to control execution parameters.
    """
        
    def __init__(self):
        """
        Constructor
        """        
        self.executable = None
        self.shell = False
        self.bufsize = 0
        self.cwd = None
        self.envs = None
        self.arguments = []
        self.pipes = {}
        self.streams = {}
        self.filters = []
        self.logger = None
        self.dview = None
    
    def set_executable(self, executable):
        self.executable = executable
    
    def set_shell(self, shell):
        if shell and shell.lower() in ('true', 'yes', '1', 1, True):
            self.shell = True
        else:
            self.shell = False
        
    def set_bufsize(self, bufsize):
        if bufsize:
            self.bufsize = int(bufsize)
    
    def set_cwd(self, cwd):
        self.cwd = cwd
    
    def set_all_envs(self, envs):
        self.envs = envs
    def set_default_view(self, dview):
        self.dview = dview
        
    def set_env(self, name, value):
        self.envs[name] = value
    
    def set_all_arguments(self, args):
        self.arguments = args
    
    def get_arguments_string(self):
        """
        Function to return arguments as a string
        """
        arg_string = ""        
        for value in self.arguments:            
            arg_string += value
            arg_string += " "                
        return arg_string
    
    def get_pipe(self, name, mode='w'):
        """
        Function to return pipe based on the pipe name in requested mode.
        """
        if self.pipes.has_key(name) and isinstance(self.pipes[name], types.IntType):
        #Subprocess pipe
            return self.pipes[name]
        elif self.pipes.has_key(name) and isinstance(self.pipes[name], types.StringType):            
            return file(self.pipes[name], mode)
        else:
            return None
    
    def set_streams(self, stdin, stdout, stderr):
        self.streams["stdin"] = stdin
        self.streams["stdout"] = stdout
        self.streams["stderr"] = stderr
                        
    def get_streams(self, name, mode="r"):
        if self.streams.has_key(name) and self.streams[name]:
        #OK for streams set with subprocess.PIPE
            return self.streams[name]
        else:
        #For file objects
            return self.get_pipe(name, mode)
    
    def set_filters(self, filters):
        self.filters = filters
        for f in self.filters:
            f.set_command(self)
    
    def get_filters(self):
        return self.filters
        
    def set_argument(self, value):
        self.arguments.append(value)
    
    def set_all_pipes(self, pipes):
        for pipe in pipes.keys():
            self.set_pipe(pipe, pipes[pipe])
        
    def set_pipe(self, name, value):
        if value == "PIPE":
            #Creating new stream for this.
            self.pipes[name] = subprocess.PIPE
        elif value == "STDOUT":
            self.pipes[name] = subprocess.STDOUT
        else:
            #Setting filename
            self.pipes[name] = value
            #self.pipes[name] = file(value, 'w')
            
    def handle_filters(self):
        """
        """
        for filter in self.filters:
            filter.report(self.logger)

    def execute(self, context, replaceDict=None):
        self.dview = context.configuration.get_default_view()
        
        self.solve_refs()
        
        try:
            if self.envs:   env_dict = eval(self.envs)
            else:           env_dict = None
        except Exception, e:
            raise RuntimeError("Failed to evaluate env dictionary: %s: %s" % (e.__class__.__name__, e))
        
        exit_code = 0
        try:
            try:
                if self.cwd is not None:
                    cwd = self.__replace_helper_variables(self.cwd, replaceDict)
                else:
                    cwd = self.cwd
                command_str = self.executable + " " + self.__replace_helper_variables(self.get_arguments_string(), replaceDict)
                self.logger.info("Running command: \"%s\"" % command_str)
                self.logger.info("with args: shell=%s envs=%s cwd=%s bufsize=%s stdin=%s stdout=%s stderr=%s" \
                                 % (self.shell, self.envs, cwd, self.bufsize, \
                                    self.get_pipe("stdin", 'r'),self.get_pipe("stdout"), self.get_pipe("stderr")))
                pid = subprocess.Popen(command_str, shell=self.shell, env=env_dict, cwd=cwd,\
                                          bufsize=self.bufsize, stdin = self.get_pipe("stdin", 'r'),\
                                          stdout = self.get_pipe("stdout"), stderr = self.get_pipe("stderr"))
                #Waiting for process to complete
                retcode = pid.wait()
                #Storing stream information for possible further processing.
                self.set_streams(pid.stdin, pid.stdout, pid.stderr)
                
                if retcode < 0:
                    self.logger.error("Child was terminated by signal %s" % (-retcode))
                else:
                    self.logger.info("Child returned: %s" % retcode)
            except OSError, e:
                self.logger.error("Execution failed: %s", repr(e))            
            self.handle_filters()
        except Exception,e:
            utils.log_exception(self.logger, "Failed to execute command: %s" % e)

    def set_logger(self, logger):
        self.logger = logger        

    def __replace_helper_variables(self, inputstr, dictionary):
        retstr = inputstr
        for key in dictionary.keys():
            retstr = retstr.replace(key, dictionary[key])            
        return retstr

    def solve_refs(self):
        """
        Function to solve references just before generation.
        """
        
        self.executable = self.__solve_ref(self.executable)
        self.shell = self.__solve_ref(self.shell)
        self.bufsize = self.__solve_ref(self.bufsize)
        self.cwd = self.__solve_ref(self.cwd)
        self.envs = self.__solve_ref(self.envs)
        for argument in self.arguments:
            self.arguments[self.arguments.index(argument)] = self.__solve_ref(argument) 
        for pipe in self.pipes.keys():
            self.pipes[pipe] = self.__solve_ref(self.pipes[pipe])

    def __solve_ref(self, inputstr):
        """
        Internal function to solve whether input is ref or just normal input string. 
        For refs actual ConfML value is resolved and returned. Non-refs are returned 
        as such.
        """        
        if inputstr and isinstance(inputstr, types.StringType):
            return utils.expand_refs_by_default_view(inputstr, self.dview)
        else:
            return inputstr


        
class Filter(object):
    """
    Filter class handles printing information to ConE log using filtering information.
    Filtering severity, condition and the format of output can be configured in command ml. 
    """

    def __init__(self):
        self.severity = None
        self.condition = None
        self.input = None
        self.command = None
        self.formatter = None
        
    def set_severity(self, severity):
        self.severity = severity

    def set_condition(self, condition):
        self.condition = condition

    def set_input(self, input):
        self.input = input

    def set_command(self, command):
        self.command = command

    def set_formatter(self, formatter):
        self.formatter = formatter
        
    def report(self, logger):
        input_pipe = self.command.get_streams(self.input)
        if isinstance(input_pipe, types.FileType):
            #Subprocess.PIPE and file descriptors supported only.
            data = input_pipe.read()
            pattern = re.compile(self.condition)
            for line in data.splitlines():
                mo = pattern.match(line)
                if mo:
                    lf = self.__get_logger_function(logger)
                    if self.formatter:                        
                        lf(self.formatter % mo.groupdict())
                    else:
                        lf(line)

    def __get_logger_function(self, logger):
        if self.severity == "info":
            return logger.info
        elif self.severity == "warning":
            return logger.warning
        elif self.severity == "debug":
            return logger.debug
        elif self.severity == "exception":
            return logger.exception
        elif self.severity == "error":
            return logger.error
        elif self.severity == "critical":
            return logger.critical
        else:
            #Default
            return logger.info

        






