#!/usr/bin/env python

# This scripts attempts to replace deprecated symbols by their new equivalents based on compiler's output log. Symbol map must be
# given explicitly (see renamedSymbols below).
#
# The only command-line argument is yade's buildRoot (trunk) directory. This script further on the fact that all filenames reported
# in compiler's warnings are relative to the buildRoot. Therefore, you better use scons to compile, since default Makefile are recursive.
#
# Build log (like "scons >build.log 2>&1") is piped from standard input. It MUST be strictly sequential, i.e. no -j2 etc.
#
# You may have to adjust the runCxxPattern below depending on the compiler you use. It must distinguish a compiler invocation.
runCxxPattern=r'^((c|g|so)\+\+|ccache|distcc).*$'
#
# Likewise for deprecatedPattern. It must have the following groups: file, line, symbol.
# This one works for g++4.0 and possibly other compilers.
deprecatedPattern=r"^(.*?):([0-9]+): warning: '.*<[a-zA-Z]*>::([a-zA-Z0-9_]+)(\(.*'|') is deprecated.*$"
#
# All files modified are backed up to their directory with some extension like .~undeprecated~.
#


import sys
from re import *
import logging
from os.path import join
import os.path
import shutil
from pprint import pprint

logging.basicConfig(level=logging.DEBUG)

#
# these symbols are class methods. Several of them are the same (like Vector{2,3}.x()), but they have been
# fortunately renamed the same for all classes. So we don't need to distinguish that.
#
# the dictionary was generated by hackett2.py from tags files
#
renamedSymbols={
	'ZERO':'Zero()',
	'UNIT_X':'UnitX()',
	'UNIT_Y':'UnitY()',
	'UNIT_Z':'UnitZ()',
	'ONE':'Ones()',
	'W':'w',
	'X':'x',
	'Y':'y',
	'Z':'z',
	'Cross':'cross',
	'Dot':'dot',
	'Conjugate':'conjugate',
	'Inverse':'inverse',
	'Determinant':'determinant',
	'Transpose':'transpose',
	'Length':'norm',
	'SquaredLength':'squaredNorm',
	'Align':'setFromTwoVectors',
	}

# places where deprecated methods are used
# keys are symbols, value is a list of (file,line,count)
# this is the global dictionary
D={}
# dtto, but for one individual compiler run; changes are merged to D afterwards
D1={}
# count of deprecation warnings, only for the heck of informing the user of what is going on
Dcount=0


# Symbols at different lines than the compiler reports.
# Maps (symbol,file,line_as_reported_by_compiler) -> real_line.
# We are not intelligent enough to do fuzzy match on line number.
Doffsets={}
#	{('sqRoot','include/yade/yade-lib-wm3-math/Quaternion.ipp',459):455,
#	('sqRoot','include/yade/yade-lib-wm3-math/Vector3.ipp',270):267,
#	('x','include/yade/yade-lib-serialization/KnownFundamentalsHandler.tpp',187):188,
#	('y','include/yade/yade-lib-serialization/KnownFundamentalsHandler.tpp',187):190,
#	('z','include/yade/yade-lib-serialization/KnownFundamentalsHandler.tpp',187):192,
#	('sqRoot','include/yade/yade-lib-wm3-math/Vector2.ipp',243):241}

unknownDeprec=set()

# parse the build log as fed by stdin
for l in sys.stdin.xreadlines():
	if match(runCxxPattern,l): #initialization, i.e. the compiler being run; until the next compiler run or EOF, all messages belong to this compilation.
		# merge dictonary from the previous compiler run
		for s in D1.keys():
			if not s in D.keys(): # new symbol; simply add
				D[s]=D1[s]
				continue
			# OK, now s is in D already
			for ss1 in D1[s]: # iterate over locations for symbol s from this compilation
				done=False # tmp to break the outer loop from the nested one
				for ss in D[s]: # iterate over locations from all previous compilations
					if ss[0]!=ss1[0]: continue # file doesn't match, doesn't belong here
					if ss[1]!=ss1[1]: continue # line doesn't match, dtto
					# here, symbol, file, and line match. Make sure that count also matches. It should.
					if ss[2]==ss1[2]: # this is OK. Last compilation all report that symbol the same number of times for that line
						done=True
					else:
						logging.error("%s:%d: Deprecations warning number doesn't match for `%s'"%(f,line,s))
						# since it is a fatal logic error, bail out
						#assert(1==0)
				if done: continue
				D[s].append(ss1) # warning not yet encountered at this place exactly
		# now hashes have been merged
		D1={}
		continue # this input line is not further processed
	# deprecation warning from the compiler
	dep=match(deprecatedPattern,l)
	if dep:
		f=dep.group(1)
		line=int(dep.group(2))
		symbol=dep.group(3)
		if 'yade/lib-miniWm3/' in f: continue # ignore warnings in wwm3 headers themselves
		# this indicates inconsistency of what symbols are marked as deprecated and what we think these are
		if not symbol in renamedSymbols.keys():
			#logging.error("%s:%d: Unknown symbol `%s' reported as deprecated."%(f,line,symbol))
			unknownDeprec.add(symbol)
			continue
		Dcount+=1
		# handle cases where compiler reports wrong line number: adjust the line number
		if (symbol,f,line) in Doffsets.keys(): line=Doffsets[(symbol,f,line)]
		if not symbol in D1.keys(): # first warning emitted for this symbol in this file
			D1[symbol]=[[f,line,1]] 
			continue
		done=False
		# iterate over all warnings about this symbols we've had so far in this run
		for ww in D1[symbol]:
			if ww[0]!=f: continue
			if ww[1]!=line: continue
			# only care if both filename and linenumber match: increment count
			ww[2]+=1
			done=True
		# otherwise create a new record
		if not done: D1[symbol].append([f,line,1])

logging.info('Total number of %d deprecation warnings processed.'%Dcount)
logging.info('These symbols were reported deprecated but are not known: %s'%(', '.join(unknownDeprec)))

#############################################################################################################################
## now go ahead and modify sources... :-( scared? huh, there will be many safety checks on the way, don't worry...
##

buildRoot=sys.argv[1]
# reoarganize D, so that it is hashed by (file,line) and contains (symbol,count)
F={}
Fcount=0
for symbol in D.keys():
	for ss in D[symbol]:
		file,line,count=ss
		if not F.has_key((file,line)):
			F[file,line]=[(symbol,count)]
			Fcount+=count
		else:
			for sss in F[file,line]: # can't have the same symbol twice...
				assert(sss!=(symbol,count))
			F[file,line].append((symbol,count))
			Fcount+=count

logging.info('Merged into the total of %d usages of deprecated functions'%Fcount)

# files that we will have to modify
fileList=[]
for f,l in F.keys():
	if not f in fileList: fileList.append(f)
logging.info('%d files to be modified'%len(fileList))

for fName in fileList:
	origFile=join(buildRoot,fName)
	origFile=os.path.realpath(origFile)
	bcupFile=origFile+'.~undeprecated~'
	logging.info("Modifying file `%s' (%s); backup is `%s'."%(origFile,fName,bcupFile))
	# the first branch is normal run. The second one is a dry-run, where original files are not modified
	if 0:
		shutil.move(origFile,bcupFile)
		fout=open(origFile,'w')
		fin=open(bcupFile)
	else:
		fin=open(origFile)
		fout=open(bcupFile,'w')
	lineno=0
	for l in fin.xreadlines():
		lineno+=1
		if F.has_key((fName,lineno)):
			for symbol,count in F[fName,lineno]: # this has list of symbols and their counts on this particular line
				l2,subs=subn(r'\b'+symbol+r'\b',renamedSymbols[symbol],l) # try textual replace
				# make sure we have found exactly the same number of matches compiler told us about!
				# this is not the case if the expressions spans several lines or if there are local symbols with the same name as the deprecated method
				# some cases have been already handled by the Doffset custom dictionary above
				#if subs!=count: 
				#	logging.info("%s:%d: Regexp found different number (%d) of `%s' than the compiler (%d).") # Fix by hand (replace by `%s'), please."%(fName,lineno,subs,symbol,count,renamedSymbols[symbol]))
					#l=l2
				#else:
				logging.info('%s:%d: %s -> %s %s'%(fName,lineno,symbol,renamedSymbols[symbol],'count mismatch %d!=%d'%(subs,count) if subs!=count else ''))
				l=l2
		fout.write(l)









