ifndef VERBOSE
MAKEFLAGS += --no-print-directory
endif

PRIV_DIR := $(abspath priv)
OBJ_DIR  := $(abspath obj)
DEBUG    ?= 0
REBAR    ?= rebar3
APP      := $(shell sed -nE 's/^\{application, ([a-zA-Z0-9_]+),.*/\1/p' src/*.app.src | head -n1)

OPTIMIZE ?= 0
ifneq ($(filter $(OPTIMIZE),1 true),)
override OPTIMIZE := 1
else
override OPTIMIZE := 0
endif

all: compile

help:
	@echo "Usage: make [target] [VAR=value ...]"
	@echo ""
	@echo "Build targets:"
	@echo "  all          Build NIF and compile Erlang (default)"
	@echo "  nif          Build only the C NIF shared library"
	@echo "  compile      Build NIF + run rebar3 compile"
	@echo "  optimize     PGO build: instrument → benchmark → rebuild (fastest binary)"
	@echo "  clean        Remove build artifacts"
	@echo "  distclean    Remove all generated files including deps"
	@echo ""
	@echo "Development:"
	@echo "  test         Run eunit test suite"
	@echo "  cover        Run eunit with coverage analysis and print a summary"
	@echo "  check        Run xref and dialyzer"
	@echo "  dialyzer     Run dialyzer"
	@echo "  memcheck     Build with ASan (-fsanitize=address) and run eunit (leak=1 adds LSan)"
	@echo "  benchmark    Run benchmarks via mix bench"
	@echo "  deps         Fetch mix dependencies"
	@echo "  doc          Generate documentation"
	@echo ""
	@echo "Publishing:"
	@echo "  bump-version Increment patch version and commit"
	@echo "  publish      Publish to hex.pm  (pass replace=1 to replace existing)"
	@echo "  deprecate    Deprecate a hex.pm release  (pass vsn=X.Y.Z)"
	@echo ""
	@echo "Variables:"
	@echo "  DEBUG=1      Build NIF without optimisations (-O0 -g)"
	@echo "  ASAN=1       Build with AddressSanitizer (implied by memcheck)"
	@echo "  leak=1       Enable LeakSanitizer during memcheck (off by default)"
	@echo "  VERBOSE=1    Show full compiler command lines"
	@echo "  OPTIMIZE=1   Make all/compile run the PGO 'optimize' build (same as 'make optimize')"

nif: $(PRIV_DIR)/glazer.so

$(PRIV_DIR)/glazer.so: $(wildcard c_src/*.cpp c_src/*.hpp)
	@$(MAKE) -C c_src PRIV_DIR=$(PRIV_DIR) OBJ_DIR=$(OBJ_DIR) DEBUG=$(DEBUG) \
	  $(if $(VERBOSE),VERBOSE=1,) --no-print-directory

ifeq ($(OPTIMIZE),1)
compile: optimize
else
compile: $(PRIV_DIR)/glazer.so
	$(REBAR) compile
endif

clean:
	$(REBAR) clean
	@$(MAKE) --no-print-directory -C c_src PRIV_DIR=$(PRIV_DIR) OBJ_DIR=$(OBJ_DIR) DEBUG=$(DEBUG) clean 2>/dev/null || true

distclean: clean
	@rm -rf obj priv/glazer.so _build .perf.txt

test:
	$(REBAR) eunit

# Locate the ASan runtime for LD_PRELOAD on Linux.
# macOS memcheck is not supported (DYLD_INSERT_LIBRARIES is dropped by the
# rebar3 shell wrapper before reaching erl), so this block is Linux-only.
# Try Clang's resource-dir path first; fall back to GCC's -print-file-name.
_CXX_RESOURCE_DIR := $(shell $(CXX) -print-resource-dir 2>/dev/null)
_HOST_ARCH        := $(shell uname -m)
_ASAN_CLANG       := $(_CXX_RESOURCE_DIR)/lib/linux/libclang_rt.asan-$(_HOST_ARCH).so
_ASAN_GCC         := $(shell $(CXX) -print-file-name=libasan.so 2>/dev/null)
ASAN_RT           := $(strip $(if $(wildcard $(_ASAN_CLANG)),$(_ASAN_CLANG),\
                       $(if $(filter-out $(notdir $(_ASAN_GCC)),$(_ASAN_GCC)),$(_ASAN_GCC))))
ASAN_PRELOAD       = LD_PRELOAD="$(ASAN_RT)"
LSAN_SUPPRESSIONS := $(abspath c_src/lsan_suppressions.txt)

leak ?= 0
ifeq ($(leak),0)
  DETECT_LEAKS := 0
else
  DETECT_LEAKS := 1
endif

check:
	$(REBAR) xref
	$(REBAR) dialyzer

memcheck:
	@echo "==> Building NIF with AddressSanitizer"
	@$(MAKE) -C c_src PRIV_DIR=$(PRIV_DIR) OBJ_DIR=$(OBJ_DIR) ASAN=1 \
	  $(if $(VERBOSE),VERBOSE=1,) clean all
	@$(REBAR) compile
	@echo "==> Running eunit under ASan$(if $(filter 1,$(DETECT_LEAKS)), + LeakSanitizer,) ($(ASAN_PRELOAD))"
	ERL_FLAGS="+A 1" ASAN_OPTIONS="detect_leaks=$(DETECT_LEAKS)" \
	  LSAN_OPTIONS="suppressions=$(LSAN_SUPPRESSIONS)" \
	  $(ASAN_PRELOAD) \
	  $(REBAR) eunit
	@echo "==> Rebuilding normal NIF (removing ASan instrumentation)"
	@$(MAKE) -C c_src PRIV_DIR=$(PRIV_DIR) OBJ_DIR=$(OBJ_DIR) \
	  $(if $(VERBOSE),VERBOSE=1,) clean all
	@$(REBAR) compile

doc docs:
	$(REBAR) ex_doc

# yaml_rustler and rusty_csv can't be resolved together (see mix.exs and the
# `deps` target below), so a full `make bench` run only includes whichever
# one BENCH_SET selects (neither, by default). Use `make bench-yaml` /
# `make bench-csv` (each sets BENCH_SET itself, see below) to benchmark
# against yaml_rustler/rusty_csv specifically; `make bench-json` needs
# neither and ignores BENCH_SET.
benchmark bench: do-bench

do-bench: deps
	@rm .perf.txt
	@$(MAKE) --no-print-directory optimize
	for b in json yaml csv; do \
	  $(MAKE) --no-print-directory bench-$$b | tee -a .perf.txt; \
	done

bench-yaml: BENCH_SET ?= yaml
bench-csv:  BENCH_SET ?= csv

bench-json bench-yaml bench-csv:
	@rm -f mix.lock
	@BENCH_SET=$(BENCH_SET) mix deps.get
	BENCH_SET=$(BENCH_SET) PARALLEL=$(if $(PARALLEL),$(PARALLEL),1) MIX_ENV=bench mix $@

# Profile-guided optimisation: instrument → run tests as workload → rebuild.
# Usage: make optimize
optimize:
	@echo "==> PGO step 1/3: build instrumented binary"
	@$(MAKE) -C c_src PRIV_DIR=$(PRIV_DIR) OBJ_DIR=$(OBJ_DIR) PGO=generate clean all
	@$(REBAR) compile
	@echo "==> PGO step 2/3: collect profile data"
	@./bin/pgo-profile.es
	@echo "==> PGO step 3/3: rebuild with profile data"
	@rm -f $(OBJ_DIR)/glazer_nif.o $(PRIV_DIR)/glazer.so
	@$(MAKE) -C c_src PRIV_DIR=$(PRIV_DIR) OBJ_DIR=$(OBJ_DIR) PGO=use all
	@$(REBAR) compile
	@echo "==> PGO build complete"

# BENCH_SET picks which of the two mutually-exclusive Rust NIF benchmark
# deps to resolve: yaml_rustler (BENCH_SET=yaml) or rusty_csv (BENCH_SET=csv).
# They can't be deps.get'd together because they pin incompatible :rustler
# versions (~> 0.34.0 vs ~> 0.37) — see mix.exs. `bench-json` and plain
# `bench`/`do-bench` need neither (mix.exs's bench_set_deps/0 returns [] for
# an unset/empty BENCH_SET), so whichever of the two was locked by a *prior*
# `bench-yaml`/`bench-csv` run must be purged first — otherwise mix.lock
# still pins it while it's absent from deps(), leaving a stale, half-built
# dependency in _build/bench (e.g. a missing .app file) that crashes
# `Mix.Task.run("app.start")`. Each branch below detects a leftover lock
# from the *other* mode (or, for an unset BENCH_SET, either mode) and forces
# mix to re-resolve from scratch.
deps:
	@if [ "$(BENCH_SET)" = "yaml" ] && grep -q '"rusty_csv"' mix.lock 2>/dev/null; then \
	  echo "==> BENCH_SET=yaml but mix.lock has rusty_csv locked; re-resolving"; \
	  rm -f mix.lock; \
	  rm -rf _build/bench deps/rusty_csv deps/rustler_precompiled deps/rustler; \
	elif [ "$(BENCH_SET)" = "csv" ] && grep -q '"yaml_rustler"' mix.lock 2>/dev/null; then \
	  echo "==> BENCH_SET=csv but mix.lock has yaml_rustler locked; re-resolving"; \
	  rm -f mix.lock; \
	  rm -rf _build/bench deps/yaml_rustler deps/rustler_precompiled deps/rustler; \
	elif [ -z "$(BENCH_SET)" ] && (grep -q '"rusty_csv"' mix.lock 2>/dev/null || grep -q '"yaml_rustler"' mix.lock 2>/dev/null); then \
	  echo "==> BENCH_SET unset but mix.lock has rusty_csv/yaml_rustler locked; re-resolving"; \
	  rm -f mix.lock; \
	  rm -rf _build/bench deps/rusty_csv deps/yaml_rustler deps/rustler_precompiled deps/rustler; \
	fi
	@BENCH_SET=$(BENCH_SET) mix deps.get

publish: docs
	$(REBAR) hex publish$(if $(replace), --replace)

deprecate:
	@if [ -z $(vsn) ]; then \
	  echo "Usage: $(MAKE) $@ vsn=X.Y.Z      - Deprecate version X.Y.Z"; \
	  exit 1; \
	fi
	$(REBAR) hex retire $(APP) $(vsn) deprecated --message Deprecated

cover:
	$(REBAR) cover --verbose

bump-version:
	@FILE=$$(ls -1 src/*.app.src | head -n1); \
	CURRENT=$$(grep -m1 '{vsn,' $$FILE | sed -E 's/.*"([0-9]+\.[0-9]+\.[0-9]+)".*/\1/'); \
	MAJOR=$$(echo $$CURRENT | cut -d. -f1); \
	MINOR=$$(echo $$CURRENT | cut -d. -f2); \
	PATCH=$$(echo $$CURRENT | cut -d. -f3); \
	NEW=$$(echo "$${MAJOR}.$${MINOR}.$$((PATCH + 1))" | tr -d '\n'); \
	echo "Bumping version from $${CURRENT} to $${NEW}"; \
	sed -i "s/{vsn, \"$${CURRENT}\"}/{vsn, \"$${NEW}\"}/" $$FILE; \
	echo "Changed: {vsn, \"$${CURRENT}\"} -> {vsn, \"$${NEW}\"}"; \
	sed -i 's/\({:\?glazer,[[:space:]]*"~>\)[^"]*/\1 '"$${MAJOR}.$${MINOR}"'/' README.md; \
	echo ""; \
	read -p "Commit this change? [Y/n] " -n 1 -r || true; \
	echo ""; \
	if [[ $$REPLY =~ ^[Yy]$$ ]] || [[ -z $$REPLY ]]; then \
	  git commit -am "Bump version to $${NEW}"; \
	fi

.PHONY: all help doc compile clean distclean test cover check dialyzer memcheck nif \
        optimize benchmark bench publish deprecate bump-version deps \
        bench-yaml bench-json bench-csv do-bench
