#!/usr/bin/env bash
#
# crossclang
# A helper script to simplify cross-compilation
#
# prepare-target-sdk is to be run on the target platform
#
# Copyright 2019, 2020 Christian Kohlschütter
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

targetrootbase="$(dirname $0)/../target-sdks/"
triple=

printHelp=0
compiler=("clang" "gcc")
linkerBinary=("ld")
allPaths=0
includeInstallLibs=0
while [ $# -gt 0 ]; do
  v="$1"
  shift

  case "$v" in 
    -h | --help ) printHelp=1 ;;
    --compiler ) compiler=("$1"); shift; ;;
    --linker ) linkerBinary=("$1"); shift; ;;
    --all-paths ) allPaths=1; ;;
    --include-install-libs ) includeInstallLibs=1; ;;
    * ) echo "Illegal argument: $v" >&2 ; printHelp=1; ;;
  esac
done

if [ $printHelp -eq 1 ]; then
    cat <<EOT
Syntax: $0 [--help|-h]
           [--compiler <path-to-compiler-binary>] [--linker <path-to-linker-binary>]
           [--all-paths] [--include-install-libs]
EOT
    exit 1
fi

compiler="$(PATH=/usr/bin:$PATH which ${compiler[@]} 2>/dev/null | grep -e "^/" | head -n 1)"
if [ -z "$compiler" ]; then
    echo "clang/gcc not available; configuration may not be accurate" >&2
else
    echo "Using compiler: $compiler"
fi

linkerBinary="$(PATH=/usr/bin:$PATH which ${linkerBinary[@]} 2>/dev/null | grep -e "^/" | head -n 1)"
if [ -z "$linkerBinary" ]; then
    echo "Linker not available; configuration may not be accurate" >&2
else
    echo "Using linker: $linkerBinary"
fi

if [ -z "$triple" ]; then
    target=$($compiler -v 2>&1 | grep "^Target: " | head -n 1)
    target=${target##* }
    if [ -n "$target" ]; then
        triple="$target"
    fi
fi

echo "Compiler default triple: $triple"

if [ -z "$triple" ]; then
    triple=$(cd /usr/lib/gcc/ ; ls -d *-* | head -n 1)
    echo "Inferring triple from /usr/lib/gcc: $triple"
fi
if [ -z "$triple" ]; then
    echo "Could not determine target triple" >&2
    exit 1
fi
if [ -z "$targetrootbase" ]; then
    echo "No targetrootbase specified" >&2
    exit 1
fi

while [ "${targetrootbase:(-1)}" == "/" ]; do
    targetrootbase="${targetrootbase%*/}"
done

targetroot="$targetrootbase/$triple"
echo "Target: $triple"
mkdir -p "$targetroot"

cd "$targetroot"
targetpwd=$(pwd)
cd - >/dev/null

echo "Location of target SDK: $targetpwd"

# add a few locations to the start of the include path (order is important!)
include_path=(
)
include_cpp_path=(
)

library_path=(
#    /usr/lib/gcc/$triple/*
)

framework_path=(
)

include_path_count_initial=${#include_path[@]}
include_cpp_path_count_initial=${#include_cpp_path[@]}
library_path_count_initial=${#library_path[@]}
framework_path_count_initial=${#framework_path[@]}

# add linker paths from ld64 and friends
section=
IFS=$'\n'; for line in $(LANG=C "$compiler" -Xlinker -v 2>&1 ); do
    if [ "${line:0:1}" == $'\t' ]; then
        path="${line:1}"
        case "$section" in
            library) library_path+=("$path") ;;
            framework) framework_path+=("$path") ;;
        esac
    else
        section=
        if [ "$line" == "Library search paths:" ]; then
            section="library"
        elif [ "$line" == "Framework search paths:" ]; then
            section="framework"
        fi
    fi
done

# add linker paths from ld and friends
old_IFS=$IFS
IFS=' '; for f in $(LANG=C "$compiler" -Xlinker --verbose 2>/dev/null | grep SEARCH_DIR); do
    f=${f#SEARCH_DIR(\"}
    f=${f%\");}
    # strip optional leading "="
    f=${f#=}

    if [[ -n "$f" && -d "$f" ]]; then
        library_path+=("$f")
    fi
done
IFS=${old_IFS}

# add include paths from clang or gcc
for lang in c c++; do
    section=
    
    # NOTE: Not using -nobuiltininc because it's not supported everywhere 
    IFS=$'\n'; for line in $( LANG=C "$compiler" -E -x "$lang" /dev/null -v 2>&1 ); do
        if [ -z "$line" ]; then
           continue
        fi
        if [ "${line:0:1}" != " " ]; then
            section=
            if [ "$line" == "#include <...> search starts here:" ]; then
                section="include"
            fi
            continue
        fi
        path="${line:1}"
        
        if [ "$path" == *" (framework directory)" ]; then
            continue
        fi
            
        if [ ! -d "$path" ]; then
            continue
        fi
        if [[ "$path" == *".xctoolchain/"* ]]; then
            # echo "Ignoring toolchain include: $path" >&2
            continue
        fi
        
        if [ "$lang" == "c" ]; then
          include_path+=("$path") 
        elif [ "$lang" == "c++" ]; then
            existing=0
            for p in ${include_path[@]}; do
                if [ "$p" == "$path" ]; then
                    existing=1
                    break
                fi
            done
            if [ $existing -eq 0 ]; then
                include_cpp_path+=("$path")
            fi 
        else
              echo "Unexpected language: $lang" >&2
        fi
    done
done

if [[ /bin/true ]]; then
  libPaths="$(LANG=C "$compiler" -print-search-dirs | grep "^libraries: " | head -n 1)"
  libPaths="${libPaths#libraries: }"
  library_path+=($(
      IFS=:
      for f in $libPaths; do
        # strip optional leading "="
        f=${f#=}

        echo $f
      done
  ))
fi


if [[ "$includeInstallLibs" -eq 1 ]]; then
  installPaths="$(LANG=C "$compiler" -print-search-dirs | grep "^install: " | head -n 1)"
  installPaths="${installPaths#install: }"
  library_path+=($(
      IFS=:
      for f in $installPaths; do
        # strip optional leading "="
        f=${f#=}

        echo $f
      done
  ))
fi

if [[ "$triple" == *"-darwin"* ]]; then
    library_path+=(
        /usr/lib
        /usr/lib/system
    )
fi

# Add /lib when missing but likely required
haveUsrLib=0
haveLib=0
for f in ${library_path[@]}; do
  if [[ "$f" == "/usr/lib" ]]; then
    haveUsrLib=1
  elif [[ "$f" == "/lib" ]]; then
    haveLib=1
  fi
done
if [[ $haveUsrLib -eq 1 && $haveLib -eq 0 ]]; then
    library_path+=(
        /lib
    )
fi

# fallback defaults
if [ ${#include_path[@]} -le $include_path_count_initial ]; then
    include_path+=(
        /usr/lib/gcc/$triple/*/include
        /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/*.sdk/usr/include
        /usr/include
        /usr/include/linux
    )
fi

if [ ${#include_cpp_path[@]} -le $include_cpp_path_count_initial ]; then
    include_cpp_path+=(
        /usr/include/c++/*
        /usr/include/c++/*/$triple
        /usr/include/c++/*/backward
    )
fi

if [ ${#library_path[@]} -le $library_path_count_initial ]; then
    library_path+=(
        /lib
        /usr/lib
    )
fi

if [ ${#framework_path[@]} -le $framework_path_count_initial ]; then
    framework_path+=(
    )
fi

declare -a arrIn
declare -a arrOut
function checkPaths() {
    arrOut=()
    for path in ${arrIn[@]}; do
        if [ "${path:0:1}" != "/" ]; then
            echo "Path must be absolute: $path" >&2
            continue
        fi
        if [ ! -d "$path" ]; then
            continue
        fi
        path=$(cd "$path"; pwd -P)
        
        skip=0
        for p in ${arrOut[@]}; do
          if [ "$p" == "$path" ]; then
            skip=1
            break
          fi
        done
        
        if [[ "$path" == "/usr/local/"* && $allPaths -eq 0 ]]; then
            # skip "/usr/local/..." paths by default
            echo "Skipping path (run with --all-paths to force): $path" >&2
            continue
        fi
        if [[ "$path" == *".xctoolchain/"* && $allPaths -eq 0  ]]; then
            # skip Xcode toolchain paths by default
            echo "Skipping path (run with --all-paths to force): $path" >&2
            continue
        fi
        
        if [ $skip -eq 0 ]; then
          arrOut+=("$path")
        fi
    done
}

arrIn=(${include_path[@]})
checkPaths
include_path=(${arrOut[@]})

arrIn=(${include_cpp_path[@]})
checkPaths
include_cpp_path=(${arrOut[@]})

arrIn=(${library_path[@]})
checkPaths
library_path=(${arrOut[@]})

arrIn=(${framework_path[@]})
checkPaths
framework_path=(${arrOut[@]})

linker=
if [ -n "$($linkerBinary -help 2>/dev/null| grep ld64)" ]; then
    linker=ld64
elif [ -n "$($linkerBinary -version 2>/dev/null| grep GNU)" ]; then 
    linker=ld
fi

targetConf="$targetroot/target.conf"
targetH="$targetroot/target.h"

cat >"$targetConf" <<EOT
# crossclang target configuration file
# created: $(date)
# on host: $(hostname)
# by user: $(whoami)
target_triple=$triple
target_linker=$linker
target_include_path=( $(for f in ${include_path[@]}; do printf "\n  %q" $f; done )
)
target_include_cpp_path=( $(for f in ${include_cpp_path[@]}; do printf "\n  %q" $f; done )
)
target_library_path=( $(for f in ${library_path[@]}; do printf "\n  %q" $f; done )
)
target_framework_path=( $(for f in ${framework_path[@]}; do printf "\n  %q" $f; done )
)
EOT

cat >"$targetH" <<EOT
/* crossclang target.h -- This header will automatically be included during compilation */
EOT

targetroot=$(cd "$targetroot" ; pwd)

cat <<EOT

Now synchronize the SDK with the following command:
    $(dirname $0)/sync-target-sdk "$targetroot"

If you do not wish to synchronize libraries at this point, enter this instead:
    $(dirname $0)/sync-target-sdk -L "$targetroot"
EOT
