#!/bin/bash

###### Usage ##################################################################
# To use the bld build system in a bash script, some setup is required:
# 1. Define paths:
#     root_path, opts_path, bld_path, local_path, src_path,
#     build_path, fuzz_path, program_pather
# 2. Define local variables:
#     compiler, compile_mode, arch, linker, ctx_opts
# 3. Then include this core file with "." prefix:
#     . $bld_path/bld_core.sh
#
#
# The following bld functions form the core "commands":
#   bld_compile, bld_link, bld_lib, bld_unit
#
#  And there are several more helper functions:
#   bld_print_implicit_opts, bld_load_local_opts
#
#
# Signatures:
#   bld_compile <source-file> <zero-or-more-options>
#   bld_link <out-name> <one-or-more-source-files-objs-or-libs> -- \
#            <zero-or-more-options>
#   bld_lib <out-name> <one-or-more-source-files-or-objs> -- \
#           <zero-or-more-options>
#   bld_unit <source-file> <zero-or-more-options>
#
#   bld_print_implicit_opts (no arguments)
#   bld_load_local_opts (no arguments)
#
#
# Shared notes for all:
# + Options are gathered from the arguments, from the local context, and
#   in the case of the commands bld_compile and bld_unit, from the source file.
# + Options gathered from local context are prefixed with their context
#   variable like so [var:val]. So for example, on windows the options list
#   includes [os:windows].
# + Options in source files are specified between the strings //$ and //.
# + Object file extensions should be named based on whether they were
#   generated by a compiler or assembler. Compiled objects should have the
#   extension .cmp:o and assembled objects should have the extension
#   .asm:o. The system automatically changes the name to match the correct
#   extension given the context.
# + Static library files should have the extension .lib. The system
#   automatically changes the name to match the correct extension given the
#   context.
# + When the option "diagnostics" is included, extra details will be printed
#   from the bld commands. Showing the full list of options, invokation lines,
#   and the build path.
#
# bld_compile:
# + Creates an object file from a single source file via the selected compiler.
#
# bld_link:
# + Creates executables and shared binaries from source files, object files,
#   and static libraries. First uses the bld_compile command on the source
#   files.
# + Uses the selected linker.
# + If the short name for the linker does not work for any reason, the
#   full path to the linker may be specified in a filter file pointed to by
#   the setup variable `program_pather`
# + The output is a shared binary (.dll or .so) when the option "dll" is
#   included.
#
# bld_lib:
# + Creates a static library from source files and object files. First uses
#   the bld_compile command on the source files. Uses the OS to determine the
#   correct archiver.
#
# bld_unit:
# + Creates an executable (or shared binary) from a single source file.
#   This command essentially does a single bld_compile then bld_link. With
#   two differences:
#   1. The options from the source file are visible to the bld_link.
#   2. The name of the executable is determined either from the first option 
#      in the list or from the source file name if there are no options.
#
# bld_print_implicit_opts:
# + Shows the implicit options loaded from the local parameters script.
#
# bld_load_local_opts:
# + It is possible and sometimes useful to modify the local parameters after
#   they are loaded to create certain kinds of build scripts. This function
#   resets the local parameters to their original states by rerunning the
#   local parameters script.


###### Flags From Opts ########################################################

function bld_flags_from_opts {
  ###### parse arguments ####################################################
  local in_file=$1
  local opts=()
  for ((i=2; i<=$#; i+=1)); do
    opts+=(${!i})
  done
  
  ###### load file ##########################################################
  local flags_raw=()
  IFS=$'\r\n' GLOBIGNORE='*' command eval 'flags_raw=($(cat $in_file))'
  
  ###### filter #############################################################
  local flags=()
  for ((i=0;i<${#flags_raw[@]};i+=1)); do
    local flag=${flags_raw[i]}
    
    ###### skip blanks and comments #######################################
    if [[ -z "${flag// }" ]]; then
      continue
    fi
    if [[ "${flag:0:1}" == "#" ]]; then
      continue
    fi
    
    ###### parse line filters #############################################
    local line_filters=()
    while [[ $flag = *">"* ]]; do
      line_filters+=("${flag%%>*}")
      flag="${flag#*>}"
    done
    
    ###### check filters ##################################################
    local can_include=1
    for ((j=0;j<${#line_filters[@]};j+=1)); do
      can_include=0
      for ((k=0;k<${#opts[@]};k+=1)); do
        if [[ ${opts[k]} = ${line_filters[j]} ]]; then
          can_include=1
          break
        fi
      done
      if [[ "$can_include" = "0" ]]; then
        break
      fi
    done
    if [[ "$can_include" = "1" ]]; then
      flags+=("${flag}")
    fi
  done
  
  echo "${flags[@]}"
}

###### Opts From Src ##########################################################

function bld_opts_from_src {
  ###### split file into tokens #############################################
  local in_file=$1
  local tokens=($(grep "//\\$" $in_file))
  
  ###### parse ##############################################################
  local in_params_range="0"
  local params=()
  for ((i=0; i<${#tokens[@]}; i+=1)); do
    local string="${tokens[i]}"
    if [[ "$in_params_range" == "0" ]]; then
      if [[ "$string" == "//$" ]]; then
        in_params_range="1"
      fi
    elif [[ "$in_params_range" == "1" ]]; then
      if [[ "${string:0:2}" == "//" ]]; then
        break
      fi
      params+=($string)
    fi
  done
  
  echo "${params[@]}"
}

###### Dedup ##################################################################

function bld_dedup {
  ###### parse arguments ####################################################
  local in=()
  for ((i=1; i<=$#; i+=1)); do
    in+=(${!i})
  done
  
  ###### dedup ##############################################################
  local out=()
  for ((i=0; i<${#in[@]}; i+=1)); do
    local string=${in[i]}
    local is_dup="0"
    for ((j=0; j<${#out[@]}; j+=1)); do
      if [[ "$string" == "${out[j]}" ]]; then
        is_dup="1"
        break
      fi
    done
    if [[ "$is_dup" == "0" ]]; then
      out+=($string)
    fi
  done
  
  echo "${out[@]}"
}

###### Has Opt ################################################################

function bld_has_opt {
  ###### parse arguments ####################################################
  local key_opt=$1
  local opts=()
  for ((i=2; i<=$#; i+=1)); do
    opts+=(${!i})
  done
  
  ###### scan ###############################################################
  local has_key=0
  for ((i=0;i<${#opts[@]};i+=1)); do
    local opt=${opts[i]}
    if [[ "$opt" == "$key_opt" ]]; then
      has_key=1
      break
    fi
  done
  
  echo $has_key
}

###### Load Local Options #####################################################

function bld_load_local_opts {
  ###### os cracking ########################################################
  os="undefined"
  if [ "$OSTYPE" == "win32" ] ||
     [ "$OSTYPE" == "msys"  ]; then
    os="windows"
  elif [ "$OSTYPE" == "linux-gnu" ]; then
    os="linux"
  elif [ "$OSTYPE" == "darwin" ]; then
    os="mac"
  fi
  ###### load parameters from the local script ################################
  if [ -f "$local_path/local_vars.sh" ]; then
    source "$local_path/local_vars.sh"
  fi
}

###### Implicit Opts ##########################################################

function bld_implicit_opts {
  local linker_opt=""
  if [[ "$linker" == "link" || "$linker" == "lld-link" ]]; then
    linker_opt="link:msvc"
  fi
  echo $ctx_opts cmp:$compiler mode:$compile_mode asm:$assembler
  echo link:$linker $linker_opt os:$os arch:$arch
}

###### Print Implicit Options #################################################

function bld_print_implicit_opts {
  local opts=($(bld_implicit_opts))
  local bracketed=()
  for ((i=0; i<=${#opts[@]}; i+=1)); do
    local opt="${opts[i]}"
    if [ "$opt" != "" ]; then
      bracketed+=("[${opts[i]}]")
    fi
  done
  echo "${bracketed[@]}"
}

###### Print Help #############################################################

function bld_print_obj_note {
  echo "NOTE: the interface does not use standrd object file extensions"
  echo "NOTE:  use 'cmp:o' for object files produced by a compiler"
  echo "NOTE:  use 'asm:o' for object files produced by an assembler"
}

###### Compile ################################################################

function bld_compile {
  local i=0
  
  ###### parse arguments ####################################################
  local in_file=$1
  local opts=()
  for ((i=2; i<=$#; i+=1)); do
      opts+=(${!i})
  done
  if [ "$in_file" == "" ]; then
    echo "ERROR(compile): missing input file"
    return 1
  fi
  
  ###### finish in file #####################################################
  local final_in_file=$in_file
  
  ###### determine build kind ###############################################
  local ext="${final_in_file##*.}"
  local build_kind="undetermined"
  if [[ "$ext" == "c" || "$ext" == "cpp" ]]; then
    build_kind="compile"
  elif [[ "$ext" == "asm" ]]; then
    build_kind="assemble"
  else
    echo "ERROR(compile): unrecgonized source type $final_in_file"
    return 1
  fi
  
  ###### finish options #####################################################
  local src_opts=($(bld_opts_from_src $final_in_file))
  local impl_opts=($(bld_implicit_opts))
  local all_opts=($(bld_dedup ${impl_opts[@]} ${opts[@]} ${src_opts[@]}))
  
  ###### diagnostics ########################################################
  local diagnostics=$(bld_has_opt diagnostics ${all_opts[@]})
    
  ###### out file name ######################################################
  local dot_ext_o=""
  if [[ "$build_kind" == "compile" ]]; then
    dot_ext_o=$(bld_ext_cmpo)
  elif [[ "$build_kind" == "assemble" ]]; then
    dot_ext_o=$(bld_ext_asmo)
  fi
  local file_base=${final_in_file##*/}
  local file_base_no_ext=${file_base%.*}
  local out_file="$file_base_no_ext$dot_ext_o"
  
  ###### get real flags #####################################################
  local flags=""
  if [[ "$build_kind" == "compile" ]]; then
    flags=$(bld_flags_from_opts $opts_path/compiler_flags.txt ${all_opts[@]})
  else
    flags=$(bld_flags_from_opts $opts_path/assembler_flags.txt ${all_opts[@]})
  fi
  
  ###### move to output folder ##############################################
  mkdir -p "$build_path"
  cd $build_path
  if [ "$diagnostics" == "1" ]; then
    echo "build path: $build_path"
  fi
  
  ###### delete existing object file ########################################
  rm -f "$out_file_base.o"
  rm -f "$out_file_base.obj"
  
  ###### final flags ########################################################
  local final_flags=""
  if [[ "$build_kind" == "compile" ]]; then
    final_flags="-c -I$src_path ${flags}"
  elif [[ "$build_kind" == "assemble" ]]; then
    final_flags="-c ${flags}"
  fi
  
  ###### compile ############################################################
  if [[ "$build_kind" == "compile" ]]; then
    if [ "$diagnostics" == "1" ]; then
      echo "cmp $final_in_file -- ${all_opts[@]}"
      echo $compiler "$final_in_file" $final_flags
    fi
    if [ "$compiler" == "clang" ]; then
      echo "$file_base"
    fi
    $compiler "$final_in_file" $final_flags
  elif [[ "$build_kind" == "assemble" ]]; then
    if [ "$diagnostics" == "1" ]; then
      echo "asm $final_in_file -- ${all_opts[@]}"
      echo $assembler $final_flags "$final_in_file"
    fi
    $assembler $final_flags "$final_in_file"
  fi
  
  # return of status from compiler is automatic here.
}

###### Link ###################################################################

function bld_link {
  local i=0
  
  ###### parse arguments ####################################################
  local out_name=$1
  local in_files=()
  for ((i=2; i<=$#; i+=1)); do
    if [ "${!i}" == "--" ]; then
      break
    fi
    in_files+=(${!i})
  done
  local opts=()
  for ((i+=1; i<=$#; i+=1)); do
    opts+=(${!i})
  done
  if [ "$out_name" == "" ]; then
    echo "link: missing output name"
    return 1
  fi
  if [ "${#in_files}" == "0" ]; then
    echo "link: missing input file(s)"
    return 1
  fi
  
  ###### finish options #####################################################
  local impl_opts=($(bld_implicit_opts))
  local all_opts=($(bld_dedup ${opts[@]} ${impl_opts[@]}))
  
  ###### diagnostics ########################################################
  local diagnostics=$(bld_has_opt diagnostics ${all_opts[@]})
  
  ###### sort in files ######################################################
  local in_src=()
  local in_cmpo=()
  local in_asmo=()
  local in_lib=()
  for ((i=0; i<${#in_files[@]}; i+=1)); do
    local file="${in_files[i]}"
    local ext="${file##*.}"
    if [[ "$ext" == "c" || "$ext" == "cpp" || "$ext" == "asm" ]]; then
      in_src+=($file)
    elif [[ "$ext" == "cmp:o" ]]; then
      in_cmpo+=($file)
    elif [[ "$ext" == "asm:o" ]]; then
      in_asmo+=($file)
    elif [[ "$ext" == "lib" ]]; then
      in_lib+=($file)
    else
      echo "WARNING: ignoring unrecgonized file type $file"
      if [[ "$ext" == "obj" || "$ext" == "o" ]]; then
        bld_print_obj_note
      fi
    fi
  done
  
  ###### auto correct compiled object files #################################
  local dot_ext_cmpo=$(bld_ext_cmpo)
  for ((i=0; i<${#in_cmpo[@]}; i+=1)); do
    local file_name="${in_cmpo[i]}"
    local base_name="${file_name%.*}"
    in_cmpo[$i]="$base_name$dot_ext_cmpo"
  done
  
  ###### auto correct assembled object files ################################
  local dot_ext_asmo=$(bld_ext_asmo)
  for ((i=0; i<${#in_asmo[@]}; i+=1)); do
    local file_name="${in_asmo[i]}"
    local base_name="${file_name%.*}"
    in_asmo[$i]="$base_name$dot_ext_asmo"
  done
  
  ###### combine object files ###############################################
  local in_obj=()
  for ((i=0; i<${#in_cmpo[@]}; i+=1)); do
    local file="${in_cmpo[i]}"
    in_obj+=($file)
  done
  for ((i=0; i<${#in_asmo[@]}; i+=1)); do
    local file="${in_asmo[i]}"
    in_obj+=($file)
  done
  
  ###### compile source files ###############################################
  for ((i=0; i<${#in_src[@]}; i+=1)); do
    local file="${in_src[i]}"
    bld_compile "$file" ${all_opts[@]}
    local status=$?
    if [ $status -ne 0 ]; then
      return $status
    fi
  done
  
  ###### intermediate object files ##########################################
  local interm_obj=()
  for ((i=0; i<${#in_src[@]}; i+=1)); do
    local file_name="${in_src[i]}"
    local base_name="${file_name##*/}"
    local base_name_no_ext="${base_name%.*}"
    local ext="${base_name##*.}"
    if [[ "$ext" == "c" || "$ext" == "cpp" ]]; then
      interm_obj+=($base_name_no_ext$dot_ext_cmpo)
    elif [[ "$ext" == "asm" ]]; then
      interm_obj+=($base_name_no_ext$dot_ext_asmo)
    fi
  done
  
  ###### get real flags #####################################################
  local flags=$(bld_flags_from_opts $opts_path/linker_flags.txt ${all_opts[@]})
  
  ###### out file name ######################################################
  local dot_ext_out=""
  local is_dll=$(bld_has_opt dll ${all_opts[@]})
  if [ "$is_dll" == "0" ]; then
    dot_ext_out=$(bld_ext_exe)
  else
    dot_ext_out=$(bld_ext_dll)
  fi
  out_file="$out_name$dot_ext_out"
  
  ###### move to output folder ##############################################
  mkdir -p "$build_path"
  cd $build_path
  if [ "$diagnostics" == "1" ]; then
    echo "build path: $build_path"
  fi
  
  ###### final files to linker ##############################################
  local final_in_files="${interm_obj[@]} ${in_obj[@]} ${in_lib[@]}"
  
  ###### set first diagnostic string ########################################
  local first_diagnostic_string="lnk $final_in_files -- ${all_opts[@]}"
  
  ###### linker executable name #############################################
  local linker_exe=$linker
  local linker_rename=$(bld_flags_from_opts $program_pather "link:$linker")
  if [[ "$linker_rename" != "" ]]; then
    if [ "$diagnostics" == "1" ]; then
      echo "linker rename: $linker_rename"
    fi
    linker_exe=$linker_rename
  fi
  
  ###### link ###############################################################
  local status=0
  local invokation=""
  if [[ "$linker" == "link" || "$linker" == "lld-link" ]]; then
    invokation="-OUT:$out_file $flags $final_in_files"
  elif [ "$linker" == "clang" ]; then
    invokation="-o $out_file $flags $final_in_files"
  else
    echo "ERROR(link): invokation not defined for this linker"
    status=1
  fi
  if [ "$invokation" != "" ]; then
    if [ "$diagnostics" == "1" ]; then
      echo $first_diagnostic_string
      echo $linker_exe $invokation
    fi
    echo "$out_file"
    "$linker_exe" $invokation
    status=$?
  fi
  
  return $status
}

###### Library ################################################################

function bld_lib {
  local i=0
  
  ###### parse arguments ####################################################
  local out_name=$1
  local in_files=()
  for ((i=2; i<=$#; i+=1)); do
    if [ "${!i}" == "--" ]; then
      break
    fi
    in_files+=(${!i})
  done
  local opts=()
  for ((i+=1; i<=$#; i+=1)); do
    opts+=(${!i})
  done
  if [ "$out_name" == "" ]; then
    echo "lib: missing output name"
    return 1
  fi
  if [ "${#in_files}" == "0" ]; then
    echo "lib: missing input file(s)"
    return 1
  fi
  
  ###### finish options #####################################################
  local impl_opts=($(bld_implicit_opts))
  local all_opts=($(bld_dedup ${opts[@]} ${impl_opts[@]}))
  
  ###### diagnostics ########################################################
  local diagnostics=$(bld_has_opt diagnostics ${all_opts[@]})
  
  ###### sort in files ######################################################
  local in_src=()
  local in_cmpo=()
  local in_asmo=()
  for ((i=0; i<${#in_files[@]}; i+=1)); do
    local file="${in_files[i]}"
    local ext="${file##*.}"
    if [[ "$ext" == "c" || "$ext" == "cpp" ]]; then
      in_src+=($file)
    elif [[ "$ext" == "cmp:o" ]]; then
      in_cmpo+=($file)
    elif [[ "$ext" == "asm:o" ]]; then
      in_asmo+=($file)
    else
      echo "WARNING: ignoring unrecgonized file type $file"
      if [[ "$ext" == "obj" || "$ext" == "o" ]]; then
        bld_print_obj_note
      fi
    fi
  done
  
  ###### auto correct compiled object files #################################
  local dot_ext_cmpo=$(bld_ext_cmpo)
  for ((i=0; i<${#in_cmpo[@]}; i+=1)); do
    local file_name="${in_cmpo[i]}"
    local base_name="${file_name%.*}"
    in_cmpo[$i]=$base_name$dot_ext_cmpo
  done
  
  ###### auto correct assembled object files ################################
  local dot_ext_asmo=$(bld_ext_asmo)
  for ((i=0; i<${#in_asmo[@]}; i+=1)); do
    local file_name="${in_asmo[i]}"
    local base_name="${file_name%.*}"
    in_asmo[$i]="$base_name$dot_ext_asmo"
  done
  
  ###### combine object files ###############################################
  local in_obj=()
  for ((i=0; i<${#in_cmpo[@]}; i+=1)); do
    local file="${in_cmpo[i]}"
    in_obj+=($file)
  done
  for ((i=0; i<${#in_asmo[@]}; i+=1)); do
    local file="${in_asmo[i]}"
    in_obj+=($file)
  done
  
  ###### compile source files ###############################################
  for ((i=0; i<${#in_src[@]}; i+=1)); do
    bld_compile "${in_src[i]}" ${all_opts[@]}
    local status=$?
    if [ $status -ne 0 ]; then
      return $status
    fi
  done
  
  ###### intermediate object files ##########################################
  local interm_obj=()
  for ((i=0; i<${#in_src[@]}; i+=1)); do
    local file_name="${in_src[i]}"
    local base_name="${file_name##*/}"
    local base_name_no_ext="${base_name%.*}"
    local ext="${base_name##*.}"
    if [[ "$ext" == "c" || "$ext" == "cpp" ]]; then
      interm_obj+=($base_name_no_ext$dot_ext_cmpo)
    elif [[ "$ext" == "asm" ]]; then
      interm_obj+=($base_name_no_ext$dot_ext_asmo)
    fi
  done
  
  ###### out file name ######################################################
  local out_file=""
  if [ "$os" == "windows" ]; then
    out_file="$out_name.lib"
  elif [ "$os" == "linux" || "$os" == "mac" ]; then
    out_file="lib$out_name.a"
  else
    echo "ERROR(lib): static library output not defined for OS: $os"
  fi
  
  ###### final library build input files ####################################
  local final_in_files="${interm_obj[@]} ${in_obj[@]}"
  
  ###### move to output folder ##############################################
  mkdir -p "$build_path"
  cd $build_path
  if [ "$diagnostics" == "1" ]; then
    echo "build path: $build_path"
  fi
  
  ###### set first diagnostic string ########################################
  local first_diagnostic_string="lib $final_in_files -- ${all_opts[@]}"
  
  ###### build library ######################################################
  local status=0
  if [ "$os" == "windows" ]; then
    if [ "$diagnostics" == "1" ]; then
      echo $first_diagnostic_string
      echo lib -nologo -OUT:"$out_file" $final_in_files
    fi
    echo "$out_file"
    lib -nologo -OUT:"$out_file" $final_in_files
    status=$?
  elif [ "$os" == "linux" || "$os" == "mac" ]; then
    # TODO(allen): invoke ar here - make sure to delete the original .a first
    # because ar does not (seem) to replace the output file, just append
    echo "TODO: implement ar path in bld_core.sh:bld_lib"
    status=1
  else
    echo "ERROR(lib): static library invokation not defined for OS: $os"
    status=1
  fi
  
  return $status
}

###### Unit ###################################################################

function bld_unit {
  local i=0
  
  ###### parse arguments ####################################################
  local main_file=$1
  local opts=()
  for ((i=2; i<=$#; i+=1)); do
    opts+=(${!i})
  done
  if [ "$main_file" == "" ]; then
    echo "unit: missing main file"
    return 1
  fi
  
  ###### set out name #######################################################
  local out_name=""
  if [ "${#opts}" == "0" ]; then
    local file_base=${main_file##*/}
    local file_base_no_ext=${file_base%.*}
    out_name=$file_base_no_ext
  else
    out_name="${opts[0]}"
  fi
  
  ###### finish options #####################################################
  local src_opts=$(bld_opts_from_src $main_file)
  local impl_opts=($(bld_implicit_opts))
  local all_opts=($(bld_dedup $out_name ${opts[@]} ${src_opts[@]} ${impl_opts[@]}))
  
  ###### link ###############################################################
  bld_link $out_name $main_file ${in_files[@]} -- ${all_opts[@]}
}

###### Special Ifs ############################################################

function bld_ext_cmpo {
  echo $(bld_flags_from_opts $opts_path/file_extensions.txt cmp:$compiler "cmp:o")
}
function bld_ext_asmo {
  echo $(bld_flags_from_opts $opts_path/file_extensions.txt asm:$assembler "asm:o")
}
function bld_ext_exe {
  echo $(bld_flags_from_opts $opts_path/file_extensions.txt os:$os exe)
}
function bld_ext_dll {
  echo $(bld_flags_from_opts $opts_path/file_extensions.txt os:$os dll)
}

###### Load Locals ############################################################
bld_load_local_opts

###############################################################################
# TODO: Notes for future itations on this sytem
# - With the addition of the assembler, it is now becoming clear that the
#   bld_compile path is feeling a bit overloaded and that the mapping of
#   interface extensions to real extensions, and the mapping of interface
#   extensions to inferred build steps, are major complicating factors.
#   A new strategy for organizing multiple builders, and multiple conversion
#   paths that may be inferred from a set of possible starting points would
#   stand to simplify a lot of the system.
#    - Switching out object file extensions
#    - Subtlties of invoking different build kinds in bld_compile
#    - Tracking subkinds through bld_link and bld_lib