#! /usr/bin/env python
# -*- encoding: utf-8 -*-
# Licensed under The MIT License (MIT)
# Copyright (c) 2016, Michel Mooij
# Copyright (c) 2023, Velaron

'''
Summary
-------
Generate *cmake* files of all C/C++ programs, static- and shared libraries
that have been defined within a *waf* build environment.
Once exported to *cmake*, all exported (C/C++) tasks can be build without
any further need for, or dependency, to the *waf* build system itself.

**cmake** is an open source cross-platform build system designed to build, test
and package software. It is available for all major Desktop Operating Systems
(MS Windows, all major Linux distributions and Macintosh OS-X).
See http://www.cmake.org for a more detailed description on how to install
and use it for your particular Desktop environment.


Description
-----------
When exporting *waf* project data, a single top level **CMakeLists.txt** file
will be exported in the top level directory of your *waf* build environment.
This *cmake* build file will contain references to all exported *cmake*
build files of each individual C/C++ build task. It will also contain generic
variables and settings (e.g compiler to use, global preprocessor defines, link
options and so on).

Example below presents an overview of an environment in which *cmake*
build files already have been exported::

        .
        ├── components
        │   └── clib
        │       ├── program
        │       │   ├── CMakeLists.txt
        │       │   └── wscript
        │       ├── shared
        │       │   ├── CMakeLists.txt
        │       │   └── wscript
        │       └── static
        │           ├── CMakeLists.txt
        │           └── wscript
        │
        ├── CMakeLists.txt
        └── wscript


Usage
-----
Tasks can be exported to *cmake* using the command, as shown in the
example below::

        $ waf cmake

All exported *cmake* build files can be removed in 'one go' using the *cmake*
*cleanup* option::

        $ waf cmake --cmake-clean

Tasks generators to be excluded can be marked with the *skipme* option
as shown below::

    def build(bld):
        bld.program(name='foo', src='foobar.c', cmake_skip=True)

'''


from waflib.Build import BuildContext
from waflib import Utils, Logs, Context, Errors


def get_deps(bld, target):
	'''Returns a list of (nested) targets on which this target depends.

	:param bld: a *waf* build instance from the top level *wscript*
	:type bld: waflib.Build.BuildContext
	:param target: task name for which the dependencies should be returned
	:type target: str
	:returns: a list of task names on which the given target depends
	'''
	try:
		tgen = bld.get_tgen_by_name(target)
	except Errors.WafError:
		return []
	else:
		uses = Utils.to_list(getattr(tgen, 'use', []))
		deps = uses[:]
		for use in uses:
			deps += get_deps(bld, use)
		return list(set(deps))


def get_tgens(bld, names):
	'''Returns a list of task generators based on the given list of task
	generator names.

	:param bld: a *waf* build instance from the top level *wscript*
	:type bld: waflib.Build.BuildContext
	:param names: list of task generator names
	:type names: list of str
	:returns: list of task generators
	'''
	tgens = []
	for name in names:
		try:
			tgen = bld.get_tgen_by_name(name)
		except Errors.WafError:
			pass
		else:
			tgens.append(tgen)
	return list(set(tgens))


def get_targets(bld):
	'''Returns a list of user specified build targets or None if no specific
	build targets has been selected using the *--targets=* command line option.

	:param bld: a *waf* build instance from the top level *wscript*.
	:type bld: waflib.Build.BuildContext
	:returns: a list of user specified target names (using --targets=x,y,z) or None
	'''
	if bld.targets == '':
		return None
	targets = bld.targets.split(',')
	for target in targets:
		targets += get_deps(bld, target)
	return targets


def options(opt):
	'''Adds command line options for the CMake *waftool*.

	:param opt: Options context from the *waf* build environment.
	:type opt: waflib.Options.OptionsContext
	'''
	opt.add_option('--cmake', dest='cmake', default=False,
				   action='store_true', help='select cmake for export/import actions')
	opt.add_option('--clean', dest='clean', default=False,
				   action='store_true', help='delete exported files')


def configure(conf):
	'''Method that will be invoked by *waf* when configuring the build
	environment.

	:param conf: Configuration context from the *waf* build environment.
	:type conf: waflib.Configure.ConfigurationContext
	'''
	conf.find_program('cmake', var='CMAKE', mandatory=False)


class CMakeContext(BuildContext):
	'''export C/C++ tasks to CMake.'''
	cmd = 'cmake'

	def execute(self):
		'''Will be invoked when issuing the *cmake* command.'''
		self.restore()
		if not self.all_envs:
			self.load_envs()
		self.recurse([self.run_dir])
		self.pre_build()

		for group in self.groups:
			for tgen in group:
				try:
					f = tgen.post
				except AttributeError:
					pass
				else:
					f()
		try:
			self.get_tgen_by_name('')
		except Exception:
			pass

		self.cmake = True
		if self.options.clean:
			cleanup(self)
		else:
			export(self)
		self.timer = Utils.Timer()


def export(bld):
	'''Exports all C and C++ task generators to cmake.

	:param bld: a *waf* build instance from the top level *wscript*.
	:type bld: waflib.Build.BuildContext
	'''
	if not bld.options.cmake and not hasattr(bld, 'cmake'):
		return

	cmakes = {}
	loc = bld.path.relpath().replace('\\', '/')
	top = CMake(bld, loc)
	cmakes[loc] = top
	targets = get_targets(bld)

	for tgen in bld.task_gen_cache_names.values():
		if targets and tgen.get_name() not in targets:
			continue
		if getattr(tgen, 'cmake_skip', False):
			continue
		if set(('c', 'cxx')) & set(getattr(tgen, 'features', [])):
			loc = tgen.path.relpath().replace('\\', '/')
			if loc not in cmakes:
				cmake = CMake(bld, loc)
				cmakes[loc] = cmake
				top.add_child(cmake)
			cmakes[loc].add_tgen(tgen)

	for cmake in cmakes.values():
		cmake.export()


def cleanup(bld):
	'''Removes all generated makefiles from the *waf* build environment.

	:param bld: a *waf* build instance from the top level *wscript*.
	:type bld: waflib.Build.BuildContext
	'''
	if not bld.options.clean:
		return

	loc = bld.path.relpath().replace('\\', '/')
	CMake(bld, loc).cleanup()
	targets = get_targets(bld)

	for tgen in bld.task_gen_cache_names.values():
		if targets and tgen.get_name() not in targets:
			continue
		if getattr(tgen, 'cmake_skip', False):
			continue
		if set(('c', 'cxx')) & set(getattr(tgen, 'features', [])):
			loc = tgen.path.relpath().replace('\\', '/')
			CMake(bld, loc).cleanup()


class CMake(object):
	def __init__(self, bld, location):
		self.bld = bld
		self.location = location
		self.cmakes = []
		self.tgens = []

	def export(self):
		content = self.get_content()
		if not content:
			return

		node = self.make_node()
		if not node:
			return
		node.write(content)
		Logs.pprint('YELLOW', 'exported: %s' % node.abspath())

	def cleanup(self):
		node = self.find_node()
		if node:
			node.delete()
			Logs.pprint('YELLOW', 'removed: %s' % node.abspath())

	def add_child(self, cmake):
		self.cmakes.append(cmake)

	def add_tgen(self, tgen):
		self.tgens.append(tgen)

	def get_location(self):
		return self.location

	def get_fname(self):
		name = '%s/CMakeLists.txt' % (self.location)
		return name

	def find_node(self):
		name = self.get_fname()
		if not name:
			return None
		return self.bld.srcnode.find_node(name)

	def make_node(self):
		name = self.get_fname()
		if not name:
			return None
		return self.bld.srcnode.make_node(name)

	def get_content(self):
		is_top = (self.location == self.bld.path.relpath())

		content = ''
		if is_top:
			content += 'cmake_minimum_required(VERSION 2.8.12)\n'
			content += 'project(%s)\n' % (getattr(Context.g_module,
												  Context.APPNAME))
			content += '\n'

			env = self.bld.env
			defines = env.DEFINES
			if len(defines):
				content += 'add_definitions(\n	-D%s\n)\n' % (
					'\n	-D'.join(defines))
				content += '\n'

			flags = env.CFLAGS
			if len(flags):
				content += 'set(CMAKE_C_FLAGS "%s")\n' % (' '.join(flags))

			flags = env.CXXFLAGS
			if len(flags):
				content += 'set(CMAKE_CXX_FLAGS "%s")\n' % (' '.join(flags))

		if len(self.tgens):
			content += '\n'
			for tgen in self.tgens:
				content += self.get_tgen_content(tgen)

		if len(self.cmakes):
			content += '\n'
			for cmake in self.cmakes:
				content += 'add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/%s)\n' % (
					cmake.get_location())

		return content

	def get_tgen_content(self, tgen):
		content = ''
		name = tgen.get_name()

		content += 'set(%s_SRC' % (name.upper())
		for src in tgen.source:
			content += '\n	%s' % (src.path_from(tgen.path).replace('\\', '/'))
		content += '\n)\n\n'

		includes = self.get_includes(tgen)
		# includes.extend(tgen.env.INCLUDES)
		if len(includes):
			content += 'set(%s_INCLUDES' % (name.upper())
			for include in includes:
				content += '\n	%s' % include
			content += '\n)\n\n'
			content += 'include_directories(${%s_INCLUDES})\n' % (name.upper())

		link_dirs = getattr(tgen.env, 'LIBPATH', [])
		if len(link_dirs):
			content += '\nlink_directories('
			for dir in link_dirs:
				content += '\n	\"%s\"' % dir.replace('\\', '/')
			content += '\n)\n\n'

		if set(('cprogram', 'cxxprogram')) & set(tgen.features):
			if tgen.env.DEST_OS == 'win32':
				content += 'add_executable(%s WIN32 ${%s_SRC})\n' % (
					name, name.upper())
			else:
				content += 'add_executable(%s ${%s_SRC})\n' % (name,
															   name.upper())

		elif set(('cshlib', 'cxxshlib')) & set(tgen.features):
			content += 'add_library(%s SHARED ${%s_SRC})\n\n' % (
				name, name.upper())

		else:  # cstlib, cxxstlib or objects
			content += 'add_library(%s ${%s_SRC})\n\n' % (name, name.upper())

		defines = self.get_genlist(tgen, 'defines')
		defines.extend(tgen.env.DEFINES)
		if len(defines):
			content += 'target_compile_definitions(%s PRIVATE\n	-D%s\n)\n' % (
				name, '\n	-D'.join(defines))
			content += '\n'

		libs = getattr(tgen.env, 'LIB', [])
		libs.extend(tgen.env.STLIB)

		if len(libs):
			content += '\n'
			content += 'target_link_libraries(%s\n	%s)\n' % (name, '\n	'.join(libs))
			content += '\n'

		return content

	def get_includes(self, tgen):
		'''returns the include paths for the given task generator.
		'''
		includes = self.get_genlist(tgen, 'includes')
		for use in getattr(tgen, 'use', []):
			key = 'INCLUDES_%s' % use
			try:
				tg = self.bld.get_tgen_by_name(use)
				if 'fake_lib' in tg.features:
					if key in tgen.env:
						includes.extend([l.replace('\\', '/')
										for l in tgen.env[key]])
			except Errors.WafError:
				if key in tgen.env:
					includes.extend([l.replace('\\', '/')
									for l in tgen.env[key]])
		return includes

	def get_genlist(self, tgen, name):
		lst = Utils.to_list(getattr(tgen, name, []))
		lst = [str(l.path_from(tgen.path)) if hasattr(
			l, 'path_from') else l for l in lst]
		return [l.replace('\\', '/') for l in lst]