#!/usr/bin/env python
# encoding: utf-8
# Thomas Nagy, 2005-2008 (ita)

"""
Configuration system

A configuration instance is created when "waf configure" is called, it is used to:
* create data dictionaries (Environment instances)
* store the list of modules to import

The old model (copied from Scons) was to store logic (mapping file extensions to functions)
along with the data. In Waf a way was found to separate that logic by adding an indirection
layer (storing the names in the Environment instances)

In the new model, the logic is more object-oriented, and the user scripts provide the
logic. The data files (Environments) must contain configuration data only (flags, ..).

Note: the c/c++ related code is in the module config_c
"""

import os, shlex, sys, time
try: import cPickle
except ImportError: import pickle as cPickle
import Environment, Utils, Options, Logs
from Logs import warn
from Constants import *

try:
	from urllib import request
except:
	from urllib import urlopen
else:
	urlopen = request.urlopen

conf_template = '''# project %(app)s configured on %(now)s by
# waf %(wafver)s (abi %(abi)s, python %(pyver)x on %(systype)s)
# using %(args)s
#
'''

class ConfigurationError(Utils.WscriptError):
	pass

autoconfig = False
"reconfigure the project automatically"

def find_file(filename, path_list):
	"""find a file in a list of paths
	@param filename: name of the file to search for
	@param path_list: list of directories to search
	@return: the first occurrence filename or '' if filename could not be found
"""
	for directory in Utils.to_list(path_list):
		if os.path.exists(os.path.join(directory, filename)):
			return directory
	return ''

def find_program_impl(env, filename, path_list=[], var=None, environ=None):
	"""find a program in folders path_lst, and sets env[var]
	@param env: environment
	@param filename: name of the program to search for
	@param path_list: list of directories to search for filename
	@param var: environment value to be checked for in env or os.environ
	@return: either the value that is referenced with [var] in env or os.environ
         or the first occurrence filename or '' if filename could not be found
"""

	if not environ:
		environ = os.environ

	try: path_list = path_list.split()
	except AttributeError: pass

	if var:
		if env[var]: return env[var]
		if var in environ: env[var] = environ[var]

	if not path_list: path_list = environ.get('PATH', '').split(os.pathsep)

	ext = (Options.platform == 'win32') and '.exe,.com,.bat,.cmd' or ''
	for y in [filename+x for x in ext.split(',')]:
		for directory in path_list:
			x = os.path.join(directory, y)
			if os.path.isfile(x):
				if var: env[var] = x
				return x
	return ''

class ConfigurationContext(Utils.Context):
	tests = {}
	error_handlers = []
	def __init__(self, env=None, blddir='', srcdir=''):
		self.env = None
		self.envname = ''

		self.environ = dict(os.environ)

		self.line_just = 40

		self.blddir = blddir
		self.srcdir = srcdir
		self.all_envs = {}

		# curdir: necessary for recursion
		self.cwd = self.curdir = os.getcwd()

		self.tools = [] # tools loaded in the configuration, and that will be loaded when building

		self.setenv(DEFAULT)

		self.lastprog = ''

		self.hash = 0
		self.files = []

		self.tool_cache = []

		if self.blddir:
			self.post_init()

	def post_init(self):

		self.cachedir = os.path.join(self.blddir, CACHE_DIR)

		path = os.path.join(self.blddir, WAF_CONFIG_LOG)
		try: os.unlink(path)
		except (OSError, IOError): pass

		try:
			self.log = open(path, 'w')
		except (OSError, IOError):
			self.fatal('could not open %r for writing' % path)

		app = Utils.g_module.APPNAME
		if app:
			ver = getattr(Utils.g_module, 'VERSION', '')
			if ver:
				app = "%s (%s)" % (app, ver)

		now = time.ctime()
		pyver = sys.hexversion
		systype = sys.platform
		args = " ".join(sys.argv)
		wafver = WAFVERSION
		abi = ABI
		self.log.write(conf_template % vars())

	def __del__(self):
		"""cleanup function: close config.log"""

		# may be ran by the gc, not always after initialization
		if hasattr(self, 'log') and self.log:
			self.log.close()

	def fatal(self, msg):
		raise ConfigurationError(msg)

	def check_tool(self, input, tooldir=None, funs=None):
		"load a waf tool"

		tools = Utils.to_list(input)
		if tooldir: tooldir = Utils.to_list(tooldir)
		for tool in tools:
			tool = tool.replace('++', 'xx')
			if tool == 'java': tool = 'javaw'
			if tool.lower() == 'unittest': tool = 'unittestw'
			# avoid loading the same tool more than once with the same functions
			# used by composite projects

			mag = (tool, id(self.env), funs)
			if mag in self.tool_cache:
				continue
			self.tool_cache.append(mag)

			module = None
			try:
				module = Utils.load_tool(tool, tooldir)
			except Exception, e:
				ex = e
				if Options.options.download:
					_3rdparty = os.path.normpath(Options.tooldir[0] + os.sep + '..' + os.sep + '3rdparty')

					# try to download the tool from the repository then
					# the default is set to false
					for x in Utils.to_list(Options.remote_repo):
						for sub in ['branches/waf-%s/wafadmin/3rdparty' % WAFVERSION, 'trunk/wafadmin/3rdparty']:
							url = '/'.join((x, sub, tool + '.py'))
							try:
								web = urlopen(url)
								if web.getcode() != 200:
									continue
							except Exception, e:
								# on python3 urlopen throws an exception
								continue
							else:
								loc = None
								try:
									loc = open(_3rdparty + os.sep + tool + '.py', 'wb')
									loc.write(web.read())
									web.close()
								finally:
									if loc:
										loc.close()
								Logs.warn('downloaded %s from %s' % (tool, url))
								try:
									module = Utils.load_tool(tool, tooldir)
								except:
									Logs.warn('module %s from %s is unusable' % (tool, url))
									try:
										os.unlink(_3rdparty + os.sep + tool + '.py')
									except:
										pass
									continue
						else:
							break

					if not module:
						Logs.error('Could not load the tool %r or download a suitable replacement from the repository (sys.path %r)\n%s' % (tool, sys.path, e))
						raise ex
				else:
					Logs.error('Could not load the tool %r in %r (try the --download option?):\n%s' % (tool, sys.path, e))
					raise ex

			if funs is not None:
				self.eval_rules(funs)
			else:
				func = getattr(module, 'detect', None)
				if func:
					if type(func) is type(find_file): func(self)
					else: self.eval_rules(func)

			self.tools.append({'tool':tool, 'tooldir':tooldir, 'funs':funs})

	def sub_config(self, k):
		"executes the configure function of a wscript module"
		self.recurse(k, name='configure')

	def pre_recurse(self, name_or_mod, path, nexdir):
		return {'conf': self, 'ctx': self}

	def post_recurse(self, name_or_mod, path, nexdir):
		if not autoconfig:
			return
		self.hash = hash((self.hash, getattr(name_or_mod, 'waf_hash_val', name_or_mod)))
		self.files.append(path)

	def store(self, file=''):
		"save the config results into the cache file"
		if not os.path.isdir(self.cachedir):
			os.makedirs(self.cachedir)

		if not file:
			file = open(os.path.join(self.cachedir, 'build.config.py'), 'w')
		file.write('version = 0x%x\n' % HEXVERSION)
		file.write('tools = %r\n' % self.tools)
		file.close()

		if not self.all_envs:
			self.fatal('nothing to store in the configuration context!')
		for key in self.all_envs:
			tmpenv = self.all_envs[key]
			tmpenv.store(os.path.join(self.cachedir, key + CACHE_SUFFIX))

	def set_env_name(self, name, env):
		"add a new environment called name"
		self.all_envs[name] = env
		return env

	def retrieve(self, name, fromenv=None):
		"retrieve an environment called name"
		try:
			env = self.all_envs[name]
		except KeyError:
			env = Environment.Environment()
			env['PREFIX'] = os.path.abspath(os.path.expanduser(Options.options.prefix))
			self.all_envs[name] = env
		else:
			if fromenv: warn("The environment %s may have been configured already" % name)
		return env

	def setenv(self, name):
		"enable the environment called name"
		self.env = self.retrieve(name)
		self.envname = name

	def add_os_flags(self, var, dest=None):
		# do not use 'get' to make certain the variable is not defined
		try: self.env.append_value(dest or var, Utils.to_list(self.environ[var]))
		except KeyError: pass

	def check_message_1(self, sr):
		self.line_just = max(self.line_just, len(sr))
		for x in ('\n', self.line_just * '-', '\n', sr, '\n'):
			self.log.write(x)
		Utils.pprint('NORMAL', "%s :" % sr.ljust(self.line_just), sep='')

	def check_message_2(self, sr, color='GREEN'):
		self.log.write(sr)
		self.log.write('\n')
		Utils.pprint(color, sr)

	def check_message(self, th, msg, state, option=''):
		sr = 'Checking for %s %s' % (th, msg)
		self.check_message_1(sr)
		p = self.check_message_2
		if state: p('ok ' + str(option))
		else: p('not found', 'YELLOW')

	# FIXME remove in waf 1.6
	# the parameter 'option' is not used (kept for compatibility)
	def check_message_custom(self, th, msg, custom, option='', color='PINK'):
		sr = 'Checking for %s %s' % (th, msg)
		self.check_message_1(sr)
		self.check_message_2(custom, color)

	def msg(self, msg, result, color=None):
		"""Prints a configuration message 'Checking for xxx: ok'"""
		self.start_msg('Checking for ' + msg)

		if not isinstance(color, str):
			color = result and 'GREEN' or 'YELLOW'

		self.end_msg(result, color)

	def start_msg(self, msg):
		try:
			if self.in_msg:
				return
		except:
			self.in_msg = 0
		self.in_msg += 1

		self.line_just = max(self.line_just, len(msg))
		for x in ('\n', self.line_just * '-', '\n', msg, '\n'):
			self.log.write(x)
		Utils.pprint('NORMAL', "%s :" % msg.ljust(self.line_just), sep='')

	def end_msg(self, result, color):
		self.in_msg -= 1
		if self.in_msg:
			return

		if not color:
			color = 'GREEN'
		if result == True:
			msg = 'ok'
		elif result == False:
			msg = 'not found'
			color = 'YELLOW'
		else:
			msg = str(result)

		self.log.write(msg)
		self.log.write('\n')
		Utils.pprint(color, msg)

	def find_program(self, filename, path_list=[], var=None, mandatory=False):
		"wrapper that adds a configuration message"

		ret = None
		if var:
			if self.env[var]:
				ret = self.env[var]
			elif var in os.environ:
				ret = os.environ[var]

		if not isinstance(filename, list): filename = [filename]
		if not ret:
			for x in filename:
				ret = find_program_impl(self.env, x, path_list, var, environ=self.environ)
				if ret: break

		self.check_message_1('Checking for program %s' % ' or '.join(filename))
		self.log.write('  find program=%r paths=%r var=%r\n  -> %r\n' % (filename, path_list, var, ret))
		if ret:
			Utils.pprint('GREEN', str(ret))
		else:
			Utils.pprint('YELLOW', 'not found')
			if mandatory:
				self.fatal('The program %r is required' % filename)

		if var:
			self.env[var] = ret
		return ret

	def cmd_to_list(self, cmd):
		"commands may be written in pseudo shell like 'ccache g++'"
		if isinstance(cmd, str) and cmd.find(' '):
			try:
				os.stat(cmd)
			except OSError:
				return shlex.split(cmd)
			else:
				return [cmd]
		return cmd

	def __getattr__(self, name):
		r = self.__class__.__dict__.get(name, None)
		if r: return r
		if name and name.startswith('require_'):

			for k in ['check_', 'find_']:
				n = name.replace('require_', k)
				ret = self.__class__.__dict__.get(n, None)
				if ret:
					def run(*k, **kw):
						r = ret(self, *k, **kw)
						if not r:
							self.fatal('requirement failure')
						return r
					return run
		self.fatal('No such method %r' % name)

	def eval_rules(self, rules):
		self.rules = Utils.to_list(rules)
		for x in self.rules:
			f = getattr(self, x)
			if not f: self.fatal("No such method '%s'." % x)
			try:
				f()
			except Exception, e:
				ret = self.err_handler(x, e)
				if ret == BREAK:
					break
				elif ret == CONTINUE:
					continue
				else:
					self.fatal(e)

	def err_handler(self, fun, error):
		pass

def conf(f):
	"decorator: attach new configuration functions"
	setattr(ConfigurationContext, f.__name__, f)
	return f

def conftest(f):
	"decorator: attach new configuration tests (registered as strings)"
	ConfigurationContext.tests[f.__name__] = f
	return conf(f)