##############################################################################
# Copyright 2004-2008, Geoffrey Irving, Frank Losasso, Andrew Selle.
# This file is party of PhysBAM whose distribution is governed by the license contained in the accompanying file PHYSBAM_COPYRIGHT.txt.
##############################################################################
import sys
import os
import glob
#import commands
import re
import functools

import SCons

### Check SCons version for qt -> qt3 change
sc_vers = Environment()._get_major_minor_revision(SCons.__version__)
qt_name = 'qt3' if sc_vers >= (4, 5) else 'qt'

### variables and base environment
variables=Variables('SConstruct.options')
variables.AddVariables(
    ('CXX','C++ compiler','g++'),
    ('YACC','which grammar parser to use','yacc'),
    ('LEX','which lexer to use','lex'),
    EnumVariable('PLATFORM','Architecture (e.g. linux, osx)','linux',allowed_values=('linux','osx')),
    EnumVariable('COMPILER_TYPE','(gcc,clang)','gcc',allowed_values=('gcc','clang')),
    EnumVariable('TYPE','Type of build (release, debug, profile, optdebug)','release',allowed_values=('release','debug','profile','optdebug')),
    ('cache','Cache directory to use',''),
    BoolVariable('SHARED','Build shared libraries',1),
    BoolVariable('USE_LEX_YACC','Use lex and yacc to allow parsing for symbolics',0),
    BoolVariable('USE_SYMBOLIC','Use symbolics',0),
    BoolVariable('INSTALL_PROGRAMS','install programs into source directories',1),
    BoolVariable('USE_RPATH','use rpaths for dynamic libraries',1),
    ('CXXFLAGS_EXTRA','',[]),
    ('LINKFLAGS_EXTRA','',[]),
    ('CPPPATH_EXTRA','',[]),
    ('LIBPATH_EXTRA','',[]),
    ('RPATH_EXTRA','',[]),
    ('LIBS_EXTRA','',[]),
    ('GITIGNORE_BIN','',[]))

variables.Add('QT_BINPATH','location for Qt binaries','')
variables.Add('QT_LIB','Qt libraries',[]),
variables.Add('QT_CPPPATH','Qt include path',[]),

### Choose libraries
external_libraries={
    'zlib': {'enable':1,'libs':['z']},
    'libjpeg': {'enable':1,'defines':['USE_LIBJPEG'],'libs':['jpeg']},
    'libpng': {'enable':1,'defines':['USE_LIBPNG'],'libs':['png']},
    'fftw': {'enable':1,'defines':['USE_FFTW'],'libs':['fftw3f','fftw3']},
    'lapack': {'enable':1,'defines':['USE_LAPACK'],'libs':['lapack','lapacke','blas']},
    'mkl': {'defines':['USE_MKL'],'libs':['mkl_rt']},
    'partio': {'defines':['USE_PARTIO'],'libs':['partio']},
    'qt': {'enable':0,'defines':['USE_QT'],'libs':['GL']},
    'qhull': {'enable':0,'defines':['USE_QHULL'],'libs':["qhullcpp","qhullstatic_r"]},
    'openmp': {'enable':1,'defines':['USE_OPENMP'],'flags':['-fopenmp'],'linkflags':['-fopenmp']},
    'openvdb': {'defines':['USE_OPENVDB'],'libs':['openvdb','tbb','Half']},
    'cnpy': {'defines':['USE_CNPY'],'libs':['cnpy']},
    'python': {'enable':0,'includes':['/usr/include/python2.7','/usr/local/lib/python3.8/dist-packages/numpy/core/include']},
    'torch':{'defines':['USE_TORCH'],'libs':['c10','torch_cpu','cudart','nvinfer','nvonnxparser']},
    'tracy':{'defines':['TRACY_ENABLE'],'libs':['dl','pthread']}    
}

lib_keys={
    'enable':(0,"definitions"),
    'flags':([],"compiler flags"),
    'defines':([],"compiler defines"),
    'includes':([],"include paths"),
    'linkflags':([],"linker flags"),
    'libpath':([],"link paths"),
    'libs':([],"libraries"),
    'rpath':([],"dynamic library path")}

for name,lib in external_libraries.items():
    for key,value in lib_keys.items():
        lib.setdefault(key,value[0])
        variables.Add(name+'_'+key,value[1]+' for '+name,lib[key])
variables.Add("nvcc_gcc","compiler to use for nvcc","g++")
variables.Add("qt_tool","SCons tool name for qt",qt_name)

### parse variables
env=Environment(variables=variables,tools=['default','lex','yacc'])
env.Replace(ENV=os.environ) # do this here to allow SConstruct.options to change environment
Help(variables.GenerateHelpText(env))
variant_build=os.path.join('build',env['TYPE'])

build_directory=Dir('.').srcnode().abspath

for name,lib in external_libraries.items():
    for key in lib_keys.keys():
        lib[key]=env[name+'_'+key]

def object_builder(env):
    if env['SHARED']: return env.SharedObject
    else: return env.StaticObject

def library_builder(env):
    if env['SHARED']: return env.SharedLibrary
    else: return env.StaticLibrary

### improve performance
if 'Decider' in dir(env): # if Decider exists, use it to avoid deprecation warnings
    env.Decider('MD5-timestamp')
else:
    env.TargetSignatures('build')
env.SetOption('max_drift',100)
env.SetDefault(CPPDEFINES=[])

### avoid annoying bug in previous versions of scons
env.EnsureSConsVersion(0,96,92)

### avoid deprecation warnings about env.Copy
if 'AddMethod' in dir(env) and 'Clone' in dir(env):
    AddMethod(Environment,Environment.Clone,'Copy')

## override platform specific library names
if env['PLATFORM']=='osx':
    opengl=external_libraries['OpenGL']
    opengl['linkflags']='-framework OpenGL -framework GLUT'
    opengl['libpath']=[]
    opengl['libs']=[]
    lapack=external_libraries['lapack']
    lapack['linkflags']='-framework Accelerate'
    lapack['libpath']=[]
    lapack['libs']=['openblas']
    env.Replace(LDMODULESUFFIX='.so')
    env.Replace(RPATH=[])
    env.Append(SHLINKFLAGS = ['-install_name',build_directory+'/$TARGET'])
else:
    if env['SHARED'] and False:
        if env['USE_RPATH']==0: env.Replace(RPATH=[])
    else: env.Append(LINKFLAGS='-rdynamic')

if env["COMPILER_TYPE"]=="clang":
    external_libraries['openmp']['enable']=False

use_qt=False
if external_libraries['qt']['enable']:
    use_qt=True

env.Append(LIBS=['tbb'])
for name,lib in external_libraries.items():
    if lib['enable']:
        env.Append(CXXFLAGS=lib['flags'])
        env.Append(LINKFLAGS=lib['linkflags'])
        env.Append(CPPPATH=lib['includes'])
        env.Append(LIBPATH=lib['libpath'])
        env.Append(RPATH=lib['rpath'])
        env.Append(LIBS=lib['libs'])
        env.Append(CPPDEFINES=lib['defines'])

### build cache
if env['cache']!='': CacheDir(env['cache'])

if env['PLATFORM']!='osx': env.Append(RPATH=os.path.join(build_directory,variant_build,'Public_Library'))
program_suffix=''
if env['TYPE']!='release': program_suffix+='_'+env['TYPE']
library_suffix=''
if not env['SHARED']: library_suffix='_static'
env.Append(LIBPATH=os.path.join('#'+variant_build,'Public_Library'))

### extra flag options
env.Append(CXXFLAGS=env['CXXFLAGS_EXTRA'])
env.Append(LINKFLAGS=env['LINKFLAGS_EXTRA'])
env.Append(CPPPATH=env['CPPPATH_EXTRA'])
env.Append(LIBPATH=env['LIBPATH_EXTRA'])
env.Append(RPATH=env['RPATH_EXTRA'])
env.Append(LIBS=env['LIBS_EXTRA'])

# Set compile flags
env.Append(CXXFLAGS=[
    '-march=native','-g3','-std=c++20','-Wall','-Winit-self',
    '-Woverloaded-virtual','-Wstrict-aliasing=2','-Wno-unknown-pragmas',
    '-Wno-strict-overflow','-Wno-sign-compare','-Wno-register',
    '-Wno-unused-local-typedefs', '-Wno-deprecated-declarations',
    '-Wno-stringop-overread', '-Wno-ignored-attributes',
    '-Wno-deprecated-enum-enum-conversion','-Wno-array-bounds',
    ])



env.Append(LINKFLAGS=['-g','-gdwarf-2'])

if env["COMPILER_TYPE"]=="gcc":
    env.Append(CXXFLAGS=['-Wno-int-in-bool-context']);
if env["COMPILER_TYPE"]=="clang":
    env.Append(CXXFLAGS=['-Wno-c99-designator', '-Wno-ambiguous-reversed-operator', '-Wno-unused-function', '-Wno-unused-lambda-capture']);

if env['TYPE']=='release' or env['TYPE']=='optdebug' or env['TYPE']=='profile':
    env.Append(CXXFLAGS=['-O3','-funroll-loops','-fno-math-errno','-fno-signed-zeros'])
    env.Append(CPPDEFINES=['NDEBUG'])

if env['TYPE']=='profile':
    env.Append(CXXFLAGS=['-pg'],LINKFLAGS=['-pg'])

if env['USE_LEX_YACC']: env.Append(CPPDEFINES=['USE_LEX_YACC'])
if env['USE_SYMBOLIC']: env.Append(CPPDEFINES=['USE_SYMBOLIC'])

### library configuration
env.Append(CPPPATH=['#/Public_Library'])

if env['PLATFORM']=='osx':
    env.Append(LINKFLAGS=['-undefined','dynamic_lookup','-bind_at_load'])

rpath_save=env['RPATH']
if env['INSTALL_PROGRAMS'] and env['SHARED'] and False: env.Replace(RPATH=[])

pb={}
pb_name={}


### find SConscript files two levels down (for Projects and Tools)
def Find_SConscripts(env,dir):
    for g in ['SConscript','*/SConscript','*/*/SConscript']:
        for c in glob.glob(os.path.join(dir,g)):
            v=os.path.join(variant_build,os.path.dirname(c))
            env.SConscript(c, variant_dir=v, duplicate=0)

### find all .cpp files below current directory
def Find_Sources(dirs):
    sources=[]
    build_directory=Dir('.').srcnode().abspath
    for d in dirs:
        for root,_,files in os.walk(os.path.join(build_directory,d)):
            r=os.path.relpath(root,build_directory)
            for f in files:
                if f.endswith('.cpp'):
                    sources.append(os.path.join(r,f))
    return sources

def Automatic_Objects(env,sources):    
    list=[]
    for s in sources:
        if type(s)==type(""):
            list.append(object_builder(env)(s))
        else:
            list.append(s)
    return list
            
### automatic generation of library targets
def Automatic_Library(env,name,dirs=['.'],extra_objects=[]):
    objects=Automatic_Objects(env,Find_Sources(dirs))+extra_objects
    return library_builder(env)(name+library_suffix,source=objects)

### automatic generation of program targets
def Automatic_Program(l_env,name,sources,level):
    objects=Automatic_Objects(l_env,sources)
    pb_libs=[pb_name[l] for l in level]
    install_name=os.path.basename(name)+program_suffix
    executable_target_path=os.path.join(Dir('.').srcnode().abspath,install_name)
    program=env.Program(name,objects,LIBS=l_env['LIBS']+pb_libs)
    env.Append(GITIGNORE_BIN=executable_target_path)                                    # add executable to VCS exclusion list
    ### Mac OS X does not support -rdynamic
    if l_env['INSTALL_PROGRAMS']:
        l_env.InstallAs(executable_target_path,program)

### magic VCS exclusion file generation (preserves original file contents)
def Write_Exclude(files, target='#.git/info/exclude'):                                  # default for git
    exclude_file = str(File(target))
    if not os.path.exists(exclude_file):                                                # no-op if not in git repo
        return
    
    padding_count=2
    header='# BEGIN KITSUNE MAGIC'
    footer='# END KITSUNE MAGIC'
    act_header=f'{header} (Changes in this section will be overwritten)\n'
    act_footer=f'{footer}\n'

    proj_dir = os.getcwd()
    act_files = [os.path.relpath(f, start=proj_dir) for f in files]
    act_files.sort()

    with open(exclude_file, 'r') as exf:                                                # open original exclude file for reading
        found = False
        update = False
        while True:                                                                     # check if file needs updating by reading section
            line = exf.readline()
            if not line:
                if not found:
                    update = True
                elif index != len(act_files):
                    update = True
                break
            if not found:
                if line.startswith(header):
                    found = True
                    index = 0
            else:                                                                       # start line by line comparison
                if line.startswith(footer):
                    if index != len(act_files):
                        update = True
                    break
                if index >= len(act_files) or act_files[index] != line.strip():         # compare line with known target list
                    update = True
                    break
                index += 1
        if update:
            print(f'Writing exclusion file: {exclude_file}')
            tmp_exclude_file = str(File(f'{target}_tmp'))
            exf.seek(0)
            found = False
            nl_count = 0
            with open(tmp_exclude_file, 'w') as tmp_exf:                                # open temporary file for writing
                while True:
                    line = exf.readline()
                    is_header = line.startswith(header)
                    if is_header or (not found and not line):                           # write list here
                        found = True
                        for i in range(max(0, padding_count-nl_count)):                 # blank line padding
                            tmp_exf.write('\n')
                        tmp_exf.write(act_header)
                        for f in act_files:                                             # write file list
                            tmp_exf.write(f)
                            tmp_exf.write('\n')
                        tmp_exf.write(act_footer)
                    if is_header:                                                       # skip section in original file
                        while not line.startswith(footer):
                            line = exf.readline()
                        line = exf.readline()
                        for i in range(padding_count):                                  # blank line padding
                            if line == '\n':
                                line = exf.readline()
                            elif not line:
                                break
                            tmp_exf.write('\n')
                    if not line:
                        break
                    if line == '\n':                                                    # keep a blank line count
                        nl_count += 1
                    else:
                        nl_count = 0
                    tmp_exf.write(line)                                                 # copy line to new file
                tmp_exf.flush()
                os.fsync(tmp_exf.fileno())                                              # sync to filesystem for copy
    if update:
        os.replace(tmp_exclude_file, exclude_file)                      # replace original file with copy (atomic operation)
        print(f'Exclusion file written.')
    return


### build everything
Export('env Automatic_Program Automatic_Library Find_Sources pb pb_name use_qt program_suffix Automatic_Objects pb_name')
Find_SConscripts(env,'Public_Library')
Find_SConscripts(env,'Projects')
Find_SConscripts(env,'Tests')
Find_SConscripts(env,'Tools')

### write VCS exclusion file
Write_Exclude(env['GITIGNORE_BIN'])
