# McRogueFace Build Makefile
# Usage:
#   make              - Build for Linux (default)
#   make windows      - Cross-compile for Windows using MinGW (release)
#   make windows-debug - Cross-compile for Windows with console & debug symbols
#   make clean        - Clean Linux build
#   make clean-windows - Clean Windows build
#   make run          - Run the Linux build
#
# WebAssembly / Emscripten:
#   make wasm         - Build full game for web (requires emsdk activated)
#   make wasm-game    - Build game for web with fullscreen canvas (no REPL)
#   make playground   - Build minimal playground for web REPL
#   make serve        - Serve wasm build locally on port 8080
#   make serve-game   - Serve wasm-game build locally on port 8080
#   make clean-wasm   - Clean Emscripten builds
#
# Packaging:
#   make package-windows-light  - Windows with minimal stdlib (~5 MB)
#   make package-windows-full   - Windows with full stdlib (~15 MB)
#   make package-linux-light    - Linux with minimal stdlib
#   make package-linux-full     - Linux with full stdlib
#   make package-all            - All platform/preset combinations
#
# Release:
#   make version-bump NEXT_VERSION=x.y.z-suffix
#     Tags HEAD with current version, builds all packages, bumps to NEXT_VERSION

.PHONY: all linux windows windows-debug clean clean-windows clean-dist run
.PHONY: wasm wasm-game wasm-debug playground playground-debug serve serve-game serve-playground clean-wasm
.PHONY: package-windows-light package-windows-full package-linux-light package-linux-full package-all
.PHONY: version-bump
.PHONY: debug debug-test asan asan-test tsan tsan-test valgrind-test massif-test analyze clean-debug

# Number of parallel jobs for compilation
JOBS := $(shell nproc 2>/dev/null || echo 4)

all: linux

linux:
	@echo "Building McRogueFace for Linux..."
	@mkdir -p build
	@cd build && cmake .. -DCMAKE_BUILD_TYPE=Release && make -j$(JOBS)
	@echo "Build complete! Run with: ./build/mcrogueface"

windows:
	@echo "Cross-compiling McRogueFace for Windows..."
	@mkdir -p build-windows
	@cd build-windows && cmake .. \
		-DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/mingw-w64-x86_64.cmake \
		-DCMAKE_BUILD_TYPE=Release && make -j$(JOBS)
	@echo "Windows build complete! Output: build-windows/mcrogueface.exe"

windows-debug:
	@echo "Cross-compiling McRogueFace for Windows (debug with console)..."
	@mkdir -p build-windows-debug
	@cd build-windows-debug && cmake .. \
		-DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/mingw-w64-x86_64.cmake \
		-DCMAKE_BUILD_TYPE=Debug \
		-DMCRF_WINDOWS_CONSOLE=ON && make -j$(JOBS)
	@echo "Windows debug build complete! Output: build-windows-debug/mcrogueface.exe"
	@echo "Run from cmd.exe to see console output"

clean:
	@echo "Cleaning Linux build..."
	@rm -rf build

clean-windows:
	@echo "Cleaning Windows builds..."
	@rm -rf build-windows build-windows-debug

clean-dist:
	@echo "Cleaning distribution packages..."
	@rm -rf dist

clean-all: clean clean-windows clean-wasm clean-debug clean-dist
	@echo "All builds and packages cleaned."

run: linux
	@cd build && ./mcrogueface

# Debug and sanitizer targets
debug:
	@echo "Building McRogueFace with debug Python (pydebug assertions)..."
	@mkdir -p build-debug
	@cd build-debug && cmake .. \
		-DCMAKE_BUILD_TYPE=Debug \
		-DMCRF_DEBUG_PYTHON=ON && make -j$(JOBS)
	@echo "Debug build complete! Output: build-debug/mcrogueface"

debug-test: debug
	@echo "Running test suite with debug Python..."
	cd tests && MCRF_BUILD_DIR=../build-debug \
		MCRF_LIB_DIR=../__lib_debug \
		python3 run_tests.py -v

asan:
	@echo "Building McRogueFace with ASan + UBSan..."
	@mkdir -p build-asan
	@cd build-asan && cmake .. \
		-DCMAKE_BUILD_TYPE=Debug \
		-DMCRF_DEBUG_PYTHON=ON \
		-DMCRF_SANITIZE_ADDRESS=ON \
		-DMCRF_SANITIZE_UNDEFINED=ON && make -j$(JOBS)
	@echo "ASan build complete! Output: build-asan/mcrogueface"

asan-test: asan
	@echo "Running test suite under ASan + UBSan..."
	cd tests && MCRF_BUILD_DIR=../build-asan \
		MCRF_LIB_DIR=../__lib_debug \
		PYTHONMALLOC=malloc \
		ASAN_OPTIONS="detect_leaks=1:halt_on_error=1:print_summary=1" \
		LSAN_OPTIONS="suppressions=$(CURDIR)/sanitizers/asan.supp" \
		UBSAN_OPTIONS="print_stacktrace=1:halt_on_error=1" \
		python3 run_tests.py -v --sanitizer

# Fuzzing targets (clang-18 + libFuzzer + ASan + UBSan).
# Design: ONE instrumented executable `mcrfpy_fuzz` that embeds CPython,
# registers the mcrfpy module, and dispatches each libFuzzer iteration to
# a Python `fuzz_one_input(data)` function loaded from the script named by
# the MCRF_FUZZ_TARGET env var. libFuzzer instruments the C++ engine code
# where all the #258-#278 bugs live. No atheris dependency.
FUZZ_TARGETS := grid_entity property_types anim_timer_scene maps_procgen fov pathfinding_behavior
FUZZ_SECONDS ?= 30

# Shared env for running the fuzz binary. PYTHONHOME points at the build-fuzz
# copy of the bundled stdlib (post-build copied into build-fuzz/lib/).
# ASAN_OPTIONS: leak detection disabled because libFuzzer intentionally holds
# inputs for its corpus; abort_on_error ensures crashes are loud and repro-able.
define FUZZ_ENV
MCRF_LIB_DIR=../__lib_debug \
PYTHONMALLOC=malloc \
PYTHONHOME=../__lib/Python \
ASAN_OPTIONS="detect_leaks=0:halt_on_error=1:abort_on_error=1:print_stacktrace=1" \
UBSAN_OPTIONS="print_stacktrace=1:halt_on_error=1"
endef

fuzz-build:
	@echo "Building mcrfpy_fuzz with libFuzzer + ASan (clang-18)..."
	@mkdir -p build-fuzz
	@cd build-fuzz && CC=clang-18 CXX=clang++-18 cmake .. \
		-DCMAKE_BUILD_TYPE=Debug \
		-DMCRF_DEBUG_PYTHON=ON \
		-DMCRF_SANITIZE_ADDRESS=ON \
		-DMCRF_SANITIZE_UNDEFINED=ON \
		-DMCRF_FUZZER=ON \
		-DCMAKE_EXE_LINKER_FLAGS=-fuse-ld=lld && make -j$(JOBS) mcrfpy_fuzz
	@echo "Fuzz build complete! Output: build-fuzz/mcrfpy_fuzz"

fuzz: fuzz-build
	@for t in $(FUZZ_TARGETS); do \
		if [ ! -f tests/fuzz/fuzz_$$t.py ]; then \
			echo "SKIP: tests/fuzz/fuzz_$$t.py does not exist yet"; \
			continue; \
		fi; \
		echo "=== fuzzing $$t for $(FUZZ_SECONDS)s ==="; \
		mkdir -p tests/fuzz/corpora/$$t tests/fuzz/crashes; \
		( cd build-fuzz && $(FUZZ_ENV) MCRF_FUZZ_TARGET=$$t \
		  ./mcrfpy_fuzz \
		    -max_total_time=$(FUZZ_SECONDS) \
		    -artifact_prefix=../tests/fuzz/crashes/$$t- \
		    ../tests/fuzz/corpora/$$t ../tests/fuzz/seeds/$$t ) || exit 1; \
	done

fuzz-long: fuzz-build
	@test -n "$(TARGET)" || (echo "Usage: make fuzz-long TARGET=<name> SECONDS=<n>"; exit 1)
	@test -f tests/fuzz/fuzz_$(TARGET).py || (echo "No target: tests/fuzz/fuzz_$(TARGET).py"; exit 1)
	@mkdir -p tests/fuzz/corpora/$(TARGET) tests/fuzz/crashes
	@( cd build-fuzz && $(FUZZ_ENV) MCRF_FUZZ_TARGET=$(TARGET) \
	  ./mcrfpy_fuzz \
	    -max_total_time=$(or $(SECONDS),3600) \
	    -artifact_prefix=../tests/fuzz/crashes/$(TARGET)- \
	    ../tests/fuzz/corpora/$(TARGET) ../tests/fuzz/seeds/$(TARGET) )

fuzz-repro:
	@test -n "$(TARGET)" || (echo "Usage: make fuzz-repro TARGET=<name> CRASH=<path>"; exit 1)
	@test -n "$(CRASH)" || (echo "Usage: make fuzz-repro TARGET=<name> CRASH=<path>"; exit 1)
	@( cd build-fuzz && $(FUZZ_ENV) MCRF_FUZZ_TARGET=$(TARGET) \
	  ./mcrfpy_fuzz ../$(CRASH) )

clean-fuzz:
	@echo "Cleaning fuzz build and corpora..."
	@rm -rf build-fuzz tests/fuzz/corpora tests/fuzz/crashes

tsan:
	@echo "Building McRogueFace with TSan + free-threaded Python..."
	@echo "NOTE: Requires free-threaded debug Python built with:"
	@echo "  tools/build_debug_python.sh --tsan"
	@mkdir -p build-tsan
	@cd build-tsan && cmake .. \
		-DCMAKE_BUILD_TYPE=Debug \
		-DMCRF_FREE_THREADED_PYTHON=ON \
		-DMCRF_SANITIZE_THREAD=ON && make -j$(JOBS)
	@echo "TSan build complete! Output: build-tsan/mcrogueface"

tsan-test: tsan
	@echo "Running test suite under TSan..."
	cd tests && MCRF_BUILD_DIR=../build-tsan \
		MCRF_LIB_DIR=../__lib_debug \
		TSAN_OPTIONS="halt_on_error=1:second_deadlock_stack=1" \
		python3 run_tests.py -v --sanitizer

valgrind-test: debug
	@echo "Running test suite under Valgrind memcheck..."
	cd tests && MCRF_BUILD_DIR=../build-debug \
		MCRF_LIB_DIR=../__lib_debug \
		MCRF_TIMEOUT_MULTIPLIER=50 \
		PYTHONMALLOC=malloc \
		python3 run_tests.py -v --valgrind

massif-test: debug
	@echo "Running heap profiling under Valgrind Massif..."
	@mkdir -p build-debug
	cd build-debug && valgrind --tool=massif \
		--massif-out-file=massif.out \
		--pages-as-heap=no \
		--detailed-freq=10 \
		--max-snapshots=100 \
		./mcrogueface --headless --exec ../tests/benchmarks/stress_test_suite.py
	@echo "Massif output: build-debug/massif.out"
	@echo "View with: ms_print build-debug/massif.out"

analyze:
	@echo "Running cppcheck static analysis..."
	cppcheck --enable=warning,performance,portability \
		--suppress=missingIncludeSystem \
		--suppress=unusedFunction \
		--suppress=noExplicitConstructor \
		--suppress=missingOverride \
		--inline-suppr \
		-I src/ -I deps/ -I deps/cpython -I deps/Python \
		-I src/platform -I src/3d -I src/tiled -I src/ldtk -I src/audio \
		--std=c++20 \
		--quiet \
		src/ 2>&1
	@echo "Static analysis complete."

clean-debug:
	@echo "Cleaning debug/sanitizer builds..."
	@rm -rf build-debug build-asan build-tsan build-fuzz

# Packaging targets using tools/package.sh
package-windows-light: windows
	@./tools/package.sh windows light

package-windows-full: windows
	@./tools/package.sh windows full

package-linux-light: linux
	@./tools/package.sh linux light

package-linux-full: linux
	@./tools/package.sh linux full

package-all: windows linux
	@./tools/package.sh all

# Legacy target for backwards compatibility
package-windows: package-windows-full

# Emscripten / WebAssembly targets
# Requires: source ~/emsdk/emsdk_env.sh (or wherever your emsdk is installed)
#
# For iterative development, configure once then rebuild:
#   source ~/emsdk/emsdk_env.sh && emmake make -C build-emscripten
#
wasm:
	@if ! command -v emcmake >/dev/null 2>&1; then \
		echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
		exit 1; \
	fi
	@if [ ! -f build-emscripten/Makefile ]; then \
		echo "Configuring WebAssembly build (full game)..."; \
		mkdir -p build-emscripten; \
		cd build-emscripten && emcmake cmake .. \
			-DCMAKE_BUILD_TYPE=Release \
			-DMCRF_SDL2=ON; \
	fi
	@echo "Building McRogueFace for WebAssembly..."
	@emmake make -C build-emscripten -j$(JOBS)
	@echo "WebAssembly build complete! Files in build-emscripten/"
	@echo "Run 'make serve' to test locally"

playground:
	@if ! command -v emcmake >/dev/null 2>&1; then \
		echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
		exit 1; \
	fi
	@if [ ! -f build-playground/Makefile ]; then \
		echo "Configuring Playground build..."; \
		mkdir -p build-playground; \
		cd build-playground && emcmake cmake .. \
			-DCMAKE_BUILD_TYPE=Release \
			-DMCRF_SDL2=ON \
			-DMCRF_PLAYGROUND=ON; \
	fi
	@echo "Building McRogueFace Playground for WebAssembly..."
	@emmake make -C build-playground -j$(JOBS)
	@echo "Playground build complete! Files in build-playground/"
	@echo "Run 'make serve-playground' to test locally"

serve:
	@echo "Serving WebAssembly build at http://localhost:8080"
	@echo "Press Ctrl+C to stop"
	@cd build-emscripten && python3 -m http.server 8080

serve-playground:
	@echo "Serving Playground build at http://localhost:8080"
	@echo "Press Ctrl+C to stop"
	@cd build-playground && python3 -m http.server 8080

wasm-game:
	@if ! command -v emcmake >/dev/null 2>&1; then \
		echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
		exit 1; \
	fi
	@if [ ! -f build-wasm-game/Makefile ]; then \
		echo "Configuring WebAssembly game build (fullscreen, no REPL)..."; \
		mkdir -p build-wasm-game; \
		cd build-wasm-game && emcmake cmake .. \
			-DCMAKE_BUILD_TYPE=Release \
			-DMCRF_SDL2=ON \
			-DMCRF_GAME_SHELL=ON; \
	fi
	@echo "Building McRogueFace game for WebAssembly..."
	@emmake make -C build-wasm-game -j$(JOBS)
	@echo "Game build complete! Files in build-wasm-game/"
	@echo "Run 'make serve-game' to test locally"

serve-game:
	@echo "Serving game build at http://localhost:8080"
	@echo "Press Ctrl+C to stop"
	@cd build-wasm-game && python3 -m http.server 8080

wasm-demo:
	@if ! command -v emcmake >/dev/null 2>&1; then \
		echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
		exit 1; \
	fi
	@if [ ! -f build-wasm-demo/Makefile ]; then \
		echo "Configuring WebAssembly demo build..."; \
		mkdir -p build-wasm-demo; \
		cd build-wasm-demo && emcmake cmake .. \
			-DCMAKE_BUILD_TYPE=Release \
			-DMCRF_SDL2=ON \
			-DMCRF_DEMO=ON \
			-DMCRF_GAME_SHELL=ON; \
	fi
	@echo "Building McRogueFace demo for WebAssembly..."
	@emmake make -C build-wasm-demo -j$(JOBS)
	@cp web/index.html build-wasm-demo/index.html
	@echo "Demo build complete! Files in build-wasm-demo/"
	@echo "Run 'make serve-demo' to test locally"

serve-demo:
	@echo "Serving demo build at http://localhost:8080"
	@echo "Press Ctrl+C to stop"
	@cd build-wasm-demo && python3 -m http.server 8080

clean-wasm:
	@echo "Cleaning Emscripten builds..."
	@rm -rf build-emscripten build-playground build-wasm-game build-wasm-demo build-wasm-debug build-playground-debug

wasm-debug:
	@if ! command -v emcmake >/dev/null 2>&1; then \
		echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
		exit 1; \
	fi
	@if [ ! -f build-wasm-debug/Makefile ]; then \
		echo "Configuring WebAssembly debug build (DWARF + source maps)..."; \
		mkdir -p build-wasm-debug; \
		cd build-wasm-debug && emcmake cmake .. \
			-DCMAKE_BUILD_TYPE=Debug \
			-DMCRF_SDL2=ON \
			-DMCRF_WASM_DEBUG=ON; \
	fi
	@echo "Building McRogueFace for WebAssembly (debug)..."
	@emmake make -C build-wasm-debug -j$(JOBS)
	@echo "Debug WASM build complete! Files in build-wasm-debug/"
	@echo "Debug artifacts: .wasm.map (source map), .symbols (symbol map)"
	@echo "Run 'make serve-wasm-debug' to test locally"

serve-wasm-debug:
	@echo "Serving debug WASM build at http://localhost:8080"
	@echo "Press Ctrl+C to stop"
	@cd build-wasm-debug && python3 -m http.server 8080

playground-debug:
	@if ! command -v emcmake >/dev/null 2>&1; then \
		echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
		exit 1; \
	fi
	@if [ ! -f build-playground-debug/Makefile ]; then \
		echo "Configuring Playground debug build (DWARF + source maps)..."; \
		mkdir -p build-playground-debug; \
		cd build-playground-debug && emcmake cmake .. \
			-DCMAKE_BUILD_TYPE=Debug \
			-DMCRF_SDL2=ON \
			-DMCRF_PLAYGROUND=ON \
			-DMCRF_WASM_DEBUG=ON; \
	fi
	@echo "Building McRogueFace Playground for WebAssembly (debug)..."
	@emmake make -C build-playground-debug -j$(JOBS)
	@echo "Playground debug build complete! Files in build-playground-debug/"
	@echo "Run 'make serve-playground-debug' to test locally"

serve-playground-debug:
	@echo "Serving debug Playground build at http://localhost:8080"
	@echo "Press Ctrl+C to stop"
	@cd build-playground-debug && python3 -m http.server 8080

# Current version extracted from source
CURRENT_VERSION := $(shell grep 'MCRFPY_VERSION' src/McRogueFaceVersion.h | sed 's/.*"\(.*\)"/\1/')

# Release workflow: tag current version, build all packages, bump to next version
# Usage: make version-bump NEXT_VERSION=0.2.6-prerelease-7drl2026
version-bump:
ifndef NEXT_VERSION
	$(error Usage: make version-bump NEXT_VERSION=x.y.z-suffix)
endif
	@if ! command -v emcmake >/dev/null 2>&1; then \
		echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
		exit 1; \
	fi
	# git status (clean working dir check), but ignore modules/, because building submodules dirties their subdirs
	@if [ -n "$$(git status --porcelain | grep -v modules)" ]; then \
		echo "Error: Working tree is not clean. Commit or stash changes first."; \
		exit 1; \
	fi
	@echo "=== Releasing $(CURRENT_VERSION) ==="
	@# Idempotent tag: ok if it already points at HEAD (resuming partial run)
	@if git rev-parse "$(CURRENT_VERSION)" >/dev/null 2>&1; then \
		TAG_COMMIT=$$(git rev-parse "$(CURRENT_VERSION)^{}"); \
		HEAD_COMMIT=$$(git rev-parse HEAD); \
		if [ "$$TAG_COMMIT" != "$$HEAD_COMMIT" ]; then \
			echo "Error: Tag $(CURRENT_VERSION) already exists but points to a different commit."; \
			exit 1; \
		fi; \
		echo "Tag $(CURRENT_VERSION) already exists at HEAD (resuming)."; \
	else \
		git tag "$(CURRENT_VERSION)"; \
	fi
	$(MAKE) package-linux-full
	$(MAKE) package-windows-full
	$(MAKE) wasm
	@echo "Packaging WASM build..."
	@mkdir -p dist
	cd build-emscripten && zip -r ../dist/McRogueFace-$(CURRENT_VERSION)-WASM.zip \
		mcrogueface.html mcrogueface.js mcrogueface.wasm mcrogueface.data
	@echo ""
	@echo "Bumping version: $(CURRENT_VERSION) -> $(NEXT_VERSION)"
	@sed -i 's|MCRFPY_VERSION "$(CURRENT_VERSION)"|MCRFPY_VERSION "$(NEXT_VERSION)"|' src/McRogueFaceVersion.h
	@TAGGED_HASH=$$(git rev-parse --short HEAD); \
	git add src/McRogueFaceVersion.h && \
	git commit -m "Version bump: $(CURRENT_VERSION) ($$TAGGED_HASH) -> $(NEXT_VERSION)"
	@echo ""
	@echo "=== Release $(CURRENT_VERSION) complete ==="
	@echo "Tag:  $(CURRENT_VERSION)"
	@echo "Next: $(NEXT_VERSION)"
	@echo "Packages:"
	@ls -lh dist/*$(CURRENT_VERSION)* 2>/dev/null
