#!/bin/bash

# --------------------------------------------
#            Usage 

VALID_VALUES_COMPILER=("clang" "clang++" "msvc")
VALID_VALUES_MODE=("debug" "release")
VALID_VALUES_PLATFORM=("win32" "osx" "linux" "raspi" "wasm")
VALID_VALUES_ARCH=("x64" "arm64")
VALID_VALUES_PACKAGE=("true" "false")

printf_r () {
  printf "\e[31m$@\e[0m\n"
}

printf_g () {
  printf "\e[32m%s\e[0m\n" "$@"
}

print_usage () {
  printf "\n"
  printf "Build Command Syntax:\n"
  printf_g "  $0 [compiler] [mode] [platform] [arch] [package]"
  printf "\n"
  printf "Compiler Options:\n"
  printf "  \e[32m%s\e[m\n" "${VALID_VALUES_COMPILER[@]}"
  printf "\n"
  printf "Release Mode Options:\n"
  printf "  \e[32m%s\e[m\n" "${VALID_VALUES_MODE[@]}"
  printf "\n"
  printf "Platform Options:\n"
  printf "  \e[32m%s\e[m\n" "${VALID_VALUES_PLATFORM[@]}"
  printf "\n"
  printf "Arch Options: \n"
  printf "  \e[32m%s\e[m\n" "${VALID_VALUES_ARCH[@]}"
  printf "\n"
  printf "Package Options:\n"
  printf_g "  'package' or no flag to omit packaging\n"
  printf "\n"
  printf "Examples:\n"
  printf "  $0 clang debug osx arm64\n"
  printf "  $0 msvc release win32 x64 package\n"
}

OPTS=()
for ((i=1; i<=$#; i+=1)); do
  OPTS+=(${!i})
done

if [ "${OPTS[0]}" == "-h" ] || [ "${OPTS[0]}" == "--help" ]
then
  print_usage
  exit 1
fi

# --------------------------------------------
#            Utilities

pushdir () {
  command pushd "$@" > /dev/null
}

popdir () {
  command popd "$@" > /dev/null
}

# --------------------------------------------
# Project Directory Identification

BUILD_SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}")
pushdir $BUILD_SCRIPT_DIR
pushdir ..
PROJECT_PATH=$(pwd)
popdir
popdir

BLD_DIR="${PROJECT_PATH}/build"
SRC_DIR="${PROJECT_PATH}/src"
OUT_DIR="${PROJECT_PATH}/run_tree"

# --------------------------------------------
# Input Flag Settings

INPUT_FLAG_UNSET="unset"

COMPILER=$INPUT_FLAG_UNSET
MODE=$INPUT_FLAG_UNSET
PLATFORM=$INPUT_FLAG_UNSET
ARCH=$INPUT_FLAG_UNSET
PACKAGE=$INPUT_FLAG_UNSET

# --------------------------------------------
# Create a local build file if there isn't one
# using local context to determine defaults

BLD_LOCAL_FILE="${BLD_DIR}/build_local.sh"

if [ ! -f $BLD_LOCAL_FILE ] 
then

  printf "Creating a build/build_local.sh file for you."
  printf "  Path: ${BLD_LOCAL_FILE}"
  printf "This file is excluded in the .gitignore. It is for you to set local compilation targets"

  touch $BLD_LOCAL_FILE
  printf "#!/bin/bash"        >> $BLD_LOCAL_FILE
  printf                      >> $BLD_LOCAL_FILE
  printf "COMPILER=\"clang\"" >> $BLD_LOCAL_FILE
  printf "MODE=\"debug\""     >> $BLD_LOCAL_FILE
  printf "PLATFORM=\"osx\""   >> $BLD_LOCAL_FILE
  printf "ARCH=\"arm64\""     >> $BLD_LOCAL_FILE
  printf "PACKAGE=\"false\""  >> $BLD_LOCAL_FILE
  printf "TEST_FILE=\"\""     >> $BLD_LOCAL_FILE
fi

# --------------------------------------------
# Call Local Build File

source ${BLD_LOCAL_FILE}

# --------------------------------------------
# Use command line arguments to override local
# build file

if [ "${#OPTS[@]}" -gt "0" ]; then
  OPTS_COUNT="${#OPTS[@]}"
  if [ $OPTS_COUNT -lt "4" ] || [ $OPTS_COUNT -gt "5" ]; then
    printf_r "Error: Incorrect number of arguments supplied"
    printf   "       You must either supply all or none of the build script arguments\n"
    print_usage
    exit 1
  fi
  
  COMPILER=${OPTS[0]}
  MODE=${OPTS[1]}
  PLATFORM=${OPTS[2]}
  ARCH=${OPTS[3]}

  PACkaGE="false"
  if [ $OPTS_COUNT -eq "5" ]; then
    if [ "${OPTS[4]}" == "package" ]; then
      PACKAGE="true"
    else
      printf_r "Error: Invalid package command provided: ${PACKAGE}"
      printf   "  You must either supply the 'package' flag or omit it"
      exit 1
    fi
  fi
fi

# --------------------------------------------
# Verify valid values for all inputs

ALL_VALID_VALUES="true"

check_valid_flag () {
  local VALID_VALUES_NAME=$1[@]
  local VALID_VALUES=("${!VALID_VALUES_NAME}")
  local VALUE_GIVEN=$2
  local VALUE_ID=$3
  
  if [[ ! " ${VALID_VALUES[*]} " =~ " ${VALUE_GIVEN} " ]]; then
    printf_r "Error: Invalid ${VALUE_ID} provided: ${VALUE_GIVEN}"
    printf   "  Must be one of: "
    printf_g "${VALID_VALUES[*]}\n"
    ALL_VALID_VALUES="false"
  fi
}

check_valid_flag VALID_VALUES_COMPILER $COMPILER "compiler"
check_valid_flag VALID_VALUES_MODE     $MODE     "mode"
check_valid_flag VALID_VALUES_PLATFORM $PLATFORM "platform"
check_valid_flag VALID_VALUES_ARCH     $ARCH     "arch"

if [[ ! " ${VALID_VALUES_PACKAGE[*]} " =~ " ${PACKAGE} " ]]; then
  printf_r "Error: Invalid package provided: ${PACKAGE}"
  printf   "  You must either supply the 'package' flag or omit it"
  ALL_VALID_VALUES="false"
fi

if [ "${ALL_VALID_VALUES}" != "true" ]; then 
  exit 1
fi

if [ "${COMPILER}" == "clang" ] || [ "${COMPILER}" == "clang++" ]; then
  LINKER=${COMPILER}
elif [ "${COMPILER}" == "msvc" ]; then
  LINKER="link"
fi

printf "Compiler: "
  printf_g "${COMPILER}"
printf "Mode:     "
  printf_g "${MODE}"
printf "Platform: "
  printf_g "${PLATFORM}"
printf "Arch:     "
  printf_g "${ARCH}"
printf "Package:  "
  printf_g "${PACKAGE}"

# --------------------------------------------
# Hooks Identification

HOOK_PREBUILD="${BLD_DIR}/hook_prebuild.sh"
HOOK_POSTBUILD="${BLD_DIR}/hook_postbuild.sh"
HOOK_PRELINK="${BLD_DIR}/hook_prelink.sh"
HOOK_POSTLINK="${BLD_DIR}/hook_postlink.sh"

printf "\nBuild Hooks:\n"
if [ -f "${HOOK_PREBUILD}" ]; then
  printf "  Pre  Build: ${HOOK_PREBUILD##*/}\n"
else
  HOOK_PREBUILD=""
fi

if [ -f "${HOOK_POSTBUILD}" ]; then
  printf "  Post Build: ${HOOK_POSTBUILD##*/}\n"
else
  HOOK_POSTBUILD=""
fi

if [ -f "${HOOK_PRELINK}" ]; then
  printf "  Pre  Link: ${HOOK_PRELINK##*/}\n"
else
  HOOK_PRELINK=""
fi

if [ -f "${HOOK_POSTLINK}" ]; then
  printf "  Post Link: ${HOOK_POSTLINK##*/}\n"
else
  HOOK_POSTLINK=""
fi

# --------------------------------------------
# File Parsing Helpers

trim() {
  local var="$*"

  # remove leading whitespace characters
  var="${var#"${var%%[![:space:]]*}"}"

  # remove trailing whitespace characters
  var="${var%"${var##*[![:space:]]}"}"

  echo "${var}"
}

load_file_into_lines_array() {
  FILE=$1
  local FILE_LINES_RAW=()
  IFS=$'\r\n'
  GLOBIGNORE='*'
  command eval 'FILE_LINES_RAW=($(cat $FILE))'

  FILE_LINES=()
  for i in "${FILE_LINES_RAW[@]}"; do
    if [ "${i:0:1}" != "#" ]; then
      # strip any trailing comments off the end
      # this lets you do things like:
      #   "compiler>msvc>-FC # full path error"
      # where you want to explain a flag
      local LINE=$i
      local LINE_NO_TRAILING_COMMENT="${LINE% #*}"
      FILE_LINES+=($LINE_NO_TRAILING_COMMENT)
    fi
  done
}

parse_flags_from_selectors() {
  SELECTORS=()
  for ((i=1; i<=$#; i+=1)); do
    SELECTORS+=(${!i})
  done
  
  FLAGS=()
  for i in "${FILE_LINES[@]}"; do
    LINE=$i
    FLAG="${LINE##*>}"
  
    INCLUDE_FLAG="true"
    while [ "${LINE}" != "${FLAG}" ]; do
      NEXT_SELECTOR="${LINE%%>*}"
      LINE="${LINE#*>}"
      if [[ ! " ${SELECTORS[@]} " =~ " ${NEXT_SELECTOR} " ]]; then
        INCLUDE_FLAG="false"
        break
      fi
      
    done

    if [ "${INCLUDE_FLAG}" == "true" ]; then
      FLAG=$(trim "${FLAG}")
      FLAG=$(eval "echo $FLAG")
      FLAGS+=($FLAG)
    fi
  done
}

load_file_into_lines_array "${BLD_DIR}/build_flags.sh"

# --------------------------------------------
# Assemble Flags

parse_flags_from_selectors "compiler" $COMPILER $MODE $PLATFORM $ARCH
COMPILER_FLAGS=(${FLAGS[@]})

parse_flags_from_selectors "compiler" "input" $PLATFORM $ARCH
COMPILER_INPUTS=(${FLAGS[@]})

parse_flags_from_selectors "linker" $MODE $PLATFORM $ARCH
LINKER_FLAGS=(${FLAGS[@]})

parse_flags_from_selectors "linker" "libs" $LINKER $MODE $PLATFORM $ARCH
LINKER_LIBRARIES=(${FLAGS[@]})

parse_flags_from_selectors "linker" "output" $LINKER $MODE $PLATFORM $ARCH
LINKER_OUTPUT=(${FLAGS[@]})

# --------------------------------------------
# Create the Run Tree path

OUT_PATH="${OUT_DIR}/${PLATFORM}/${ARCH}/${MODE}"

if [ ! -d $OUT_PATH ]; then
  mkdir -p $OUT_PATH
fi

# --------------------------------------------
# Compile The Program

printf "\nBeginning Compilation...\n"
pushdir $OUT_PATH

if [[ -f ${HOOK_PREBUILD} ]]; then
  source "${HOOK_PREBUILD}"
fi

COMPILATION_SUCCESS="true"
COMPILER_OUTPUT=()
FAILED_COMPILES=()
for i in "${COMPILER_INPUTS[@]}"; do
  INPUT="${i}"
  
  INPUT_FILE="${INPUT##*/}"
  INPUT_EXTENSION="${INPUT_FILE##*.}"
  INPUT_NAME="${INPUT_FILE%.*}"
  OUTPUT_FILE="${INPUT_NAME}_${INPUT_EXTENSION}.o"
  
  COMPILER_ARGS="-o ${OUTPUT_FILE} -c ${COMPILER_FLAGS[@]} -DPLATFORM_$PLATFORM=1 -DMODE_$MODE=1 -DARCH_$ARCH=1 $INPUT"

  # echo $COMPILER $COMPILER_ARGS
  eval $COMPILER $COMPILER_ARGS
  if [ $? -eq 0 ]; then
    COMPILER_OUTPUT+=(${OUTPUT_FILE})
  else
    COMPILATION_SUCCESS="false"
    FAILED_COMPILES+=(${OUTPUT_FILE})
  fi

  # TODO: if the output file was a .dll or .lib we don't want to include
  # those in the final compilation gather
done

printf "\nCompiler Output\n"
if [ ${#COMPILER_OUTPUT[@]} -gt 0 ]; then
  printf "  %s \e[32m[SUCCESS]\e[0m\n" "${COMPILER_OUTPUT[@]}"
fi
if [ ${#FAILED_COMPILES[@]} -gt 0 ]; then
  printf "  %s \e[31m[FAILED]\e[0m\n" "${FAILED_COMPILES[@]}"
fi
printf "\n"

if [[ -f ${HOOK_POSTBUILD} ]]; then
  source "${HOOK_POSTBUILD}"
fi

if [ $COMPILATION_SUCCESS != "true" ]; then
  printf "Compilation Failed.\n  Exiting..."
  exit 1
fi

if [[ -f ${HOOK_PRELINK} ]]; then
  source "${HOOK_PRELINK}"
fi

LINKER_ARGS="-o ${LINKER_OUTPUT} ${COMPILER_OUTPUT[@]} ${LINKER_FLAGS[@]} ${LINKER_LIBRARIES[@]}"


printf "Linking...\n"
# echo $LINKER $LINKER_ARGS
eval $LINKER $LINKER_ARGS
if [ $? -eq 0 ]; then
  printf   "  Link: "
  printf_g "[SUCCEEDED]"
else
  printf   "\n  Link: "
  printf_r "[FAILED]"
fi

if [[ -f ${HOOK_POSTLINK} ]]; then
  source "${HOOK_POSTLINK}"
fi

popdir

exit 0