NPROC != nproc
MAKEFLAGS += -j$(NPROC)
MAKEFLAGS += -r -R

DEFAULT_OPTLEVEL := g
DEFAULT_PREFIX := /usr/local
DEFAULT_TYPE := default
DEFAULT_USE_CCACHE := y
DEFAULT_EMIT_LLVM := n
DEFAULT_BUILD_FROM_ASM := n

.PHONY: $(shell grep -ho "^[0-9a-z-]\\+:" $(MAKEFILE_LIST) | sed -e "s/://")

define ERROR_INVALID_VALUE
  $(error invalid value for $$($1): expected one of $(or $2,[yn]) but got $($1))
endef

CLANG21 != command -v clang-21
CLANG20 != command -v clang-20
CLANG19 != command -v clang-19
CLANG != command -v clang

CC := $(or $(CLANG21),$(CLANG20),$(CLANG19),$(CLANG),$(error CC not found: install clang (>=19)))

USE_CCACHE ?= $(DEFAULT_USE_CCACHE) ## use ccache [yn] (default: y)
ifeq ($(strip $(USE_CCACHE)),y)
  CCACHE != command -v ccache
  CC := $(CCACHE) $(CC)
else ifeq ($(strip $(USE_CCACHE)),n)
else
  $(call ERROR_INVALID_VALUE,USE_CCACHE)
endif

PROJECT_NAME := $(notdir $(CURDIR))
CDIR := src
INCDIR := include
BUILDDIR := .build
PREFIX ?= $(DEFAULT_PREFIX) ## install prefix (default: /usr/local)
OPTLEVEL ?= $(or $(OL), $(DEFAULT_OPTLEVEL)) ## optimization level [0-3|g] (default: g)
LOGLEVEL ?= $(or $(LL), $(DEFAULT_LOGLEVEL))
TYPE ?= $(or $(T), $(DEFAULT_TYPE)) ## build type [test|bench|default]

# compiler flags
CFLAGS != cat build/cflags

OPTFLAGS != cat build/optflags

# linker flags
LDFLAGS != cat build/ldflags
OPTLDFLAGS != cat build/optldflags
DEBUGFLAGS != cat build/debugflags
ASMFLAGS != cat build/asmflags

# Enables macro in the source
CFLAGS += -DLOGLEVEL=$(LOGLEVEL)

ifdef ASAN
  CFLAGS += -fsanitize=$(ASAN)
  LDFLAGS += -fsanitize=$(ASAN)
endif

ifeq ($(MAKECMDGOALS),coverage)
  CFLAGS += -fprofile-instr-generate -fcoverage-mapping
  LDFLAGS += -fprofile-instr-generate
  TYPE := test
  OPTLEVEL := 0
endif

ifeq ($(strip $(TYPE)),test)
  CFLAGS += -DTEST_MODE
else ifeq ($(strip $(TYPE)),bench)
  CFLAGS += -DBENCHMARK_MODE
else ifeq ($(strip $(TYPE)),default)
else
  $(call ERROR_INVALID_VALUE,TYPE,[test|bench|default])
endif

ifeq ($(strip $(OPTLEVEL)),g)
  CFLAGS += $(DEBUGFLAGS)
  LDFLAGS += $(DEBUGFLAGS)
  RUNNER ?= gdb ## runner (default in debug run: gdb)
else ifneq ($(filter 1 2 3,$(OPTLEVEL)),)
  CFLAGS += $(OPTFLAGS) -O$(OPTLEVEL)
  LDFLAGS += $(OPTLDFLAGS) -O$(OPTLEVEL)
else ifeq ($(strip $(OPTLEVEL)),0)
else
  $(call ERROR_INVALID_VALUE,OPTLEVEL,[0-3|g])
endif

ifeq ($(MAKECMDGOALS),profile)
  CFLAGS += -pg
  LDFLAGS += -pg
  LDFLAGS := $(filter-out -s,$(LDFLAGS))
endif

ifdef TEST_FILTER
  CFLAGS += -DTEST_FILTER="\"$(TEST_FILTER)\""
endif

# generate output path
GITBRANCH != git branch --show-current 2>/dev/null
SEED = $(CC)$(CFLAGS)$(LDFLAGS)$(GITBRANCH)$(IN_NIX_SHELL)
HASH != echo '$(SEED)' | md5sum | cut -d' ' -f1
OUTDIR := $(BUILDDIR)/$(HASH)

TARGET := $(OUTDIR)/$(PROJECT_NAME)

EMIT_LLVM ?= $(DEFAULT_EMIT_LLVM) ## use llvmIR instead of asm [yn] (default: n)
ifeq ($(strip $(EMIT_LLVM)),y)
  ASMFLAGS += -emit-llvm
  ASMEXT := ll
else ifeq ($(strip $(EMIT_LLVM)),n)
  ASMEXT := s
else
  $(call ERROR_INVALID_VALUE,EMIT_LLVM)
endif

# source files
SRCS := $(wildcard $(CDIR)/*.c)
OBJS := $(patsubst $(CDIR)/%.c,$(OUTDIR)/%.o,$(SRCS))
DEPS := $(OBJS:.o=.d)
ASMS := $(OBJS:.o=.$(ASMEXT))

# e.g.)
# $ make asm OL=3
# $ # edit asm files...
# $ make BUILD_FROM_ASM=y OL=3
BUILD_FROM_ASM ?= $(DEFAULT_BUILD_FROM_ASM) ## use asm instead of c files [yn] (default: n)
ifeq ($(strip $(BUILD_FROM_ASM)),y)
  SRCDIR := $(OUTDIR)
  SRCEXT := $(ASMEXT)
  CFLAGS =
else ifeq ($(strip $(BUILD_FROM_ASM)),n)
  SRCDIR := $(CDIR)
  SRCEXT := c
else
  $(call ERROR_INVALID_VALUE,BUILD_FROM_ASM)
endif

include $(realpath $(DEPS))

### build rules
.DEFAULT_GOAL := $(TARGET)

# link
$(TARGET): $(OBJS)
	$(CC) $(LDFLAGS) $^ -o $@

# compile
$(OBJS): $(OUTDIR)/%.o: $(SRCDIR)/%.$(SRCEXT) $(OUTDIR)/%.d | $(OUTDIR)/
	$(CC) $< $(CFLAGS) -c -o $@

define DEPFLAGS
  -MM -MP -MT $1.o -MF $1.d
endef

$(DEPS): $(OUTDIR)/%.d: $(CDIR)/%.c | $(OUTDIR)/
	$(CC) $< $(CFLAGS) $(call DEPFLAGS,$(@:.d=))

# e.g.) run with valgrind
# make run RUNNER=valgrind
# e.g.) don't use gdb (default debug RUNNER) in debug run
# make run RUNNER=
run: $(TARGET) ## run target
	$(RUNNER) $<

# `make run-foo` is same as `make run RUNNER=foo`
run-%: $(TARGET)
	$* $<

test: ; $(MAKE) run TYPE=test RUNNER= ## run test

asm: $(ASMS) ## generate asm files

$(ASMS): $(OUTDIR)/%.$(ASMEXT): $(CDIR)/%.c | $(OUTDIR)/
	$(CC) $< $(ASMFLAGS) $(CFLAGS) -o $@

clean-all: ; rm -rf $(BUILDDIR)

# e.g.) remove test build for opt level 3
# make clean OPTLEVEL=3 TYPE=test
clean:
ifneq ($(realpath $(OUTDIR)),)
	rm -rf $(OUTDIR)
endif

$(PREFIX)/bin/$(PROJECT_NAME): $(TARGET) | $(PREFIX)/bin/
	cp $< $@

install: $(PREFIX)/bin/$(PROJECT_NAME)

uninstall: ; rm $(PREFIX)/bin/$(PROJECT_NAME)

doc: doc/Doxyfile ## generate doc
	doxygen $<

doc/Doxyfile: doc/
	doxygen -g $@

fmt: ; clang-format -Werror --dry-run $(SRCS) $(INCDIR)/*.h

lint: $(SRCS)
	clang-tidy $^ -- $(CFLAGS)
	flawfinder $^

check: $(SRCS)
	cppcheck $^ --enable=all --suppress=missingIncludeSystem -I$(INCDIR)

pre-commit: fmt test ## .git/hooks/pre-commit

FP ?= /dev/stdout
log: ## show build flags
	@echo "Compiler: $(CC)" > $(FP)
	@echo "CFLAGS: $(CFLAGS)" >> $(FP)
	@echo "LDFLAGS: $(LDFLAGS)" >> $(FP)
	@echo "TARGET: $(TARGET)" >> $(FP)
	@echo "SRCS: $(SRCS)" >> $(FP)
	@echo "OBJS: $(OBJS)" >> $(FP)
	@echo "DEPS: $(DEPS)" >> $(FP)

info: $(TARGET) ## show target info
	@echo "target file size:"
	@size $(TARGET)

help: ## show help
	@echo "$$ make          # debug build"
	@echo "$$ make test     # run test"
	@echo "$$ make run OL=3 # run release build"
	@echo
	@echo "build files: .build/HASH/*"
	@echo
	@echo "Variables:"
	@grep -h "^[^\t].* ## " $(MAKEFILE_LIST) \
	| sed -En "s/^ *([0-9A-Z_]+) .?= .*## (.*)$$/\\1 = \\2/p"
	@echo
	@echo "Targets:"
	@grep -h "^[^\t].* ## " $(MAKEFILE_LIST) \
	| sed -En "s/^([0-9a-z-]+): .*## (.*)$$/\\1: \\2/p"

### llmfile

LLMFILE ?= llmfile.txt
FILES ?= makefile build.zig doc/t.typ doc/readme.typ doc/manual.typ
DIRS ?= build include src
FILES_IN_DIRS := $(wildcard $(addsuffix /*, $(DIRS)))
SORTED_FILES_IN_DIRS := $(sort $(notdir $(basename $(FILES_IN_DIRS))))
REAL_PATH_FILES_IN_DIRS := $(foreach f,$(SORTED_FILES_IN_DIRS),$(wildcard */$f.[ch]))
LIST_FILES ?= $(FILES) $(REAL_PATH_FILES_IN_DIRS)
$(LLMFILE): $(LIST_FILES)
	echo $^ | sed 's/ /\n/g' > $@
	echo >> $@ # newline
	# `head` automatically inserts the file name at the start of the file
	head -n 9999 $^ >> $@

llmfile: $(LLMFILE) ## for the llm to read

### compiledb

, = ,
LOCAL_CCJ = $(OUTDIR)/compile_commands.json
define STRINGIFY
  $(foreach f,$1,"$f"$,)
endef
$(LOCAL_CCJ).%: | $(dir $(LOCAL_CCJ))
	echo '{"arguments":[' > $@
	echo '"$(lastword $(CC))",' >> $@
	echo '$(call STRINGIFY,$(CFLAGS))' >> $@
	echo '$(call STRINGIFY,$(call COMPILEFLAGS,$(OUTDIR)/$*.o))' >> $@
	echo '"$(SRCDIR)/$*.c"],' >> $@
	echo '"directory":"$(CURDIR)",' >> $@
	echo '"file":"$(CURDIR)/$(SRCDIR)/$*.c",' >> $@
	echo '"output":"$(CURDIR)/$(OUTDIR)/$*.o"},' >> $@

$(LOCAL_CCJ): $(addprefix $(LOCAL_CCJ)., $(basename $(notdir $(SRCS))))
	echo "[" > $@
	cat $^ >> $@
	sed -i "\$$s/,$$/]/" $@

compile_commands.json: $(LOCAL_CCJ)
	cp $< $@

compiledb: compile_commands.json ## for lsp

### coverage

COVDIR ?= $(OUTDIR)/coverage-report
PROFRAW := $(OUTDIR)/default.profraw
PROFDATA := $(OUTDIR)/coverage.profdata

$(PROFRAW): $(TARGET)
	LLVM_PROFILE_FILE=$@ ./$<

$(PROFDATA): $(PROFRAW)
	llvm-profdata merge --sparse $< -o $@

$(COVDIR): $(PROFDATA)
	llvm-cov show $(TARGET) --format=html --instr-profile=$< --output-dir=$@

BROWSER ?= cha
coverage: $(COVDIR) ## report test coverage
	$(BROWSER) $</index.html

### perf

$(OUTDIR)/perf.data: $(TARGET)
	perf record -o $@ -g ./$<

perf: $(OUTDIR)/perf.data ## run perf analysis
	perf report -i $<

### profile

gmon.out: $(TARGET) ; ./$<

$(OUTDIR)/gmon.out: gmon.out | $(OUTDIR)/
	mv $< $@

$(OUTDIR)/profile.txt: $(OUTDIR)/gmon.out
	gprof $(TARGET) $< > $@

profile: $(OUTDIR)/profile.txt
	less $<

### last resort

%/: ; mkdir -p $@