## -*- Mode: python; py-indent-offset: 4; indent-tabs-mode: nil; coding: utf-8; -*-
from __future__ import print_function
from collections import deque
import os, os.path
import sys
import shutil
import types
import warnings

from waflib import TaskGen, Task, Options, Build, Utils, Context
from waflib.Errors import WafError
import wutils

try:
    set
except NameError:
    from sets import Set as set # Python 2.3 fallback

# Allow mulitple modules to live in a single directory in contrib.
# For example, a directory structure like:
# contrib/package/module1
#                /module2
# Useful for external projects that are building interdependent modules that
# are logically packaged together.
def find_contrib_modules(ctx, log=False):
    modules = []

    entries = deque( (ctx.path, d) for d in ctx.path.listdir() )
    
    while entries:
        parent, entry = entries.popleft()
 
        if not entry or entry[0] == '.' or entry.endswith('CVS'):
            continue

        node = parent.find_node(entry) 

        if not node:
            continue

        if node.isdir():
            #does this directory have a wscript file?
            wscript_node = node.find_node('wscript')

            if wscript_node:
                #found a wscript file, treat this directory as a module.
                
                #get the path relative to the context path 
                module_path = node.path_from(ctx.path)
                modules.append(module_path)

                if log:
                    ctx.msg("Found contrib module", module_path)
            else:
                #maybe this directory is a project, 
                #add its children to the list of entries to process
                entries.extend( (node, d) for d in node.listdir() )

    return sorted(modules)

def get_required_boost_libs(conf):
    for module in find_contrib_modules(conf):
        conf.recurse (module, name="required_boost_libs", mandatory=False)

def options(opt):
    for module in find_contrib_modules(opt):
        opt.recurse(module, mandatory=False)

def configure(conf):
    all_contrib_modules = find_contrib_modules(conf, True)

    # Append blddir to the module path before recursing into modules
    # This is required for contrib modules with test suites
    blddir = os.path.abspath(os.path.join(conf.bldnode.abspath(), conf.variant))
    conf.env.append_value('NS3_MODULE_PATH', blddir)

    # Remove duplicate path items
    conf.env['NS3_MODULE_PATH'] = wutils.uniquify_list(conf.env['NS3_MODULE_PATH'])

    for module in all_contrib_modules:
        conf.recurse(module, mandatory=False)

    ## Used to link the 'test-runner' program with all of ns-3 code
    conf.env['NS3_CONTRIBUTED_MODULES'] = ['ns3-' + module.split('/')[-1] for module in all_contrib_modules]


# we need the 'ns3module' waf "feature" to be created because code
# elsewhere looks for it to find the ns3 module objects.

def create_ns3_module(bld, name, dependencies=(), test=False):
    static = bool(bld.env.ENABLE_STATIC_NS3)
    # Create a separate library for this module.
    if static:
        module = bld(features='cxx cxxstlib ns3module')
    else:
        module = bld(features='cxx cxxshlib ns3module')
    target = '%s/lib/ns%s-%s%s' % (bld.srcnode.path_from(module.path),
                                          wutils.VERSION,
                                          name, bld.env.BUILD_SUFFIX)
    
    module.target = target
    linkflags = []
    cxxflags = []
    ccflags = []
    if not static:
        cxxflags = module.env['shlib_CXXFLAGS']
        ccflags = module.env['shlib_CXXFLAGS']
        # Turn on the link flags for shared libraries if we have the
        # proper compiler and platform.
        if module.env['CXX_NAME'] in ['gcc', 'icc'] and module.env['WL_SONAME_SUPPORTED']:
            # Get the module library name without any relative paths
            # at its beginning because all of the libraries will end
            # up in the same directory.
            module_library_name = module.env.cshlib_PATTERN % (os.path.basename(module.target),)
            linkflags = '-Wl,--soname=' + module_library_name
    cxxdefines = ["NS3_MODULE_COMPILATION"]
    ccdefines = ["NS3_MODULE_COMPILATION"]

    module.env.append_value('CXXFLAGS', cxxflags)
    module.env.append_value('CCFLAGS', ccflags)
    module.env.append_value('LINKFLAGS', linkflags)
    module.env.append_value('CXXDEFINES', cxxdefines)
    module.env.append_value('CCDEFINES', ccdefines)

    module.is_static = static
    module.vnum = wutils.VNUM
    # Add the proper path to the module's name.
    # Set the libraries this module depends on.  
    module.module_deps = list(dependencies)

    module.install_path = "${LIBDIR}"

    module.name = "ns3-" + name
    module.dependencies = dependencies
    # Initially create an empty value for this because the pcfile
    # writing task assumes every module has a uselib attribute.
    module.uselib = ''
    module.use = ['ns3-' + dep for dep in dependencies]
    module.test = test
    module.is_ns3_module = True
    module.ns3_dir_location = bld.path.path_from(bld.srcnode)

    module.env.append_value("INCLUDES", Context.out_dir)

    module.pcfilegen = bld(features='ns3pcfile')
    module.pcfilegen.module = module.name
    
    return module

def create_ns3_module_test_library(bld, name):
    # Create an ns3 module for the test library that depends only on
    # the module being tested.
    library_name = name + "-test"
    library = bld.create_ns3_module(library_name, [name], test=True)
    library.features += " ns3testlib"

    # Modify attributes for the test library that are different from a
    # normal module.
    del library.is_ns3_module
    library.is_ns3_module_test_library = True
    library.module_name = 'ns3-' + name

    # Add this module and test library to the list.
    bld.env.append_value('NS3_MODULES_WITH_TEST_LIBRARIES', [(library.module_name, library.name)])

    # Set the include path from the build directory to modules. 
    relative_path_from_build_to_here = bld.path.path_from(bld.bldnode)
    include_flag = '-I' + relative_path_from_build_to_here
    library.env.append_value('CXXFLAGS', include_flag)
    library.env.append_value('CCFLAGS',  include_flag)

    return library

def create_obj(bld, *args):
    warnings.warn("(in %s) Use bld(...) call now, instead of bld.create_obj(...)" % str(bld.path),
                  DeprecationWarning, stacklevel=2)
    return bld(*args)


def ns3_python_bindings(bld):
    # this method is called from a module wscript, so remember bld.path is not bindings/python!
    module_abs_src_path = bld.path.abspath()
    module = os.path.basename(module_abs_src_path)
    env = bld.env
    env.append_value("MODULAR_BINDINGS_MODULES", "ns3-"+module)

    if Options.options.apiscan:
        return

    if not env['ENABLE_PYTHON_BINDINGS']:
        return

    bindings_dir = bld.path.find_dir("bindings")
    if bindings_dir is None or not os.path.exists(bindings_dir.abspath()):
        warnings.warn("(in %s) Requested to build modular python bindings, but apidefs dir not found "
                      "=> skipped the bindings." % str(bld.path),
                      Warning, stacklevel=2)
        return

    if ("ns3-%s" % (module,)) not in env.NS3_ENABLED_CONTRIBUTED_MODULES:
        #print "bindings for module %s which is not enabled, skip" % module)
        return

    env.append_value('PYTHON_MODULES_BUILT', module)
    try:
        apidefs = env['PYTHON_BINDINGS_APIDEFS'].replace("-", "_")
    except AttributeError:
        # we likely got an empty list for env['PYTHON_BINDINGS_APIDEFS']
        return

    #debug = ('PYBINDGEN_DEBUG' in os.environ)
    debug = True # XXX
    source = [bld.srcnode.find_resource('bindings/python/ns3modulegen-modular.py'),
              bld.path.find_resource("bindings/modulegen__%s.py" % apidefs)]

    modulegen_customizations = bindings_dir.find_resource("modulegen_customizations.py")
    if modulegen_customizations is not None:
        source.append(modulegen_customizations)

    modulegen_local = bld.path.find_resource("bindings/modulegen_local.py")
    # the local customization file may or not exist
    if modulegen_local is not None:
        source.append("bindings/modulegen_local.py")

    module_py_name = module.replace('-', '_')
    module_target_dir = bld.srcnode.find_dir("bindings/python/ns").path_from(bld.path)

    # if bindings/<module>.py exists, it becomes the module frontend, and the C extension befomes _<module>
    if bld.path.find_resource("bindings/%s.py" % (module_py_name,)) is not None:
        bld(features='copy',
            source=("bindings/%s.py" % (module_py_name,)),
            target=('%s/%s.py' % (module_target_dir, module_py_name)))
        extension_name = '_%s' % (module_py_name,)
        bld.install_files('${PYTHONARCHDIR}/ns', ["bindings/%s.py" % (module_py_name,)])
    else:
        extension_name = module_py_name

    target = ['bindings/ns3module.cc', 'bindings/ns3module.h', 'bindings/ns3modulegen.log']
    #if not debug:
    #    target.append('ns3modulegen.log')

    argv = ['NS3_ENABLED_FEATURES=${FEATURES}',
            'GCC_RTTI_ABI_COMPLETE=${GCC_RTTI_ABI_COMPLETE}',
            '${PYTHON}']
    #if debug:
    #    argv.extend(["-m", "pdb"])
    
    argv.extend(['${SRC[0]}', module_abs_src_path, apidefs, extension_name, '${TGT[0]}'])

    argv.extend(['2>', '${TGT[2]}']) # 2> ns3modulegen.log

    features = []
    for (name, caption, was_enabled, reason_not_enabled) in env['NS3_OPTIONAL_FEATURES']:
        if was_enabled:
            features.append(name)

    bindgen = bld(features='command', source=source, target=target, command=argv)
    bindgen.env['FEATURES'] = ','.join(features)
    bindgen.dep_vars = ['FEATURES', "GCC_RTTI_ABI_COMPLETE"]
    bindgen.before = ['cxxprogram', 'cxxshlib', 'cxxstlib']
    bindgen.after = 'gen_ns3_module_header'
    bindgen.name = "pybindgen(ns3 module %s)" % module
    bindgen.module = module
    bindgen.install_path = None

    # generate the extension module
    pymod = bld(features='cxx cxxshlib pyext')
    pymod.source = ['bindings/ns3module.cc']
    pymod.target = '%s/%s' % (module_target_dir, extension_name)
    pymod.name = 'ns3module_%s' % module
    pymod.module = module
    pymod.use = ["%s" % mod for mod in pymod.env['NS3_ENABLED_CONTRIBUTED_MODULES']]
    if pymod.env['ENABLE_STATIC_NS3']:
        if sys.platform == 'darwin':
            pymod.env.append_value('LINKFLAGS', '-Wl,-all_load')
            for mod in pymod.usel:
                #mod = mod.split("--lib")[0]
                pymod.env.append_value('LINKFLAGS', '-l' + mod)
        else:
            pymod.env.append_value('LINKFLAGS', '-Wl,--whole-archive,-Bstatic')
            for mod in pymod.use:
                #mod = mod.split("--lib")[0]
                pymod.env.append_value('LINKFLAGS', '-l' + mod)
            pymod.env.append_value('LINKFLAGS', '-Wl,-Bdynamic,--no-whole-archive')
    defines = list(pymod.env['DEFINES'])
    defines.extend(['NS_DEPRECATED=', 'NS3_DEPRECATED_H'])
    defines.extend(['NS_DEPRECATED_3_35=', 'NS3_DEPRECATED_H'])
    defines.extend(['NS_DEPRECATED_3_34=', 'NS3_DEPRECATED_H'])
    if Utils.unversioned_sys_platform() == 'win32':
        try:
            defines.remove('_DEBUG') # causes undefined symbols on win32
        except ValueError:
            pass
    pymod.env['DEFINES'] = defines
    # The following string should lead to includes of 
    # '-I.', '-Isrc/core/bindings' when compiling module_helpers.cc
    pymod.includes = Context.out_dir + ' ' + Context.out_dir + '/src/core/bindings'
    pymod.install_path = '${PYTHONARCHDIR}/ns'

    # Workaround to a WAF bug, remove this when ns-3 upgrades to WAF > 1.6.10
    # https://www.nsnam.org/bugzilla/show_bug.cgi?id=1335
    # http://code.google.com/p/waf/issues/detail?id=1098
    if Utils.unversioned_sys_platform() == 'darwin':
        pymod.mac_bundle = True

    return pymod

def build(bld):
    bld.create_ns3_module = types.MethodType(create_ns3_module, bld)
    bld.create_ns3_module_test_library = types.MethodType(create_ns3_module_test_library, bld)
    bld.create_obj = types.MethodType(create_obj, bld)
    bld.ns3_python_bindings = types.MethodType(ns3_python_bindings, bld)
    
    all_contrib_modules = find_contrib_modules(bld)

    # Remove these modules from the list of all modules.
    for not_built in bld.env['MODULES_NOT_BUILT']:

        if not_built in all_contrib_modules:
            all_contrib_modules.remove(not_built)

    bld.recurse(list(all_contrib_modules))

    for module in all_contrib_modules:
        modheader = bld(features='ns3moduleheader')
        modheader.module = module.split('/')[-1]
