#
# 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 the License "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: 
# Classes, methods and regex available for use in log filters
#


import re


# General log structure
logTag = re.compile('</?(?P<name>\?xml|buildlog|info|warning|error|recipe|whatlog|build|export|archive|member|bitmap|resource|stringtable|bmconvcmdfile)[>| ]')
logHeader = re.compile('<buildlog sbs_version=[\'|\"](?P<version>.+)[\'|\"] xmlns=[\'|\"](?P<xmlns>.+)[\'|\"] xmlns:xsi=[\'|\"](?P<xsdi>.+)[\'|\"] xsi:schemaLocation=[\'|\"](?P<schemaLocation>.+)[\'|\"]>')
clean = re.compile('.*<rm(dir)? (files|dirs)=[\'|\"](?P<removals>.+)[\'|\"] />')
exports = re.compile('<info>(Copied|Unzipped (?P<unpacked>\d+) files from) (?P<source>.+) to (?P<destination>.+)</info>')

# Tool errors and warnings
mwError = re.compile('(.+:\d+:(?! (note|warning):) .+|mw(ld|cc)sym2(.exe)?:(?! (note|warning):) .+ \'.+\' .+)')
mwWarning = re.compile('.+:\d+: warning: .+|mw(ld|cc)sym2(.exe)?: warning: .+')


class AutoFlushedStream(file):
	""" Wrapper for STDOUT/STDERR streams to ensure that a flush is performed
	after write methods.
	Use to avoid buffering when log output in real time is required."""
	
	def __init__(self, aStream):
		self.__stream = aStream
    
	def write(self, aText):
		self.__stream.write(aText)
		self.__stream.flush()

	def writelines(self, aTextList):
		self.__stream.writelines(aTextList)
		self.__stream.flush()


class RecipeFactory(object):
	"Factory class to ease creation of appropriately specialised Recipe objects."
	
	def newRecipe(self, aLine=None, aCustomIgnore=None):
		""" Creates objects of base type Recipe depending on the name
		of the recipe being processed."""
		
		name = ""
		header = None
		if aLine:
			header = Recipe.header.match(aLine)
		if header:
			name = header.group("name")	
		
		if name.startswith("win32"):
			return Win32Recipe(aLine, aCustomIgnore)
		else:
			return Recipe(aLine, aCustomIgnore)
	

class Recipe(object):
	""" Recipe base class.
	Provides a means to get hold of recipe content in a generic way.
	Includes a basic understanding of errors and warnings - sub-classes can
	override output, error and warning methods to specialise."""
	
	# Flags to normalise client access, mapping directly to regex groups
	name		= "name"
	target		= "target"
	host		= "host"
	layer		= "layer"
	component	= "component"
	bldinf		= "bldinf"
	mmp			= "mmp"
	config		= "config"
	platform	= "platform"
	phase		= "phase"
	source		= "source"
	start		= "start"
	elapsed		= "elapsed"
	exit		= "exit"
	code		= "code"
	attempts	= "attempts"
	
	# Basic errors/warnings
	error = re.compile('Error: ')
	warning = re.compile('Warning: ')
	
	# Recipe metadata
	header  = re.compile('<recipe\s+name=[\'|\"](?P<name>.+)[\'|\"]\s+target=[\'|\"](?P<target>.+)[\'|\"]\s+host=[\'|\"](?P<host>.+)[\'|\"]\s+layer=[\'|\"](?P<layer>.*)[\'|\"]\s+component=[\'|\"](?P<component>.*)[\'|\"]\s+bldinf=[\'|\"](?P<bldinf>.+)[\'|\"]\s+mmp=[\'|\"](?P<mmp>.*)[\'|\"]\s+config=[\'|\"](?P<config>.+)[\'|\"]\s+platform=[\'|\"](?P<platform>.*)[\'|\"]\s+phase=[\'|\"](?P<phase>.+)[\'|\"]\s+source=[\'|\"](?P<source>.*)[\'|\"]\s*>')
	call    = re.compile('^\+ (?P<call>.+)$')
	status  = re.compile('\<status\s+exit=[\'|\"](?P<exit>(ok|failed|retry))[\'|\"](\s+code=[\'|\"](?P<code>\d+)[\'|\"])?\s+attempt=[\'|\"](?P<attempts>\d+)[\'|\"]\s*\/>')
	ignore  = re.compile('<!\[CDATA\[')
	time    = re.compile(']]><time\s+start=[\'|\"](?P<start>\d+\.\d+)[\'|\"]\s+elapsed=[\'|\"](?P<elapsed>\d+.\d+)[\'|\"]\s*/>$')
	footer  = re.compile('</recipe>$')
	
	
	def __init__(self, aLine=None, aCustomIgnore=None):
		"""
		@param aLine			Optional first line of a recipe (typically the recipe header)
		@param aCustomIgnore	Optional compiled regular expression object listing additional
								lines to be ignored in this recipe's output.
		"""				
		self.__customIgnore = aCustomIgnore	
		
		self.__detail = {
						Recipe.name		:"",
						Recipe.target	:"",
						Recipe.host		:"",
						Recipe.layer	:"",
						Recipe.component:"",
						Recipe.bldinf	:"",
						Recipe.mmp		:"",
						Recipe.config	:"",
						Recipe.platform	:"",
						Recipe.phase	:"",
						Recipe.source	:"",
						Recipe.start	:"",
						Recipe.elapsed	:0.0,
						Recipe.exit		:"",
						Recipe.code		:0,
						Recipe.attempts	:0
						}
		
		self.__calls = []
		self.__lines = []
		self.__complete = False
		
		if aLine:
			self.addLine(aLine)
	
	def isComplete(self):
		"""Signifies that the recipe footer has been reached, the
		recipe is complete and so is in a fit state to be queried."""
		return self.__complete

	def __storeDetail(self, aMatchObject):
		for key in aMatchObject.groupdict().keys():
			value = aMatchObject.group(key)
			if value:
				if (key in [Recipe.code,Recipe.attempts]):
					value = int(value)
				elif key == Recipe.elapsed:
					value = float(value)
				self.__detail[key] = value
	
	def addLine(self, aLine):
		"""Add a log line to an existing recipe object, processing anything
		that can be examined at this point in time directly."""
		if Recipe.ignore.match(aLine) or (self.__customIgnore and self.__customIgnore.match(aLine)):
			return

		header = Recipe.header.match(aLine)
		if header:
			self.__storeDetail(header)
			return
		
		call = Recipe.call.match(aLine)
		if call:
			self.__calls.append(call.group("call"))
			return
		
		time = Recipe.time.match(aLine)
		if time:
			self.__storeDetail(time)
			return
		
		status = Recipe.status.match(aLine)
		if status:
			self.__storeDetail(status)
			return
		
		if Recipe.footer.match(aLine):
			self.__complete = True
			return

		self.__lines.append(aLine)
	
	def getDetail(self, aItem):
		"""Retrieve attribute detail from recipe tags.
		Class data flags provide known items e.g. getDetail(Recipe.source)"""
		if self.__detail.has_key(aItem):
			return self.__detail[aItem]
		
	def getCalls(self):
		"Return a list of all '+' prefixed tool calls from this recipe."
		return self.__calls
	
	def isError(self, aLine):
		"""Convenience matcher for basic errors.
		Override in sub-classes to specialise."""
		if Recipe.error.match(aLine):
			return True
		return False
	
	def isWarning(self, aLine):
		"""Convenience matcher for basic warnings.
		Override in sub-classes to specialise."""
		if Recipe.warning.match(aLine):
			return True
		return False
	
	def getOutput(self):
		""""Return a list of all output that isn't an error or a warning.
		Override in sub-classes to specialise."""
		output = []
		for line in self.__lines:
			if not self.isError(line) and not self.isWarning(line):
				output.append(line)
		return output
	
	def getErrors(self):
		""""Return a list of all output identified as an error.
		Override in sub-classes to specialise."""
		errors = []
		for line in self.__lines:
			if self.isError(line):
				errors.append(line)
		return errors
	
	def getWarnings(self):
		""""Return a list of all output identified as a warning.
		Override in sub-classes to specialise."""
		warnings = []
		for line in self.__lines:
			if self.isWarning(line):
				warnings.append(line)
		return warnings
	
	def isSuccess(self):
		"Convenience method to get overall recipe status."
		return (self.getDetail(Recipe.exit) == "ok")
	
	
class Win32Recipe(Recipe):
	"Win32 tailored recipe class."
	def isError(self, aLine):
		if mwError.match(aLine):
			return True
		return False
	
	def isWarning(self, aLine):
		if mwWarning.match(aLine):
			return True
		return False
