#!/usr/bin/env python
# -*- Encoding: utf-8 -*-

"""
This program should really be run by the erskine3-apply.sh script

Erskine is Watt's colleague in the house of Mr. Nutting.

"""
from __future__ import print_function

from builtins import filter
from builtins import map
from builtins import range
import sys,pprint,string,os
from logging import *
from os.path import *
from re import *
from copy import deepcopy
import pprint

def warning2(msg): warning(currentPro+' '+msg)

bgroup=('(','{','[')
egroup=(')','}',']')
# variables that do not propagate to sub-builds
localVars=['TEMPLATE','SUBDIRS','HEADERS','SOURCES','FORMS','LEXSOURCES','YACCSOURCES','TARGET','DESTDIR','PROJECT']
# variables / function that are ignored if undefined (replaced by ''); for all other, a warning is printed (and are ignored anyway)
undefOK=['CXXPATH','CXX','CXXFLAGS','INCPATH','IDL_COMPILER']
# internal qmake variables that may be replace with the environment ones without warning
replaceOK=['YADECOMPILATIONPATH'] 
discardedStatements=['^isEmpty\s*\(\s*YADE_QMAKE_PATH\s*\).*$']
#discardedStatements=[]

targetLangType={'yade-lib-serialization-qt':'qt3','QtGUI':'qt3'}
# used to be yadeQt, but it was dropped...
targetEnv={'yade-lib-serialization-qt':'env','QtGUI':'env'}
# was: yade
defaultEnv='env'

currentPro='[none]'

allEnvs=[defaultEnv]
for t in list(targetEnv.keys()):
	if not targetEnv[t] in allEnvs: allEnvs.append(targetEnv[t])
allSystemLibs=['glut','boost_date_time','boost_filesystem','boost_thread']


def splitgroup(s,delims=' '):
	"Split string at delims, but do not break parenthesized/bracketed/curly-bracketed groups"
	nest=0;ret=[];lastsplit=0
	for i in range(0,len(s)):
		if s[i] in bgroup: nest+=1
		elif s[i] in egroup: nest-=1
		elif (nest==0 and s[i] in delims):
			ret.append(s[lastsplit:i])
			lastsplit=i+1
	ret.append(s[lastsplit:len(s)])
	return ret

def parseProject(proFile,inheritedVariables={}):
	"""Returns dictionary of variables defined in this qmake project, replacing variables, that may propagate from parent project.
	No functions, only a few conditionals are expanded.
	
	An important 'parser' difference is that qmake obviously allows \\ continuation _after_ #.
	Since it is not nice at all, it doesn't work here. Also, it would be difficult to add."""

	vars=inheritedVariables

	# sanitize input: no comments, no \\ linebreaks (join lines), no empty lines, no leading/trailing whitespace
	qm=[]; l=''; nest=0
	for line in proFile:
		m=match(r'^\s*([^#]*)(#.*)?$',line[:-1])
		l+=m.group(1)
		l=l.rstrip()
		beginnest=nest
		if len(l)==0: continue
		for c in l:
			if c in bgroup:nest+=1
			elif c in egroup:nest-=1
		if l[-1]!='\\' and nest==0:
			qm.append(l)
			l=''
		else:
			if l[-1]=='\\': l=l[:-1]
			nest=beginnest
	#pprint.pprint(qm)

	# process statements sequentially, replace variables / functions
	for stat in qm:
		# discard what should be discarded
		discard=False
		for pat in discardedStatements:
			if match(pat,stat):
				discard=True; break
		if discard: continue
		#replace environment and qmake variables
		while True:
			m=search(r'\$\$?([a-zA-Z0-9_]+\b|\([a-zA-Z0-9_]+\)|\{[a-zA-Z0-9_]+\})',stat) # possible patters of variables
			if not m: break

			v=m.group(1)
			if v[0]=='{':
				v=v[1:-1]
				if v in os.environ:
					repl=os.environ[v]
					info("Substituting environment variable `%s` → `%s`"%(v,repl))
				else:
					if not v in undefOK: warning2("Undefined environment variable `%s'."%(v))
					repl=''
			else:
				if v[0]=='(': v=v[1:-1]
				if v in vars: repl=vars[v]
				else:
					if not v in undefOK:
						if v in os.environ:
							repl=os.environ[v]
							if not v in replaceOK: warning2("Undefined internal variable `%s', using environment variable instead."%(v))
						else:
							warning2("Undefined internal variable `%s'."%(v))
							repl=''
					else: repl=''
			stat=stat[:m.start()]+repl+stat[m.end():]

		# scons variables (namely, $PREFIX, were smuggled unexpanded by using @PREFIX instead. Change it to what it should be now.
		stat=stat.replace('@','$')

		conditional=match(r'^\s*(!?[a-zA-Z0-9_]+)\s*{(.*)}$',stat) # this is a (possibly negated) conditional; nothing complicated, please; specifically, nested conditionals do not work
		if conditional:
			cond,then=conditional.group(1,2)
			if cond=='win32': continue # windows stuff is discarded completely
			elif cond=='!win32': stat=then # otherwise we pass the whole then-clause to further treatment
			else: warning2("Unknown condition `%s'."%cond)
		assign=match(r'^\s*([a-zA-Z0-9_.]+)\s*(=|\+=)\s*(.*)$',stat) # this line is an assignment
		call=match(r'^\s*(!?[a-z][a-zA-Z0-9_]+)\s*(\(.*)$',stat) # this line is a "function call"
		if assign:
			param,action,val=assign.group(1,2,3)
			val=sub('\s+$','',val)
			val=splitgroup(sub('\s+',' ',val))
			if param not in vars: vars[param]=[]
			if action =='=' or action=='+=':	vars[param]+=val
		elif call:
			func,rest=call.group(1,2)
			if not func in undefOK: warning2("Function `%s' not implemented; arguments `%s'.\n"%(func,rest))
		else:
			warning2("Line not parsed: `%s'.\n"%(stat))
	return vars

def proProcess(filename,_vars={},dir='.',nest=0):
	info("(%s) Processing project file `%s'."%(nest,filename))
	global currentPro
	currentPro=filename
	posterityVars=[]
	vars=deepcopy(_vars)
	for l in localVars:
		if l in vars: del vars[l] # these are NOT propagated to sub-builds
	childVars=parseProject(open(filename,'r'),vars)
	childVars['PROJECT']=filename
	assert('TEMPLATE' in childVars) # otherwise the file is broken
	assert(len(childVars['TEMPLATE'])==1) # dtto
	template=childVars['TEMPLATE'][0]
	if template=='subdirs': #
		if 'SUBDIRS' not in childVars:
			#warning("Template is `subdirs' but SUBDIRS is empty.")
			return None
		for subdir in childVars['SUBDIRS']:
			#print "dir=%s,subdir=%s"%(dir,subdir)
			subPro=dir+'/'+subdir+'/'+subdir.split('/')[-1]+'.pro' # .pro file is named the same as its immediate parent directory + .pro
			posterityVars.append(proProcess(subPro,_vars=childVars,dir=dir+'/'+subdir,nest=nest+1)) # recurse
		return [posterityVars]
	elif template=='app' or 'lib':
		guessedTarget=os.path.split(filename)[-1].split('.')[0] # garbage/foo.pro -> foo
		explicitTarget=None
		if 'TARGET' in childVars: explicitTarget=os.path.split(childVars['TARGET'][0])[1].split('.')[0]
		if explicitTarget: childVars['TARGET']=explicitTarget
		else: childVars['TARGET']=guessedTarget
		#if oldTarget!=newTarget: warning("Old and new target differ: `%s' vs. `%s' (using new one)."%(oldTarget,newTarget))
		return [childVars]
	else:
		warning2("%s: Unknown TEMPLATE `%s'"%(currentPro,template))
		return None

def listUnique(List):
	if len(List)<=1: return List
	List.sort()
	last=List[-1]
	for i in range(len(List)-2, -1, -1):
		if last==List[i]: del List[i]
		else: last=List[i]
	return List

def processVars(V,dir):
	ret=""
	for v in V:
		if isinstance(v,list):
			ret+=processVars(v,dir)
		if isinstance(v,dict):
			ret+=scriptGen(v,dir)
	return ret

def pythonicTarget(t):
	#return target name such that it is acceptable as python identifier
	return sub('(^[0-9]|^|[^a-zA-Z0-9_])','_',t)


def scriptGen(v,dir):
	project=v['PROJECT']
	global currentPro
	currentPro=project
	target=v['TARGET']
	commentOut=False # waf doesn't like targetless sources; scons is fine with that
	template=v['TEMPLATE'][0]

	###################
	### PATHS

	projAbsPath=realpath(normpath(dirname(project)))
	dirAbsPath=realpath(normpath(dir))
	prefix=commonprefix([projAbsPath,dirAbsPath])
	relPath=projAbsPath[len(prefix)+1:]
	#warning("PATHS:\n%s\n%s →\n%s"%(projAbsPath,dirAbsPath,relPath))
	assert(not isabs(relPath))
	#return 'PROJECT=%s\nTARGET=%s\nSOURCES=%s\nINCLUDES=%s\nrelPath=%s\n'%(v['PROJECT'],v['TARGET'],string.join(v['SOURCES']),string.join(v['INCLUDEPATH']),relPath)

	###################
	### SOURCES

	if 'SOURCES' in v: sources=v['SOURCES']
	else: sources=[];
	if 'FORMS' in v: sources+=v['FORMS']
	#if v.has_key('IDLS'): sources+=v['IDLS']
	sources=listUnique(sources)

	###################
	### LIBRARIES
	
	def prependDirFile(d,f): return [join(d,x) for x in f]
	def listLibs(ll):
		ret=[]
		for l in ll:
			if l[0:2]=='-l': ret.append(l[2:])
			elif l=='-rdynamic': pass
			else:
				warning2("Unknown LIBS item `%s'."%l)
		return ret
	libs=[]
	if 'LIBS' in v: libs=listLibs(v['LIBS'])

	###################
	### INCLUDE PATHS

	# waf/scons don't compile from the current directory, make sure it is there; duplicata removed later
	if 'INCLUDEPATH' not in v: includePath=['.']
	else:	includePath=v['INCLUDEPATH']+['.']
	includePath=listUnique(includePath)
	#print "relPath=%s,INCLUDEPATH=%s"%(relPath,includePath)
	def prependDirFileIfRelative(d,f):
		def prepIf(ff):
			if not isabs(ff) and ff[0]!='$': return join(d,ff)
			return ff
		return list(map(prepIf,f))
	includePath=prependDirFileIfRelative(relPath,includePath)
	# filter out non-existent paths; without warning, it could be a short lambda...
	def DelAndWarnNonexistentPath(x):
		if not exists(normpath(join(dirAbsPath,x))) and x[0]!='$':
			warning2("Include path `%s' is invalid, removed!"%(normpath(join(dirAbsPath,x))))
			return False
		return True
	includePath=list(filter(DelAndWarnNonexistentPath,includePath))
	includePath=[normpath(p) for p in includePath]
	# remove duplicates... http://stinkpot.afraid.org:8080/tricks/index.php/2006/05/find-all-the-unique-elements-in-a-python-list/
	includePath=list(dict([(i,1) for i in includePath]).keys())



	# begin building the actual script
	ret="## %s\n"%v['PROJECT']

	if template=='lib': 
		if 'CONFIG' in v and 'dll' in v['CONFIG']: targetType='shlib'
		else: targetType='staticlib'
	elif template=='app': targetType='program'

	if buildEngine=='waf':
		assert(targetType) # "TEMPLATE is neither `lib' nor `app'.

		if not len(sources)==0:
			warning2("Project `%s' has empty source list, build rule will be commented out."%project)
			commentOut=True

		langType='cpp'
		localLibs,systemLibs=[],[]
		if target in targetLangType: langType=targetLangType[target]
		if langType=='qt3': systemLibs.append('QT3')

		ret+="obj=bld.create_obj('%s','%s')\n"%(langType,targetType)

		ret+="obj.name='%s'\n"%target
		ret+="obj.target='%s'\n"%target
		
		ret+="obj.source='%s'\n"%string.join(prependDirFile(relPath,sources))

		# FIXME: install not yet there
		#ret+="obj.install_in='%s'\n"%
		
		#includePath=filter(lambda x: exists(normpath(join(dirAbsPath,x))),includePath)

		ret+="obj.includes='%s'\n"%string.join(includePath)
		# libs
		if len(libs)>0:
			for l in libs:
				if l in allSystemLibs: systemLibs.append(l)
				else: localLibs.append(ll)
		if len(localLibs)>0:	ret+="obj.uselib_local='%s'\n"%(string.join(localLibs))
		if len(systemLibs)>0: ret+="obj.uselib='%s'\n"%(string.join(systemLibs))
	elif buildEngine=='scons':
		env=defaultEnv
		if target in targetEnv: env=targetEnv[target]
		ret+="%s.%s('%s',%s%s"%(env,{'program':'Program','shlib':'SharedLibrary','staticlib':'StaticLibrary'}[targetType],
			target,fieldSep,toStr(prependDirFile(relPath,sources)))
		if (env,targetType) in installable: installable[(env,targetType)].append(pythonicTarget(target))
		else: installable[(env,targetType)]=[pythonicTarget(target),]
		if len(libs)>0:
			ret+=",%sLIBS=%s['LIBS']+%s"%(fieldSep,env,toStr(libs))
		if len(includePath)>0:
			# using CPPPATH would override top-level settings (question posted on scons ML how to avoid this)
			# for now, put all paths prefixed with -I to CPPFLAGS as workaround
			#ret+=",%sCPPFLAGS=%s"%(fieldSep,toStr(map(lambda x: '-I'+x,includePath)))
			### NO, that doesn't work since CPPPATH is relative to SConscript dir, whereas CPPFLAGS are not prepended (logically)
			### try the inverse:
			ret+=",%sCPPPATH=%s['CPPPATH']+%s"%(fieldSep,env,toStr(includePath))
		ret+=')'
		instDir=installDirs[targetType]
		if (env,instDir) in instDirTargets: instDirTargets[(env,instDir)].append((ret,target))
		else: instDirTargets[(env,instDir)]=[(ret,target)]
		
	else: raise ValueError('buildEngine must be waf or scons (--waf or --scons)');


	if commentOut:
		ret='# This rule is commented out because it has no source'+sub('(^|\n)','\n#',ret)

	ret+='\n'
	return ret


def masterScriptGen(V,dir):
	#
	s=''
	if buildEngine=='scons':
		s+="Import('*')\n"
		processVars(V,dir) # return value of processVars discarded, results passed in instDirTarget instead
		for env,dir in list(instDirTargets.keys()):
			idt=instDirTargets[(env,dir)]
			#binary targets are handles specially: they install under _filename_ postfixed
			if match('.*/bin$',dir): ## warhing: this is OS-specific to tell binary target!
				s+="%s.InstallAs(["%env+','.join(["'%s$POSTFIX'"%(join(dir,tgSpec[1])) for tgSpec in idt])+"],[\n"
				s+=',\n'.join([sub(r'(?m)^','\t',tgSpec[0]) for tgSpec in idt])
				s+='\n])'
			#whereas other targets install under _filename_ into a dir that has been postfixed already
			else:
				s+="%s.Install('%s',[\n"%(env,dir)
				s+=',\n'.join([sub(r'(?m)^','\t',tgSpec[0]) for tgSpec in idt])+'\n])\n'
				#for tgSpec in idt: print tgSpec[0]
	elif buildEngine=='waf':
		s+=processVars(V,dir)
	return s


assert(len(sys.argv)>=3)
buildEngine,projects,scriptDir=sys.argv[1][2:],sys.argv[2:-1],sys.argv[-1]
assert(buildEngine in ['waf','scons'])
allVars=[]
for project in projects:
	allVars.append(proProcess(project,dir=dirname(project)))

# HACK: useful stuff to pass down via globals...
pretty=True
if pretty:
	def toStr(what):
		ret=pprint.pformat(what); n=1
		# replace leading spaces (nesting) by tabs...
		while n: ret,n=subn(r'(?m)^(\t*) ',r'\1\t',ret)
		# indent all lines by one more tab
		ret=sub(r'(?m)^','\t',ret)
		# de-indent the first one
		return ret[1:]
	fieldSep='\n\t'
else:
	toStr=str; fieldSep='';
installable={} # hash indexed by (env,targetType)->pythonicTargetName
instDirTargets={} # hash indexed by (env,targetDir)->str_target_spec
installDirs={'shlib':join('$PREFIX','lib','yade$POSTFIX',string.split(scriptDir,os.sep)[-1]),'staticlib':join('$PREFIX','lib','yade$POSTFIX'),'program':join('$PREFIX','bin')}

print(masterScriptGen(allVars,scriptDir))
