#!/usr/bin/env bash

## INIT

# Get script directory
script_dir="$(cd "$(dirname "$0")" && pwd)"

# Change working directory to script directory
cd $script_dir

# Set up log file
log_file="$script_dir/build.log"

rm -f $log_file

## FUNCTIONS

# Function to print usage instructions
help() {
  echo "Usage: $0 [options]"
  echo "Options:"
  echo "  -h, --help     Print this help and exit"
  echo "  -i             Input project name (required)"
  echo "  -g             Generated project name (default: <input-project-name>.Generated)"
  echo "  -o             Output folder name (default: libs)"
  echo "  -y             Assume yes to all prompts"
  echo "  --ndk          Android NDK path (default: ANDROID_NDK_HOME environment variable)"
  echo "  --no-android   Disable Android build (default: false)"
  echo "  --no-ios       Disable iOS build (default: false)"
  echo "  --input-spec   Input project .NET spec (default: net8.0)"
  echo "  --output-spec  Output project .NET spec (default: net8.0)"
  echo "  --flutter      Generate flutter project"
}

# Function to log a message to the console and log file
log() {
  echo "$1"
  echo "$1" >>$log_file
}

# Function to run a command and log the output
run() {
  eval $1 2>&1 | tee -a $log_file
}

# Function to check if a command exists
command_exists() {
  command -v "$1" >/dev/null 2>&1
}

# Function to replace INPUT_PROJECT_NAME and GENERATED_PROJECT_NAME
# by the actual project names in the passed file
replace_project_names() {
  sed -i '' "s/INPUT_PROJECT_NAME/$input_project_name/g" $1
  sed -i '' "s/GENERATED_PROJECT_NAME/$generated_project_name/g" $1
  sed -i '' "s/INPUT_NET_SPEC/$input_project_net_spec/g" $1
  sed -i '' "s/OUTPUT_NET_SPEC/$output_project_net_spec/g" $1
}

# Function to ask for confirmation before continuing
# $1: message
ask_confirmation() {
  if [ "$assume_yes" = true ]; then
    return
  fi
  read -p "$1 (y/n) " -n 1 -r
  echo
  if [[ ! $REPLY =~ ^[Yy]$ ]]; then
    exit 1
  fi
}

# Generate project
generate_project() {
  cd $script_dir

  # Copy template beyondnetgen.*.config.json
  cp template.beyondnetgen.android.config.json beyondnetgen.android.config.json
  replace_project_names beyondnetgen.android.config.json

  cp template.beyondnetgen.ios.config.json beyondnetgen.ios.config.json
  replace_project_names beyondnetgen.ios.config.json

  # Generate project
  log "Generating project"
  run "beyondnetgen beyondnetgen.android.config.json"
}

# Generic function to build a project
build_project() {
  local platform="$1"
  local config_file="$2"
  local template_csproj="$3"
  local output_dir="$4"
  local library_extension="$5"
  local abis=("${!6}")

  cd "$script_dir"

  # Copy template config file
  cp "$config_file" "beyondnetgen.$platform.config.json"
  replace_project_names "beyondnetgen.$platform.config.json"

  # Generate project
  log "Generating $platform project"
  run "beyondnetgen beyondnetgen.$platform.config.json"

  # Copy template csproj
  cp "$template_csproj" "$generated_project_dir/$generated_project_name.csproj"
  replace_project_names "$generated_project_dir/$generated_project_name.csproj"

  cd "$generated_project_dir"

  # For each .dll in external folder add it as a reference in the generated project
  search_dir="$script_dir/external"
  for entry in "$search_dir"/*; do
    dll_name=$(basename "$entry")
    log "Adding reference to $dll_name"
    # Add reference to the generated project by editing the csproj file
    # Search for the line <!-- ADD REFERENCES HERE (DO NOT DELETE THIS)-->
    # and add a reference to the dll below it
    sed -i '' "/<!-- ADD REFERENCES HERE (DO NOT DELETE THIS)-->/a \\
    <Reference Include=\"$entry\" />\\
" "$generated_project_dir/$generated_project_name.csproj"
  done

  if [ "$platform" == "android" ]; then
    cp $script_dir/android_fake_clang.* "$generated_project_dir/"
  fi

  # Build for each architecture
  for arch in "${abis[@]}"; do
    log "Building $platform library for $arch"

    run "dotnet publish -r $arch -v normal -c Release -p:DisableUnsupportedError=true -p:PublishAotUsingRuntimePack=true -p:RemoveSections=true -p:AllowUnsafeBlocks=true $generated_project_name.csproj"

    # Copy the generated library to the output directory
    mkdir -p "$output_dir/$arch"
    cp $generated_project_dir/bin/Release/$output_project_net_spec/$arch/native/$generated_project_name.$library_extension $output_dir/$arch/$input_project_name.$library_extension
    if [ "$platform" == "ios" ]; then
      cd $output_dir/$arch
      install_name_tool -id "$(pwd)/"$input_project_name.$library_extension"" $input_project_name.$library_extension
      cd -
    fi
  done

  # Copy the generated headers to the output directory
  log "Copying header to output directory"
  cp "$generated_project_dir/Generated.h" "$output_dir/$input_project_name.h"
}

# Definition of specific parameters for each platform
android_config_file="template.beyondnetgen.android.config.json"
ios_config_file="template.beyondnetgen.ios.config.json"
android_template_csproj="template.android.csproj"
ios_template_csproj="template.ios.csproj"
ios_library_extension="dylib"
android_library_extension="so"

# Definition of architectures for each platform
android_abis=("linux-bionic-arm64" "linux-bionic-x64")
ios_abis=( "osx-x64" "osx-arm64" "ios-arm64" "iossimulator-arm64" "iossimulator-x64")

# Calling the generic function to build the Android project
build_android() {
  build_project "android" "$android_config_file" "$android_template_csproj" "$output_dir" "$android_library_extension" android_abis[@]
}

# Calling the generic function to build the iOS project
build_ios() {
  build_project "ios" "$ios_config_file" "$ios_template_csproj" "$output_dir" "$ios_library_extension" ios_abis[@]
}

generate_flutter() {
  cd $script_dir

  # From input_project_dir generate a flutter project name
  flutter_project_name="$input_project_name.Flutter"
  # Dart project name is the same as flutter project name but in snake case (remove dots and capitalize each word)
  dart_project_name=$(echo $flutter_project_name | sed -E 's/([A-Z])/_\1/g' | sed -E 's/^_//g' | sed -E 's/\.//g' | tr '[:upper:]' '[:lower:]')

  # If flutter_project_name already exists, ask for confirmation before continuing
  if [ -d "$flutter_project_name" ]; then
    ask_confirmation "Flutter project $flutter_project_name already exists. Continue?"
    rm -rf $flutter_project_name
  fi

  # Create flutter project
  log "Creating flutter project in $flutter_project_name"
  run "flutter create -e --platforms android,ios --project-name $dart_project_name $flutter_project_name"

  # Copy generated files to flutter project
  log "Copying generated files to flutter project"
  mkdir -p $flutter_project_name/android/app/src/main/jniLibs
  cp $generated_project_dir/Generated.h $flutter_project_name/Generated.h
  cp -r libs/linux* $flutter_project_name/android/app/src/main/jniLibs/
  cp ffigen.yaml $flutter_project_name/ffigen.yaml

  cd $flutter_project_name
  run "flutter pub add -d ffigen && flutter pub get && dart run ffigen --config ffigen.yaml"
}

## ARGUMENTS

# Set default values
WANT_TO_BUILD_ANDROID=true
WANT_TO_BUILD_IOS=true
ONLY_GENERATE=false
WANT_TO_GENERATE_FLUTTER=false

# Check for arguments
if [ $# -eq 0 ]; then
  log "No arguments supplied"
  help
  exit 1
fi

# Parse arguments
while [ "$1" != "" ]; do
  case $1 in
  -h | --help)
    help
    exit 0
    ;;
  -i)
    shift
    input_project_name=$1
    ;;
  -g)
    shift
    generated_project_name=$1
    ;;
  -o)
    shift
    output=$1
    ;;
  -y)
    assume_yes=true
    ;;
  --ndk)
    shift
    export ANDROID_NDK_HOME=$1
    ;;
  --input-spec)
    shift
    input_project_net_spec=$1
    ;;
  --output-spec)
    shift
    output_project_net_spec=$1
    ;;
  --no-android)
    WANT_TO_BUILD_ANDROID=false
    ;;
  --no-ios)
    WANT_TO_BUILD_IOS=false
    ;;
  --flutter)
    WANT_TO_GENERATE_FLUTTER=true
    ;;
  *)
    log "Unknown argument: $1"
    help
    exit 1
    ;;
  esac
  shift
done

# Check for required arguments
if [ -z "$input_project_name" ]; then
  log "Input project name is required"
  help
  exit 1
fi

# Set default values
if [ -z "$generated_project_name" ]; then
  generated_project_name="$input_project_name.Generated"
fi

if [ -z "$input_project_net_spec" ]; then
  input_project_net_spec="net8.0"
fi

if [ -z "$output_project_net_spec" ]; then
  output_project_net_spec="net8.0"
fi

if [ -z "$output" ]; then
  output="libs"
fi

input_project_dir="$script_dir/$input_project_name"
generated_project_dir="$script_dir/$generated_project_name"
output_dir="$script_dir/$output"

## PREREQUISITES CHECK

# Check for bash >= 4.0
if [ "${BASH_VERSINFO:-0}" -lt 4 ]; then
  log "bash >= 4.0 is required. Aborting."
  exit 1
fi

# Check for dotnet
command_exists dotnet || {
  log "dotnet is required but it's not installed or not in PATH. Aborting."
  exit 1
}

# Check if dotnet is in version 8.0 or higher
dotnet_version=$(dotnet --version)

if [[ $dotnet_version =~ ^8\. ]]; then
  log "dotnet version $dotnet_version is supported"
else
  log "dotnet version $dotnet_version is not supported. Aborting."
  exit 1
fi

# Check for beyondnetgen
command_exists beyondnetgen || {
  log "beyondnetgen is required but it's not installed or not in PATH. Aborting."
  exit 1
}

# Check if host is macos, if so, check for xcode, and set BUILD_IOS to true
ostype=$(uname -s)
log "Host OS: $ostype"
if [ "$ostype" == "Darwin" ]; then
  if [ "$WANT_TO_BUILD_IOS" = false ]; then
    log "Disabling iOS build."
    BUILD_IOS=false
  else
    command_exists xcodebuild || {
      log "Xcode is not installed or not in PATH. Disabling iOS build."
      BUILD_IOS=false
    }

    log "Xcode detected. Enabling iOS build."
    BUILD_IOS=true
  fi
else
  BUILD_IOS=false
fi

# Check for ANDROID_NDK_HOME
if [ "$ANDROID_NDK_HOME" ] && [ "$WANT_TO_BUILD_ANDROID" = true ]; then
  log "ANDROID_NDK_HOME is set to $ANDROID_NDK_HOME. Enabling Android build."
  BUILD_ANDROID=true
else
  BUILD_ANDROID=false
  if [ -z "$ANDROID_NDK_HOME" ]; then
    log "ANDROID_NDK_HOME is not set. Disabling Android build."
  else
    log "Disabling Android build."
  fi
fi

# Check if BUILD_IOS or BUILD_ANDROID is true
if [ "$BUILD_IOS" = false ] && [ "$BUILD_ANDROID" = false ]; then
  log "Neither iOS nor Android build is enabled."
  log "Only generating project."
  ONLY_GENERATE=true
fi

## CONFIG

cd $script_dir

# Copy template beyondnetgen.*.config.json
cp template.beyondnetgen.android.config.json beyondnetgen.android.config.json
replace_project_names beyondnetgen.android.config.json

cp template.beyondnetgen.ios.config.json beyondnetgen.ios.config.json
replace_project_names beyondnetgen.ios.config.json

# Check if output_dir exists, if not, create it
if [ ! -d "$output_dir" ]; then
  log "Creating output directory $output_dir"
  mkdir -p $output_dir
fi

## BUILD

# Clean previous build
log "Cleaning previous build..."

# Ask for confirmation
ask_confirmation "Will delete: $generated_project_dir, $output_dir, $input_project_dir/obj $input_project_dir/bin.  Continue?"

rm -r $generated_project_dir
rm -r "$input_project_dir/obj"
rm -r "$input_project_dir/bin"

# Build input project
log "Building input project"
cd $input_project_dir
run "dotnet publish -v normal -c Release"

# Only generate project
if [ "$ONLY_GENERATE" = true ]; then
  generate_project
fi

# Build Android libraries
if [ "$BUILD_ANDROID" = true ]; then
  build_android
fi

# Build iOS libraries
if [ "$BUILD_IOS" = true ]; then
  build_ios
fi

# Generate flutter project
if [ "$WANT_TO_GENERATE_FLUTTER" = true ]; then
  generate_flutter
fi

# Check last command exit code
if [ $? -ne 0 ]; then
  log "Build failed. See $log_file for details."
  exit 1
else
  log "Finished building. See $log_file for details."
fi
