import sys
import os
import imp
from glob import glob

#######################################################
# reusable functions and data structures
#######################################################
def LoadTool(name, env, **kw):
    config_path = GetBuildPath('#/Build/Tools/SCons')
    file, path, desc = imp.find_module(name, [config_path])
    module = imp.load_module(name, file, path, desc)
    module.generate(env, **kw)

def MergeListUnique(item_list, items):
    for item in items: 
        if not item in item_list: item_list.append(item)

def MergeItemUnique(item_list, item):
    if not item in item_list: item_list.append(item)
            
def GlobSources(drct, patterns, excluded_files=[]):
    root = GetBuildPath(GetDirPath(drct))
    files = []
    for pattern in Split(patterns):
        files += glob(root+'/'+pattern)
    return [drct+'/'+os.path.basename(x) for x in  files if os.path.basename(x) not in excluded_files]

def GetDirPath(drct):
    for key in SourceDirLookup.keys():
        if drct.startswith(key):
            return SourceDirLookup[key]+drct[len(key):]
    return '#/'+drct
    
def DeclareBuildDir(env, drct):
    env.BuildDir(drct, GetDirPath(drct), duplicate=0)
    
def GetIncludeDirs(modules, exclude=None):
    dirs = []
    for module in Split(modules):
        if Modules.has_key(module) and not module == exclude:
            MergeListUnique(dirs, Modules[module].GetIncludeDirs())
        else:
            MergeItemUnique(dirs, GetDirPath(module))
    return dirs
    
def GetLibraries(modules, modules_only=False):
    libs = []
    for module in Split(modules):
        if module in libs: continue
        if Modules.has_key(module):
            dep_libs = Modules[module].GetLibraries()
            libs = [lib for lib in libs if not lib in dep_libs]+dep_libs
        elif not modules_only:
            libs.append(module)
    return libs

def GetLibraryDirs(modules):
    lib_dirs = []
    for module in modules:
        if Modules.has_key(module):
            MergeListUnique(lib_dirs, Modules[module].GetLibraryDirs())
    return lib_dirs

Modules = {}
class Module:
    def __init__(self, name, 
                 included_modules = [], 
                 linked_modules   = [],
                 lib_dirs         = []):
        self.name             = name
        self.included_modules = included_modules
        self.linked_modules   = linked_modules
        self.lib_dirs         = [GetDirPath(dir) for dir in lib_dirs]
        self.product          = []
        Modules[name]         = self        
        
    def GetLibraries(self):
        return self.product+GetLibraries(self.linked_modules)
        
    def GetLibraryDirs(self):
        return self.lib_dirs+GetLibraryDirs(self.linked_modules)

    def GetIncludeDirs(self):
        return GetIncludeDirs(self.included_modules, self.name)

    
class BuildModule(Module):
    def __init__(self, name, 
                 build_source_dirs     = ['.'], 
                 build_source_files    = {},
                 source_root           = 'Source',
                 build_source_pattern  = ['*.c', '*.cpp'], 
                 build_include_dirs    = [], 
                 included_modules      = [], 
                 included_only_modules = [],
                 linked_modules        = [],
                 environment           = None,
                 excluded_files        = [],
                 extra_cpp_defines     = {},
                 extra_lib_dirs        = [],
                 type                  = 'StaticLibrary') :

        # store this new object in the module dictionary
        build_source_dirs = [source_root+'/'+drct for drct in build_source_dirs]
        Module.__init__(self, 
                        name, 
                        Split(included_modules)+Split(included_only_modules)+Split(build_source_dirs), 
                        Split(linked_modules)+Split(included_modules),
                        extra_lib_dirs)
        self.build_include_dirs = build_include_dirs
        self.type               = type

        # setup the environment        
        self.env = environment and environment.Clone() or env.Clone()
        if extra_cpp_defines: self.env.Append(CPPDEFINES = extra_cpp_defines.items())
        
        # for each source drct to build, create a BuildDir
        # to say where we want the object files to be built,
        # and compute the list of source files to build
        sources = []
        for drct in Split(build_source_dirs):
            DeclareBuildDir(self.env, drct)
            sources += GlobSources(drct, build_source_pattern, excluded_files)
            
        # add cherry-picked files
        for drct in build_source_files.keys():
            pattern = build_source_files[drct]
            if drct.startswith('/'):
                drct_path = drct[1:]
            else:
                drct_path = source_root+'/'+drct
            DeclareBuildDir(self.env, drct_path)
            sources += GlobSources(drct_path, pattern)

        # calculate our build include path
        cpp_path = GetIncludeDirs(Split(self.build_include_dirs) + Split(build_source_dirs) + self.included_modules + self.linked_modules)

        # check that the source list is not empty
        if len(sources) == 0:
            raise 'Module '+name+' has no sources'
        
        # create the module's product
        self.env.AppendUnique(CPPPATH=cpp_path)
        self.env.AppendUnique(LIBS=GetLibraries(Split(linked_modules)))
        if type == 'SharedLibrary':
            self.product = self.env.SharedLibrary(target=name, source=sources)
            self.env.Alias(name, self.product)
        elif type == 'StaticLibrary':
            self.product = self.env.Library(target=name, source=sources)
            self.env.Alias(name, self.product)
        elif type == 'Executable':
            # libraries    
            libs = GetLibraries(linked_modules)
            lib_path = GetLibraryDirs(linked_modules)
            self.env.AppendUnique(LIBS=libs, LIBPATH=lib_path)
            self.product = self.env.Program(target=name.lower(), source=sources)
            self.env.Alias(name, self.product)    
        

#######################################################
# Main Build
#######################################################
Import("env")

SourceDirLookup = {}

### try to read in any target specific configuration
target_config_file = env.GetBuildPath('#/Build/Targets/'+env['target']+'/Config.scons')
if os.path.exists(target_config_file):
    # Load the target-specific config file    
    execfile(target_config_file)

#######################################################
# modules
#
# Usage:
#
# The LibraryModule() function declares a code module
# The included_modules parameter is a list of all the modules and/or directories
# that will be added to the include path when building this module AND to
# the include path of any other module that depends on this one.
# The linked_modules parameter is a list of all the modules and/or directories
# that are necessary to build this module. These modules will be added to
# the include path of this module, but not to that of the modules that depend
# on this module. The modules that depend no this module, however, will 
# automatically link with the linked_modules. 
# Note that the included_modules list is automatically added to the 
# linked_modules list, so that you do not need to list in linked_modules
# the modules that are already listed in included_modules.
# If a module needs to export an include path to its dependents that
# is not a module that the dependent can link with (ex: an include dir),
# list it in the included_only_modules.
# To summarize: included_modules should list all the modules that users
# of the public interface should depend on; linked_modules should list
# all the modules not listed in included_modules that are used by the
# module's implementation only.
#######################################################

OptionalModules = []

############################ Bento4
AP4_HOME= os.environ.has_key('BENTO4_HOME') and os.environ['BENTO4_HOME'] or '#/../Bento4'

# check if Bento4 is available
if os.path.exists(AP4_HOME):
    OptionalModules.append('MeloDecode')
    AP4_SOURCE_ROOT = 'Bento4/Source/C++'
    SourceDirLookup[AP4_SOURCE_ROOT] = AP4_HOME+'/Source/C++'  
    if env.has_key('AP4_CPP_DEFINES'):
        AP4_EXTRA_CPP_DEFINES = env['AP4_CPP_DEFINES']
    else:
        AP4_EXTRA_CPP_DEFINES = {}
    if env['build_config'] == 'Debug':
        AP4_EXTRA_CPP_DEFINES['AP4_DEBUG'] = None
    
    BuildModule(name                  = 'Bento4',
                build_source_dirs     = ['Core', 'MetaData', 'Crypto'],
                build_source_files    = env['AP4_SYSTEM_SOURCES'],
                included_only_modules = [AP4_SOURCE_ROOT+'/'+dir for dir in ['Config', 'Core', 'MetaData', 'Crypto']],
                source_root           = AP4_SOURCE_ROOT,
                extra_cpp_defines     = AP4_EXTRA_CPP_DEFINES)
else:
    print "*** Bento4 external package not available, the MeloDecode test program will not be compiled"
    
############################ Melo
if env.has_key('MLO_CPP_DEFINES'):
    MLO_EXTRA_CPP_DEFINES = env['MLO_CPP_DEFINES'] 
else:
    MLO_EXTRA_CPP_DEFINES = {}

if env['build_config'] == 'Debug':
    MLO_EXTRA_CPP_DEFINES['MLO_DEBUG'] = None

BuildModule(name              = 'Melo',
            build_source_dirs = ['.'],
            extra_cpp_defines = MLO_EXTRA_CPP_DEFINES)

############################# MeloDecode
if 'MeloDecode' in OptionalModules:
    BuildModule(name               = 'MeloDecode',
                type               = 'Executable',
                source_root        = 'Apps',
                build_source_dirs  = ['MeloDecode'],
                build_include_dirs = ['Source'],
                linked_modules     = ['Melo', 'Bento4'])

############################# SDK


################################################
# Default Build Modues
################################################
env.Default(['Melo'] + OptionalModules)
