#!/usr/bin/env bash

#
#	MetaCall Build Bash Script by Parra Studios
#	Build and install bash script utility for MetaCall.
#
#	Copyright (C) 2016 - 2026 Vicente Eduardo Ferrer Garcia <vic798@gmail.com>
#
#	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.
#

set -euxo pipefail

# Enable BuildKit whenever possible
export COMPOSE_DOCKER_CLI_BUILD=1
export DOCKER_BUILDKIT=1
export BUILDKIT_PROGRESS=plain
export PROGRESS_NO_TRUNC=1

# MetaCall build options
export METACALL_BUILD_SANITIZER=
export METACALL_BUILD_CLANG=
export METACALL_BUILD_COVERAGE=
export METACALL_BUILD_MEMCHECK=

# Check if docker compose command is available
if [ -x "$(command -v docker-compose)" ]; then
	DOCKER_COMPOSE="docker-compose"
elif $(docker compose &>/dev/null) && [ $? -eq 0 ]; then
	DOCKER_COMPOSE="docker compose"
else
	echo "Docker Compose not installed, install it and re-run the script"
	exit 1
fi

# List of tags
METACALL_TAGS=("deps" "dev" "runtime" "cli")

# Pull MetaCall Docker Compose
sub_pull() {
	if [ -z "${IMAGE_NAME+x}" ]; then
		echo "Error: IMAGE_NAME variable not defined"
		exit 1
	fi

	for tag in "${METACALL_TAGS[@]}"; do
		docker pull $IMAGE_NAME:${tag} && docker tag $IMAGE_NAME:${tag} metacall/core:${tag} || true
	done
}

# Build MetaCall Docker Compose
sub_build() {
	$DOCKER_COMPOSE -f docker-compose.yml build --force-rm cli
}

# Build MetaCall Docker Compose without cache
sub_rebuild() {
	$DOCKER_COMPOSE -f docker-compose.yml build --force-rm --no-cache cli
}

# Build MetaCall Docker Compose for testing
sub_test() {
	# Define build type
	export METACALL_BUILD_TYPE=${METACALL_BUILD_TYPE:-debug}

	$DOCKER_COMPOSE -f docker-compose.yml build --force-rm dev
}

# Build MetaCall Docker Compose with Sanitizer for testing
sub_test_sanitizer() {
	# Enable build with sanitizer
	export METACALL_BUILD_SANITIZER=${METACALL_BUILD_SANITIZER:-address-sanitizer}

	# Define build type
	export METACALL_BUILD_TYPE=${METACALL_BUILD_TYPE:-debug}

	if [ ! -z "${SANITIZER_SKIP_SUMMARY:-}" ]; then
		$DOCKER_COMPOSE -f docker-compose.yml build --force-rm dev
	else
		$DOCKER_COMPOSE -f docker-compose.yml build --force-rm dev | tee /tmp/metacall-test-output

		# Retrieve all the summaries
		SUMMARY=$(grep "SUMMARY:" /tmp/metacall-test-output)
		echo "${SUMMARY}"
		printf "Number of leaks detected: "
		echo "${SUMMARY}" | awk '{print $7}' | awk '{s+=$1} END {print s}'

		# Count the number of tests that really failed and avoid the false positives
		FAILED=$(grep "FAILED TEST" /tmp/metacall-test-output)
		printf "Number of tests failed: "
		echo "${FAILED}" | awk '{print $1}' | awk '{s+=$1} END {print s}'

		# Get the potential tests that failed
		BEGIN=$(grep -n "The following tests FAILED:" /tmp/metacall-test-output | cut -d : -f 1)
		END=$(grep -n "Errors while running CTest" /tmp/metacall-test-output | cut -d : -f 1)

		if [ -z "${BEGIN+x}" ] || [ -z "${END+x}" ]; then
			echo "ERROR! CTest failed to print properly the output, run tests again:"
			echo "	Recompiling everything: docker rmi metacall/core:dev && ./docker-compose.sh test-${METACALL_BUILD_SANITIZER}"
			echo "	Without recompiling (needs to be built successfully previously): docker run --rm -it metacall/core:dev sh -c \"cd build && ctest -j$(getconf _NPROCESSORS_ONLN) --output-on-failure\""
		else
			BEGIN=$((BEGIN + 1))
			END=$((END - 1))
			echo "List of potential failed tests:"
			sed -n "${BEGIN},${END}p" /tmp/metacall-test-output
		fi

		rm /tmp/metacall-test-output
	fi
}

# Build MetaCall Docker Compose for coverage
sub_coverage() {
	# Disable build with coverage
	export METACALL_BUILD_COVERAGE=coverage

	# Define build type
	export METACALL_BUILD_TYPE=debug

	$DOCKER_COMPOSE -f docker-compose.yml build --force-rm dev
}

# Build MetaCall Docker Compose with Valgrind for testing
sub_test_memcheck() {
	# Enable build with memcheck
	export METACALL_BUILD_MEMCHECK=memcheck

	# Define build type
	export METACALL_BUILD_TYPE=debug

	$DOCKER_COMPOSE -f docker-compose.yml build --force-rm dev
}

# Build MetaCall Docker Compose with Clang for testing
sub_test_clang() {
	# Enable build with clang
	export METACALL_BUILD_CLANG=clang

	# Define build type
	export METACALL_BUILD_TYPE=debug

	$DOCKER_COMPOSE -f docker-compose.yml build --force-rm dev
}

# Build MetaCall Docker Compose with Memory Sanitizer for testing
sub_test_memory_sanitizer() {
	# Enable build with clang
	export METACALL_BUILD_CLANG=clang-msan

	# Enable build with memory sanitizer
	export METACALL_BUILD_SANITIZER=memory-sanitizer

	# Define build type
	export METACALL_BUILD_TYPE=debug

	$DOCKER_COMPOSE -f docker-compose.yml build --force-rm dev
}

# Build MetaCall Docker Compose with caching
sub_cache() {
	if [ -z "${IMAGE_REGISTRY+x}" ]; then
		echo "Error: IMAGE_REGISTRY variable not defined"
		exit 1
	fi

	$DOCKER_COMPOSE -f docker-compose.yml -f tools/docker/docker-compose.cache.yml build cli
}

# Build MetaCall Docker Compose with multi-platform specifier
sub_platform() {
	if [ -z "${METACALL_PLATFORM+x}" ]; then
		echo "Error: METACALL_PLATFORM variable not defined"
		exit 1
	fi

	# Initialize QEMU for Buildkit
	docker run --rm --privileged tonistiigi/binfmt --install all

	# Get path where docker-compose.sh is located
	BASE_DIR="$(cd "$(dirname "$0")" && pwd)"

	# Load default environment variables
	set -a
	. $BASE_DIR/.env
	set +a

	# Debian in Docker Hub does not support LoongArch64 yet, let's use official LoongArch repository instead
	if [ "$METACALL_PLATFORM" = "linux/loong64" ]; then
		export METACALL_BASE_IMAGE="ghcr.io/loong64/${METACALL_BASE_IMAGE}"
	fi

	# Generate the docker compose file with all .env variables substituted (bake seems not to support this)
	$DOCKER_COMPOSE -f docker-compose.yml config &> docker-compose.bake.yml

	# Build with Bake, so the image can be loaded into local docker context
	for tag in "${METACALL_TAGS[@]}"; do
		docker buildx bake -f docker-compose.bake.yml --set *.platform="${METACALL_PLATFORM}" --load "${tag}"
	done

	# Delete temporal docker compose file
	rm -rf docker-compose.bake.yml
}

# Build MetaCall Docker Bake with multi-platform specifier
sub_bake() {
	if [ -z "${METACALL_PLATFORM+x}" ]; then
		echo "Error: METACALL_PLATFORM variable not defined"
		exit 1
	fi

	if [ -z "${DOCKER_USERNAME+x}" ]; then
		echo "Error: DOCKER_USERNAME variable not defined"
		exit 1
	fi

	if [ -z "${IMAGE_NAME+x}" ]; then
		echo "Error: IMAGE_NAME variable not defined"
		exit 1
	fi

	# Initialize QEMU for Buildkit
	docker run --rm --privileged tonistiigi/binfmt --install all

	# Get path where docker-compose.sh is located
	BASE_DIR="$(cd "$(dirname "$0")" && pwd)"

	# Load default environment variables
	set -a
	. $BASE_DIR/.env
	set +a

	# Get the options from the compose file
	export METACALL_INSTALL_OPTIONS=$(grep "METACALL_INSTALL_OPTIONS:" docker-compose.yml | head -n 1 | sed 's/.*METACALL_INSTALL_OPTIONS: //' | sed 's/#.*//g')
	export METACALL_BUILD_OPTIONS=$(grep "METACALL_BUILD_OPTIONS:" docker-compose.yml | head -n 1 | sed 's/.*METACALL_BUILD_OPTIONS: //' | sed 's/#.*//g')
	export METACALL_RUNTIME_OPTIONS=$(grep "METACALL_RUNTIME_OPTIONS:" docker-compose.yml | head -n 1 | sed 's/.*METACALL_RUNTIME_OPTIONS: //' | sed 's/#.*//g')

	# Debian in Docker Hub does not support LoongArch64 yet, let's use official LoongArch repository instead
	if [ "$METACALL_PLATFORM" = "linux/loong64" ]; then
		export METACALL_BASE_IMAGE="ghcr.io/loong64/${METACALL_BASE_IMAGE}"
	fi

	# Create temporal folder for storing metadata
	mkdir -p .bake

	# Copy the docker ingore file
	cp .dockerignore .bake/.dockerignore

	# Build all images all at once
	docker buildx bake \
		--metadata-file .bake/metadata.json \
		-f docker-bake.hcl \
		--set "*.platform=${METACALL_PLATFORM}" \
		--set "*.output=type=image,name=docker.io/${DOCKER_USERNAME}/${IMAGE_NAME},push-by-digest=true,name-canonical=true,push=true"

	# Generate the digests
	for tag in "${METACALL_TAGS[@]}"; do
		mkdir -p ".bake/digests/${tag}"
		digest=$(jq -r ".${tag}[\"containerimage.digest\"] | ltrimstr(\"sha256:\")" .bake/metadata.json)
		touch ".bake/digests/${tag}/${digest}"
	done
}

# Create Manifest for multi-platform images from Docker Bake
sub_manifest() {
	if [ -z "${DOCKER_USERNAME+x}" ]; then
		echo "Error: IMAGE_NAME variable not defined"
		exit 1
	fi

	if [ -z "${IMAGE_NAME+x}" ]; then
		echo "Error: IMAGE_NAME variable not defined"
		exit 1
	fi

	if [ "${TAG_VERSION:-}" == "true" ]; then
		VERSION=$(tail -n 1 VERSION | tr -d '\n')
	fi

	# Get current directory
	workdir=$(pwd)

	for tag in "${METACALL_TAGS[@]}"; do
		cd "${workdir}/.bake/digests/${tag}"
		image_hashes=$(printf "${DOCKER_USERNAME}/${IMAGE_NAME}@sha256:%s " *)

		# Tag each image
		docker buildx imagetools create -t ${DOCKER_USERNAME}/${IMAGE_NAME}:${tag} ${image_hashes}
		docker buildx imagetools inspect ${DOCKER_USERNAME}/${IMAGE_NAME}:${tag}

		# Tag each image with version if any
		if [ -n "${VERSION+x}" ]; then
			docker buildx imagetools create -t ${DOCKER_USERNAME}/${IMAGE_NAME}:${VERSION}-${tag} ${image_hashes}
			docker buildx imagetools inspect ${DOCKER_USERNAME}/${IMAGE_NAME}:${VERSION}-${tag}
		fi

		if [[ "${tag}" == "cli" ]]; then
			# Tag the latest
			docker buildx imagetools create -t ${DOCKER_USERNAME}/${IMAGE_NAME}:latest ${image_hashes}
			docker buildx imagetools inspect ${DOCKER_USERNAME}/${IMAGE_NAME}:latest

			# Tag the version
			if [ -n "${VERSION+x}" ]; then
				docker buildx imagetools create -t ${DOCKER_USERNAME}/${IMAGE_NAME}:${VERSION} ${image_hashes}
				docker buildx imagetools inspect ${DOCKER_USERNAME}/${IMAGE_NAME}:${VERSION}
			fi
		fi
	done
}

# Push MetaCall Docker Compose
sub_push() {
	if [ -z "${IMAGE_NAME+x}" ]; then
		echo "Error: IMAGE_NAME variable not defined"
		exit 1
	fi

	# Push images
	for tag in "${METACALL_TAGS[@]}"; do
		docker tag metacall/core:${tag} $IMAGE_NAME:${tag}
		docker push $IMAGE_NAME:${tag}
	done

	# Push cli as a latest
	docker tag metacall/core:cli $IMAGE_NAME:latest
	docker push $IMAGE_NAME:latest
}

# Version MetaCall Docker Compose
sub_version() {
	if [ -z "${IMAGE_NAME+x}" ]; then
		echo "Error: IMAGE_NAME variable not defined"
		exit 1
	fi

	VERSION=$(tail -n 1 VERSION | tr -d '\n')

	# Push images
	for tag in "${METACALL_TAGS[@]}"; do
		docker tag metacall/core:${tag} $IMAGE_NAME:${VERSION}-${tag}
		docker push $IMAGE_NAME:${VERSION}-${tag}
	done

	# Push cli image as version
	docker tag metacall/core:cli $IMAGE_NAME:${VERSION}
	docker push $IMAGE_NAME:${VERSION}
}

# Pack MetaCall Docker Compose
sub_pack() {
	if [ -z "${ARTIFACTS_PATH+x}" ]; then
		echo "Error: ARTIFACTS_PATH variable not defined"
		exit 1
	fi

	# Get path where docker-compose.sh is located
	BASE_DIR="$(cd "$(dirname "$0")" && pwd)"

	# Load default environment variables
	set -a
	. $BASE_DIR/.env
	set +a

	# Run the package builds
	docker run --name metacall_core_pack -i metacall/core:dev /bin/bash -c 'cd build && make pack'

	# Create artifacts folder
	mkdir -p $ARTIFACTS_PATH

	# Copy artifacts
	docker cp metacall_core_pack:$METACALL_PATH/build/packages $ARTIFACTS_PATH

	# Remove docker instance
	docker rm metacall_core_pack

	# List generated artifacts
	ls -la $ARTIFACTS_PATH/packages
}

# Help
sub_help() {
	echo "Usage: `basename "$0"` option"
	echo "Options:"
	echo "	pull"
	echo "	build"
	echo "	rebuild"
	echo "	test"
	echo "	test-address-sanitizer"
	echo "	test-thread-sanitizer"
	echo "	test-memory-sanitizer"
	echo "	coverage"
	echo "	test-memcheck"
	echo "	test-clang"
	echo "	test-clang-address-sanitizer"
	echo "	test-clang-thread-sanitizer"
	echo "	cache"
	echo "	platform"
	echo "	push"
	echo "	pack"
	echo ""
}

case "$1" in
	pull)
		sub_pull
		;;
	build)
		sub_build
		;;
	rebuild)
		sub_rebuild
		;;
	test)
		sub_test
		;;
	test-address-sanitizer)
		export METACALL_BUILD_SANITIZER="address-sanitizer"
		sub_test_sanitizer
		;;
	test-thread-sanitizer)
		export METACALL_BUILD_SANITIZER="thread-sanitizer"
		sub_test_sanitizer
		;;
	test-memory-sanitizer)
		sub_test_memory_sanitizer
		;;
	coverage)
		sub_coverage
		;;
	test-memcheck)
		sub_test_memcheck
		;;
	test-clang)
		sub_test_clang
		;;
	test-clang-address-sanitizer)
		export METACALL_BUILD_CLANG="clang"
		export METACALL_BUILD_SANITIZER="address-sanitizer"
		sub_test_sanitizer
		;;
	test-clang-thread-sanitizer)
		export METACALL_BUILD_CLANG="clang"
		export METACALL_BUILD_SANITIZER="thread-sanitizer"
		sub_test_sanitizer
		;;
	cache)
		sub_cache
		;;
	platform)
		sub_platform
		;;
	bake)
		sub_bake
		;;
	manifest)
		sub_manifest
		;;
	push)
		sub_push
		;;
	version)
		sub_version
		;;
	pack)
		sub_pack
		;;
	*)
		sub_help
		;;
esac
