SRC_DIR := ../../src
BUILD_DIR := build
CXX ?= g++
CXXFLAGS ?= -std=c++17 -g -O0 -I$(SRC_DIR)

SRCS := $(wildcard $(SRC_DIR)/*.cpp) test_parser.cpp
OBJS := $(addprefix $(BUILD_DIR)/,$(notdir $(SRCS:.cpp=.o)))

$(BUILD_DIR)/test_parser: $(OBJS)
	$(CXX) $(CXXFLAGS) -o $@ $(OBJS)

$(BUILD_DIR)/%.o: $(SRC_DIR)/%.cpp | $(BUILD_DIR)
	$(CXX) $(CXXFLAGS) -c $< -o $@

$(BUILD_DIR)/%.o: %.cpp | $(BUILD_DIR)
	$(CXX) $(CXXFLAGS) -c $< -o $@

$(BUILD_DIR):
	mkdir -p $(BUILD_DIR)

run: $(BUILD_DIR)/test_parser
	./$(BUILD_DIR)/test_parser

# ArduinoJson-dependent test for updateJsonParameters() (see
# test_json.cpp) -- separate build dir/flags so the default `run` target
# above never needs __JSON_OPTION__ or ArduinoJson at all.
JSON_BUILD_DIR := build-json
JSON_SRCS := $(wildcard $(SRC_DIR)/*.cpp) test_json.cpp
JSON_OBJS := $(addprefix $(JSON_BUILD_DIR)/,$(notdir $(JSON_SRCS:.cpp=.o)))
JSON_CXXFLAGS = -std=c++17 -g -O0 -D__JSON_OPTION__ -I$(SRC_DIR) -I$(ARDUINOJSON_DIR)

run-json: $(JSON_BUILD_DIR)/test_json
	./$(JSON_BUILD_DIR)/test_json

$(JSON_BUILD_DIR)/test_json: $(JSON_OBJS)
	$(CXX) $(JSON_CXXFLAGS) -o $@ $(JSON_OBJS)

$(JSON_BUILD_DIR)/%.o: $(SRC_DIR)/%.cpp | $(JSON_BUILD_DIR)
	$(CXX) $(JSON_CXXFLAGS) -c $< -o $@

$(JSON_BUILD_DIR)/%.o: %.cpp | $(JSON_BUILD_DIR)
	$(CXX) $(JSON_CXXFLAGS) -c $< -o $@

$(JSON_BUILD_DIR):
	mkdir -p $(JSON_BUILD_DIR)

# sc_examples pipeline check (see test_sc_examples.cpp) -- compiles the
# real *.sc scripts from the sibling v1 repo's sc_examples/ through the
# full parse->createBinary->createExecutableFromBinary pipeline. Separate
# binary/build dir from the default `run` target, same reasoning as
# run-json above: keeps test_parser.cpp's 30-odd hand-written tests free
# of a second main().
SC_BUILD_DIR := build-sc
SC_SRCS := $(wildcard $(SRC_DIR)/*.cpp) test_sc_examples.cpp
SC_OBJS := $(addprefix $(SC_BUILD_DIR)/,$(notdir $(SC_SRCS:.cpp=.o)))

run-sc: $(SC_BUILD_DIR)/test_sc_examples
	./$(SC_BUILD_DIR)/test_sc_examples

$(SC_BUILD_DIR)/test_sc_examples: $(SC_OBJS)
	$(CXX) $(CXXFLAGS) -o $@ $(SC_OBJS)

$(SC_BUILD_DIR)/%.o: $(SRC_DIR)/%.cpp | $(SC_BUILD_DIR)
	$(CXX) $(CXXFLAGS) -c $< -o $@

$(SC_BUILD_DIR)/%.o: %.cpp | $(SC_BUILD_DIR)
	$(CXX) $(CXXFLAGS) -c $< -o $@

$(SC_BUILD_DIR):
	mkdir -p $(SC_BUILD_DIR)

# Large-script / resource-budget check (see test_large_script.cpp) --
# compiles fixtures/multi_effect_controller.sc, a purpose-written
# several-hundred-line script, and checks the resulting binary against
# an ESP32-without-PSRAM size budget. Separate binary/build dir, same
# reasoning as run-json/run-sc above.
LARGE_BUILD_DIR := build-large
LARGE_SRCS := $(wildcard $(SRC_DIR)/*.cpp) test_large_script.cpp alloc_tracker.cpp
# The compiler's own allocations go through alloc_tracker.cpp (built
# without these) so the test can measure its heap use.
TRACK_FLAGS := -Dmalloc=trk_malloc -Dcalloc=trk_calloc -Drealloc=trk_realloc -Dfree=trk_free \
               -Dstrdup=trk_strdup -Dstrndup=trk_strndup
LARGE_OBJS := $(addprefix $(LARGE_BUILD_DIR)/,$(notdir $(LARGE_SRCS:.cpp=.o)))

run-large: $(LARGE_BUILD_DIR)/test_large_script
	./$(LARGE_BUILD_DIR)/test_large_script

$(LARGE_BUILD_DIR)/test_large_script: $(LARGE_OBJS)
	$(CXX) $(CXXFLAGS) -o $@ $(LARGE_OBJS)

$(LARGE_BUILD_DIR)/%.o: $(SRC_DIR)/%.cpp | $(LARGE_BUILD_DIR)
	$(CXX) $(CXXFLAGS) $(TRACK_FLAGS) -c $< -o $@

$(LARGE_BUILD_DIR)/alloc_tracker.o: alloc_tracker.cpp alloc_tracker.h | $(LARGE_BUILD_DIR)
	$(CXX) $(CXXFLAGS) -c $< -o $@

$(LARGE_BUILD_DIR)/%.o: %.cpp | $(LARGE_BUILD_DIR)
	$(CXX) $(CXXFLAGS) $(TRACK_FLAGS) -c $< -o $@

$(LARGE_BUILD_DIR):
	mkdir -p $(LARGE_BUILD_DIR)

# Optimized-build check (-Os, matching Arduino's default optimization
# level) -- every other target here uses -O0, which never defines GCC's
# own builtin __OPTIMIZE__ macro. optimize.h's include guard used to be
# named __OPTIMIZE__ too, so under any real -O1/-O2/-Os build (i.e. every
# Arduino IDE build, but no build in this Makefile) it collided with that
# builtin and was always already "defined", silently skipping the whole
# header (its #include "stackfunctions.h" and the optimize() declaration
# itself) -- "Text was not declared", "optimize was not declared". Never
# caught by `run`/`run-sc`/`run-large`/`run-json` above since none of them
# ever pass an optimization flag; only found by actually building a real
# example with the Arduino toolchain's default -Os. This target exists so
# that class of bug (an include guard colliding with a compiler-builtin
# macro name) gets caught here too, not just by chance on a real device
# build.
OPT_BUILD_DIR := build-optimized
OPT_SRCS := $(wildcard $(SRC_DIR)/*.cpp) test_parser.cpp
OPT_OBJS := $(addprefix $(OPT_BUILD_DIR)/,$(notdir $(OPT_SRCS:.cpp=.o)))
OPT_CXXFLAGS := -std=c++17 -Os -I$(SRC_DIR)

check-optimized: $(OPT_BUILD_DIR)/test_parser
	./$(OPT_BUILD_DIR)/test_parser >/dev/null
	@echo "check-optimized: -Os build compiles and runs the full suite cleanly"

$(OPT_BUILD_DIR)/test_parser: $(OPT_OBJS)
	$(CXX) $(OPT_CXXFLAGS) -o $@ $(OPT_OBJS)

$(OPT_BUILD_DIR)/%.o: $(SRC_DIR)/%.cpp | $(OPT_BUILD_DIR)
	$(CXX) $(OPT_CXXFLAGS) -c $< -o $@

$(OPT_BUILD_DIR)/%.o: %.cpp | $(OPT_BUILD_DIR)
	$(CXX) $(OPT_CXXFLAGS) -c $< -o $@

$(OPT_BUILD_DIR):
	mkdir -p $(OPT_BUILD_DIR)

# AddressSanitizer check -- every other target's plain host malloc (glibc/
# macOS) tolerates realloc(ptr, 0) by historically returning a small
# non-NULL pointer, which is *not* something the C standard guarantees:
# realloc(ptr, 0) is explicitly permitted to return NULL, the same way
# free(ptr) would. vect<T>::erase() used to call p_realloc() (whose
# assert(tmp!=NULL) treats any NULL return as an allocation failure) with
# a 0-byte request whenever it removed the last remaining element, which
# `run` above's plain malloc silently absorbed -- but ASan's allocator
# (and, confirmed separately, real ESP32 hardware) both return NULL there,
# exactly as the standard allows, aborting the program for a script as
# trivial as a single fibonacci() function. `run`'s 36/36 passing never
# caught this; this target does, immediately. Fixed in vect.h's erase()
# to special-case shrinking to zero the same way pop_back()/pop_front()/
# shrink_to_fit() already did.
ASAN_BUILD_DIR := build-asan
ASAN_SRCS := $(wildcard $(SRC_DIR)/*.cpp) test_parser.cpp
ASAN_OBJS := $(addprefix $(ASAN_BUILD_DIR)/,$(notdir $(ASAN_SRCS:.cpp=.o)))
ASAN_CXXFLAGS := -std=c++17 -g -O0 -fsanitize=address -fno-omit-frame-pointer -I$(SRC_DIR)

check-asan: $(ASAN_BUILD_DIR)/test_parser
	ASAN_OPTIONS=detect_leaks=0 ./$(ASAN_BUILD_DIR)/test_parser >/dev/null
	@echo "check-asan: AddressSanitizer build runs the full suite cleanly"

$(ASAN_BUILD_DIR)/test_parser: $(ASAN_OBJS)
	$(CXX) $(ASAN_CXXFLAGS) -o $@ $(ASAN_OBJS)

$(ASAN_BUILD_DIR)/%.o: $(SRC_DIR)/%.cpp | $(ASAN_BUILD_DIR)
	$(CXX) $(ASAN_CXXFLAGS) -c $< -o $@

$(ASAN_BUILD_DIR)/%.o: %.cpp | $(ASAN_BUILD_DIR)
	$(CXX) $(ASAN_CXXFLAGS) -c $< -o $@

$(ASAN_BUILD_DIR):
	mkdir -p $(ASAN_BUILD_DIR)

clean:
	rm -rf $(BUILD_DIR) $(JSON_BUILD_DIR) $(SC_BUILD_DIR) $(LARGE_BUILD_DIR) $(OPT_BUILD_DIR) $(ASAN_BUILD_DIR)

.PHONY: run run-json run-sc run-large check-optimized check-asan clean
