# this python/scons script implements Agency's build logic
# it may make the most sense to read this file beginning
# at the bottom and proceeding towards the top

import os


def create_a_program_for_each_source_in_the_current_directory(env):
  """Collects all source files in the current directory and creates a program from each of them.
  Returns the list of all such programs created.
  """
  sources = []
  directories = ['.']
  extensions = ['.cpp', '.cu']
  
  for dir in directories:
    for ext in extensions:
      regex = os.path.join(dir, '*' + ext)
      sources.extend(env.Glob(regex))

  programs = []
  for src in sources:
    # env.Program() always returns a list of targets
    # but an executable program always has a single target,
    # so collect the first element of the list
    program = env.Program(src)[0]
    programs.append(program)

  return programs


def create_an_alias_to_execute_programs_as_unit_tests(env, programs, run_programs_command):
  """Creates an alias with a name given by run_programs_command which runs each program in programs after it is built"""
  relative_path_from_root = env.Dir('.').path

  # XXX WAR an issue where env.Dir('.').path does not return a relative path for the root directory
  root_abspath = os.path.dirname(os.path.realpath("__file__"))
  if relative_path_from_root == root_abspath:
    relative_path_from_root = '.'

  # elide '.'
  if relative_path_from_root == '.':
    relative_path_from_root = ''
  alias_name = os.path.join(relative_path_from_root, run_programs_command)

  program_absolute_paths = [p.abspath for p in programs]
  alias = env.Alias(alias_name, programs, program_absolute_paths)
  env.AlwaysBuild(alias)
  return [alias]


# this is the function each SConscript in the directory tree calls
# we will add it as a method to the SCons environment that subsidiary SConscripts import
def RecursivelyCreateProgramsAndUnitTestAliases(env):
  # create a program for each source found in the current directory
  programs = create_a_program_for_each_source_in_the_current_directory(env)

  # recurse into all SConscripts in immediate child directories and add their programs to our collection 
  
  # we either receive a list of programs or a list of list of programs
  # when there are multiple child directories, this returns a list of lists of programs
  # when there are 1 or 0 child directories, this returns a list of programs
  programs_of_each_child = env.SConscript(env.Glob('*/SConscript'), exports='env')
  try:
    for child_programs in programs_of_each_child:
      programs.extend(child_programs)
  except:
    programs.extend(programs_of_each_child)
  
  # create unit tests for these programs and run them when "run_examples" is given as a scons command line option
  create_an_alias_to_execute_programs_as_unit_tests(env, programs, 'run_examples')

  return programs
  

# this function takes a SCons environment and specifies some compiler flags to use
def apply_compiler_flags(env):
  # a dictionary mapping compiler features to the list of compiler switches implementing them
  gnu_compiler_flags = {
    'warnings' : {
      'all' : '-Wall',
      'extra' : '-Wextra'
    },

    'warnings_as_errors' : '-Werror'
  }

  clang_compiler_flags = {
    'warnings' : {

      # XXX with clang, nvcc generates -Wunused-local-typedefs warnings due to nvbug 1890561
      #     eliminate this workaround once 1890561 is resolved
      # XXX with clang, nvcc generates -Wunused-private-field warnings due to nvbug 1890717
      #     eliminate this workaround once 1890717 is resolved
      'all' : '-Wall -Wno-unused-local-typedef -Wno-unused-private-field', 
                                                 

      # -Wmismatched-tags produces warnings we cannot eliminate, so don't enable it
      # XXX with clang, nvcc generates -Wunused-parameter warnings due to nvbug 1889862
      #     eliminate this workaround once 1889862 is resolved
      'extra' : '-Wextra -Wno-mismatched-tags -Wno-unused-parameter'
    },

    'warnings_as_errors' : '-Werror'
  }

  all_compiler_flags = {}
  all_compiler_flags['g++'] = gnu_compiler_flags
  all_compiler_flags['clang'] = clang_compiler_flags

  # chop off any version suffix from C++ compiler name
  compiler_name = env['CXX'].split('-')[0]

  this_compilers_flags = all_compiler_flags[compiler_name]

  # get all the c++ compiler flags for the warnings enabled
  cxx_warning_flags = [this_compilers_flags['warnings'][key] for key in env['warnings']]

  if env['warnings_as_errors']:
    cxx_warning_flags.append(this_compilers_flags['warnings_as_errors'])

  # first, general C++ flags
  env.MergeFlags(['-O3', '-std=c++11', '-lstdc++', '-lpthread'] + cxx_warning_flags)
  
  # next, flags for nvcc
  env.MergeFlags(['--expt-extended-lambda', '-arch=' + str(env['arch'])])


# script execution begins here

# set up some variables we can control from the command line
vars = Variables()
vars.Add('CXX', 'C++ compiler', 'clang')
vars.Add('CPPPATH', 'Agency include path', Dir('..'))
vars.Add(ListVariable('arch', 'Compute capability code generation', 'sm_52',
                      ['sm_30', 'sm_32', 'sm_35', 'sm_37',
                       'sm_50', 'sm_52',
                       'sm_60']))
vars.Add(ListVariable('warnings', 'Compiler warning options', 'all',
                      ['all', 'extra']))
vars.Add(BoolVariable('warnings_as_errors', 'Treat warnings as errors', True))

# create a SCons build environment
env = Environment(variables = vars, tools = ['default', 'nvcc-scons/nvcc'])

apply_compiler_flags(env)

# add our custom shorthand methods for subsidiary SConscripts' use
env.AddMethod(RecursivelyCreateProgramsAndUnitTestAliases)

# call this directory's SConscript
env.SConscript('./SConscript', exports = 'env')

