# Build profiles
# BUILD_MODE=debug   -> development/test defaults (-O0 -g)
# BUILD_MODE=release -> release defaults (-O2 -DNDEBUG)
#
# Examples:
#   make test BUILD_MODE=debug
#   make all BUILD_MODE=release

BUILD_MODE ?= debug

ifeq ($(BUILD_MODE),debug)
MODE_CPPFLAGS +=
MODE_CFLAGS += -O0 -g
MODE_LDFLAGS += -g
else ifeq ($(BUILD_MODE),release)
MODE_CPPFLAGS += -DNDEBUG
MODE_CFLAGS += -O2
MODE_LDFLAGS +=
else
$(error Invalid BUILD_MODE='$(BUILD_MODE)' (expected 'debug' or 'release'))
endif

# Version
VERSION = 0.7.6
STACKMAN_VERSION = 1.2.5

# Stack-integrity compile-time defaults (can be overridden from shell).
# Example disable: make TEALET_WITH_STACK_GUARD=0 TEALET_WITH_STACK_SNAPSHOT=0
TEALET_WITH_STACK_GUARD ?= 1
TEALET_WITH_STACK_SNAPSHOT ?= 1
TEALET_WITH_TESTING ?= 0

CPPFLAGS += -Isrc -Istackman/stackman $(PLATFORMFLAGS) $(MODE_CPPFLAGS) -DTEALET_WITH_STATS=1 \
	-DTEALET_WITH_STACK_GUARD=$(TEALET_WITH_STACK_GUARD) \
	-DTEALET_WITH_STACK_SNAPSHOT=$(TEALET_WITH_STACK_SNAPSHOT) \
	-DTEALET_WITH_TESTING=$(TEALET_WITH_TESTING)
CFLAGS += -fPIC -Wall $(PLATFORMFLAGS) $(MODE_CFLAGS)
DEPFLAGS = -MMD -MP
LDFLAGS += -Lbin $(PLATFORMFLAGS) $(MODE_LDFLAGS)

# Handle cross-compilation
ifdef PLATFORM_PREFIX
CC = $(PLATFORM_PREFIX)-gcc
CXX = $(PLATFORM_PREFIX)-g++
AR = $(PLATFORM_PREFIX)-ar
endif

# Add the path to the correct stackman libs
ABI := $(shell sh stackman/tools/abiname.sh "$(CC)" "$(CFLAGS)")
ifndef ABI
$(error Could not determine platform)
endif
LIB := stackman/lib/$(ABI)
LDFLAGS += -L$(LIB)

# Keep Darwin artifacts usable on the oldest macOS release supported by
# the corresponding architecture family unless the caller overrides it.
ifeq ($(ABI),darwin_x86_64)
MACOSX_DEPLOYMENT_TARGET ?= 10.13
export MACOSX_DEPLOYMENT_TARGET
else ifeq ($(ABI),darwin_arm64)
MACOSX_DEPLOYMENT_TARGET ?= 11.0
export MACOSX_DEPLOYMENT_TARGET
endif

# Debug output (only if MAKEFILE_DEBUG is set)
ifdef MAKEFILE_DEBUG
$(info Detected ABI: $(ABI))
$(info Stackman library path: $(LIB))
$(info Checking for libstackman.a: $(wildcard $(LIB)/libstackman.a))
endif

.PHONY: all
all: bin/libtealet.so bin/libtealet.a

FORMAT_FILES := $(shell find src tests -type f \( -name '*.c' -o -name '*.h' \))
CLANG_FORMAT ?= clang-format
DOXYGEN ?= doxygen
DOXYFILE ?= Doxyfile
DOC_OUTPUT_DIR ?= docs/_build/doxygen
SCRIPT_DIR ?= scripts

coreobj = src/tealet.o #src/switch_S.o src/switch_c.o
allobj = $(coreobj) src/tealet_extras.o

src/tealet.o: src/tealet.c src/tealet.h
	$(CC) $(CPPFLAGS) $(CFLAGS) $(DEPFLAGS) -c -o $@ src/tealet.c

src/tealet_extras.o: src/tealet_extras.c src/tealet_extras.h src/tealet.h
	$(CC) $(CPPFLAGS) $(CFLAGS) $(DEPFLAGS) -c -o $@ src/tealet_extras.c

bin:
	mkdir -p bin

bin/libtealet.so: bin $(allobj)
	$(CC) $(LDFLAGS) -shared -o $@ $(allobj) -lstackman

bin/libtealet.a: bin $(allobj)
	$(AR) -rcs $@ $(allobj)
	@# Extract stackman objects and merge into libtealet.a
	@mkdir -p bin/tmp_ar
	@cd bin/tmp_ar && $(AR) -x ../../$(LIB)/libstackman.a
	@$(AR) -rs $@ bin/tmp_ar/*.o
	@rm -rf bin/tmp_ar

clean:
	rm -f src/*.o src/*.d tests/*.o tests/*.d *.out *.so
	rm -rf bin/*

.PHONY: abiname
abiname:
	@echo $(ABI)

DEBUG = #-DDEBUG_DUMP

# macOS doesn't support static linking
STATIC_FLAG := -static
ifeq ($(shell uname -s),Darwin)
	STATIC_FLAG :=
endif

.PHONY: test tests format check-format docs docs-clean docs-check sync-version check-version-sync \
	roll-changelog check-changelog-roll rollup

# Version metadata synchronization
# Authoritative source: src/tealet.h (TEALET_VERSION)
# Mirrored fields that must match:
#   - Makefile: VERSION = ...
#   - README.md: **Version x.y.z**
#   - Doxyfile: PROJECT_NUMBER = ...
# Use `make sync-version` after updating TEALET_VERSION in src/tealet.h.
# Use `make check-version-sync` in CI/release checks to prevent drift.

check-version-sync:
	@$(SCRIPT_DIR)/sync-version.sh -check

sync-version:
	@$(SCRIPT_DIR)/sync-version.sh

# Changelog rolling helpers
# Usage:
#   make roll-changelog ROLL_VERSION=0.5.2 ROLL_DATE=2026-05-08
# Defaults:
#   ROLL_VERSION defaults to VERSION from this Makefile
#   ROLL_DATE defaults to today's date (YYYY-MM-DD)
#
# Expected changelog conventions:
# - "## [Unreleased]" section exists at top.
# - Footer link exists as: [Unreleased]: .../compare/vX.Y.Z...HEAD
#
# Behavior:
# - Inserts "## [ROLL_VERSION] - ROLL_DATE" immediately after Unreleased.
# - Repoints [Unreleased] link to compare/vROLL_VERSION...HEAD.
# - Adds [ROLL_VERSION] compare link from previous unreleased base to ROLL_VERSION.
ROLL_VERSION ?= $(VERSION)
ROLL_DATE ?= $(shell date +%F)

check-changelog-roll:
	@$(SCRIPT_DIR)/roll-changelog.sh -check

roll-changelog: check-changelog-roll
	@$(SCRIPT_DIR)/roll-changelog.sh "$(ROLL_VERSION)" "$(ROLL_DATE)"

# Release metadata rollup helper:
# - validates/syncs version metadata from src/tealet.h
# - rolls CHANGELOG.md using ROLL_VERSION/ROLL_DATE
#
# Example:
#   make rollup ROLL_VERSION=0.5.2 ROLL_DATE=2026-05-08
rollup: sync-version roll-changelog
	@echo "*** Rollup complete for $(ROLL_VERSION) ($(ROLL_DATE)) ***"

tests: bin/test-static bin/test-dynamic
tests: bin/test-setcontext bin/test-chunks bin/test-stochastic bin/test-fork bin/test-config
tests: export LD_RUN_PATH := bin

test: tests
	$(EMULATOR) bin/test-static > /dev/null
ifndef EMULATOR
	$(EMULATOR) bin/test-dynamic > /dev/null
endif
	$(EMULATOR) bin/test-setcontext > /dev/null
	$(EMULATOR) bin/test-stochastic -n 100 > /dev/null
	$(EMULATOR) bin/test-fork
	$(EMULATOR) bin/test-config
	@echo "*** All test suites passed ***"

format:
	@command -v $(CLANG_FORMAT) >/dev/null 2>&1 || (echo "ERROR: $(CLANG_FORMAT) not found" && exit 1)
	$(CLANG_FORMAT) -i $(FORMAT_FILES)

check-format:
	@command -v $(CLANG_FORMAT) >/dev/null 2>&1 || (echo "ERROR: $(CLANG_FORMAT) not found" && exit 1)
	$(CLANG_FORMAT) --dry-run --Werror $(FORMAT_FILES)

docs:
	@command -v $(DOXYGEN) >/dev/null 2>&1 || (echo "ERROR: $(DOXYGEN) not found" && exit 1)
	@rm -rf $(DOC_OUTPUT_DIR)
	@mkdir -p $(DOC_OUTPUT_DIR)
	$(DOXYGEN) $(DOXYFILE)
	@echo "*** Docs generated at $(DOC_OUTPUT_DIR)/html/index.html ***"

docs-clean:
	rm -rf $(DOC_OUTPUT_DIR)

docs-check:
	@command -v $(DOXYGEN) >/dev/null 2>&1 || (echo "ERROR: $(DOXYGEN) not found" && exit 1)
	@rm -rf $(DOC_OUTPUT_DIR)
	@mkdir -p $(DOC_OUTPUT_DIR)
	$(DOXYGEN) $(DOXYFILE)
	@test ! -s $(DOC_OUTPUT_DIR)/warnings.log || (echo "ERROR: Doxygen warnings detected:" && cat $(DOC_OUTPUT_DIR)/warnings.log && exit 1)
	@echo "*** Docs check passed (no Doxygen warnings) ***"

# Multiple chunks and sharing test
bin/test-chunks: bin tests/test_chunks.o bin/libtealet.a
	$(CC) $(LDFLAGS) $(STATIC_FLAG) -o $@ tests/test_chunks.o -ltealet

tests/test_chunks.o: tests/test_chunks.c src/tealet.h
	$(CC) $(CPPFLAGS) $(CFLAGS) $(DEPFLAGS) -c -o $@ tests/test_chunks.c

# Stochastic switching test
bin/test-stochastic: bin tests/test_stochastic.o bin/libtealet.a
	$(CC) $(LDFLAGS) $(STATIC_FLAG) -o $@ tests/test_stochastic.o -ltealet

tests/test_stochastic.o: tests/test_stochastic.c src/tealet.h
	$(CC) $(CPPFLAGS) $(CFLAGS) $(DEPFLAGS) -c -o $@ tests/test_stochastic.c

# Fork test
bin/test-fork: bin tests/test_fork.o bin/libtealet.a
	$(CC) $(LDFLAGS) $(STATIC_FLAG) -o $@ tests/test_fork.o -ltealet

tests/test_fork.o: tests/test_fork.c src/tealet.h
	$(CC) $(CPPFLAGS) $(CFLAGS) $(DEPFLAGS) -c -o $@ tests/test_fork.c

# Configure API test
bin/test-config: bin tests/test_config.o bin/libtealet.a
	$(CC) $(LDFLAGS) $(STATIC_FLAG) -o $@ tests/test_config.o -ltealet

tests/test_config.o: tests/test_config.c src/tealet.h
	$(CC) $(CPPFLAGS) $(CFLAGS) $(DEPFLAGS) -c -o $@ tests/test_config.c

# Current tealet test
bin/test-current: bin tests/test_current.o bin/libtealet.a
	$(CC) $(LDFLAGS) $(STATIC_FLAG) -o $@ tests/test_current.o -ltealet

tests/test_current.o: tests/test_current.c src/tealet.h
	$(CC) $(CPPFLAGS) $(CFLAGS) $(DEPFLAGS) -c -o $@ tests/test_current.c

# Load compiler-generated header dependencies when present.
-include $(wildcard src/*.d tests/*.d)

tests/setcontext.o: tests/setcontext.c src/tealet.h
	$(CC) $(CPPFLAGS) $(CFLAGS) $(DEPFLAGS) -c -o $@ tests/setcontext.c

bin/test-setcontext: bin tests/setcontext.o bin/libtealet.so
	$(CC) $(LDFLAGS) $(STATIC_FLAG) -o $@ tests/setcontext.o ${DEBUG} -ltealet

tests/tests.o: tests/tests.c src/tealet.h
	$(CC) $(CPPFLAGS) $(CFLAGS) $(DEPFLAGS) -c -o $@ tests/tests.c

tests/test_locking.o: tests/test_locking.c src/tealet.h
	$(CC) $(CPPFLAGS) $(CFLAGS) $(DEPFLAGS) -c -o $@ tests/test_locking.c

tests/test_transfer.o: tests/test_transfer.c src/tealet.h
	$(CC) $(CPPFLAGS) $(CFLAGS) $(DEPFLAGS) -c -o $@ tests/test_transfer.c

tests/test_stress.o: tests/test_stress.c src/tealet.h
	$(CC) $(CPPFLAGS) $(CFLAGS) $(DEPFLAGS) -c -o $@ tests/test_stress.c

tests/test_resilience.o: tests/test_resilience.c src/tealet.h
	$(CC) $(CPPFLAGS) $(CFLAGS) $(DEPFLAGS) -c -o $@ tests/test_resilience.c

tests/test_lifecycle.o: tests/test_lifecycle.c src/tealet.h
	$(CC) $(CPPFLAGS) $(CFLAGS) $(DEPFLAGS) -c -o $@ tests/test_lifecycle.c

tests/test_stack.o: tests/test_stack.c src/tealet.h
	$(CC) $(CPPFLAGS) $(CFLAGS) $(DEPFLAGS) -c -o $@ tests/test_stack.c

tests/test_stats_extra.o: tests/test_stats_extra.c src/tealet.h
	$(CC) $(CPPFLAGS) $(CFLAGS) $(DEPFLAGS) -c -o $@ tests/test_stats_extra.c

bin/test-static: bin tests/tests.o tests/test_locking.o tests/test_transfer.o tests/test_stress.o tests/test_resilience.o tests/test_lifecycle.o tests/test_stack.o tests/test_stats_extra.o bin/libtealet.a
	$(CC) $(LDFLAGS) $(STATIC_FLAG) -o $@ tests/tests.o tests/test_locking.o tests/test_transfer.o tests/test_stress.o tests/test_resilience.o tests/test_lifecycle.o tests/test_stack.o tests/test_stats_extra.o ${DEBUG} -ltealet

bin/test-dynamic: bin tests/tests.o tests/test_locking.o tests/test_transfer.o tests/test_stress.o tests/test_resilience.o tests/test_lifecycle.o tests/test_stack.o tests/test_stats_extra.o bin/libtealet.so
	$(CC) $(LDFLAGS) -g -o $@ tests/tests.o tests/test_locking.o tests/test_transfer.o tests/test_stress.o tests/test_resilience.o tests/test_lifecycle.o tests/test_stack.o tests/test_stats_extra.o ${DEBUG} -ltealet

# Sanitizer tests - run on single platform for sanity checking
.PHONY: test-sanitizers test-ubsan test-valgrind

# Note: AddressSanitizer is incompatible with stack-slicing.
# ASan tracks stack boundaries and reports false positives when we
# save/restore stack data to/from the heap. Use Valgrind instead for
# memory leak detection.

# UndefinedBehaviorSanitizer - detects undefined behavior
test-ubsan: clean
	@echo "=== Building with UndefinedBehaviorSanitizer ==="
	$(MAKE) bin/test-static bin/test-setcontext bin/test-stochastic bin/test-fork \
		TEALET_WITH_TESTING=$(TEALET_WITH_TESTING) \
		CFLAGS="-fPIC -Wall $(PLATFORMFLAGS) -g -fsanitize=undefined -fno-omit-frame-pointer" \
		LDFLAGS="-Lbin -L$(LIB) $(PLATFORMFLAGS) -fsanitize=undefined" \
		STATIC_FLAG=""
	@echo "=== Running tests with UBSan ==="
	UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 bin/test-static
	LD_LIBRARY_PATH=bin UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 bin/test-setcontext
	LD_LIBRARY_PATH=bin UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 bin/test-stochastic -n 100
	LD_LIBRARY_PATH=bin UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 bin/test-fork
	@echo "*** UBSan tests passed ***"

# Valgrind - comprehensive memory checking
test-valgrind: clean tests
	@echo "=== Running tests with Valgrind ==="
	@which valgrind > /dev/null || (echo "ERROR: valgrind not installed" && exit 1)
	valgrind --leak-check=full --show-leak-kinds=all --error-exitcode=1 \
		--errors-for-leak-kinds=all --undef-value-errors=no bin/test-static
	LD_LIBRARY_PATH=bin valgrind --leak-check=full --show-leak-kinds=all --error-exitcode=1 \
		--errors-for-leak-kinds=all --undef-value-errors=no bin/test-setcontext
	valgrind --leak-check=full --show-leak-kinds=all --error-exitcode=1 \
		--errors-for-leak-kinds=all --undef-value-errors=no bin/test-stochastic -n 100
	valgrind --leak-check=full --show-leak-kinds=all --error-exitcode=1 \
		--errors-for-leak-kinds=all --undef-value-errors=no bin/test-fork
	@echo "*** Valgrind tests passed ***"

# Run all sanitizer tests
test-sanitizers: test-ubsan test-valgrind
	@echo "*** All sanitizer tests passed ***"
