NAME := image-transformer

##### Compiler / analyzer common configuration.

CC = clang
LINKER = $(CC)

RM = rm -rf
MKDIR = mkdir -p

# Clang-tidy
CLANG_TIDY = clang-tidy

_noop =
_space = $(noop) $(noop)
_comma = ,

# Using `+=` to let user define their own checks in command line
CLANG_TIDY_CHECKS += $(strip $(file < clang-tidy-checks.txt))
CLANG_TIDY_ARGS = \
	-warnings-as-errors=* -header-filter="$(abspath $(INCDIR.main))/.*" \
	-checks="$(subst $(_space),$(_comma),$(CLANG_TIDY_CHECKS))" \

# Sanitizers
CFLAGS.none :=
CFLAGS.asan := -fsanitize=address
CFLAGS.lsan := -fsanitize=leak
CFLAGS.msan := -fsanitize=memory -fsanitize-memory-track-origins=2 -fno-omit-frame-pointer -fno-optimize-sibling-calls
CFLAGS.usan := -fsanitize=undefined

SANITIZER ?= none
ifeq ($(SANITIZER),)
override SANITIZER := none
endif

ifeq ($(words $(SANITIZER)),1)
ifeq ($(filter $(SANITIZER),all asan lsan msan usan none),)
$(error Please provide correct argument value for SANITIZER: all, asan, lsan, msan, usan or none)
endif
endif

# Using `+=` to let user define their own flags in command line
CFLAGS += $(CFLAGS.$(SANITIZER))
LDFLAGS += $(CFLAGS.$(SANITIZER))

ifeq ($(SANITIZER),none)
OBJDIR = obj
BUILDDIR = build
else
OBJDIR = obj/$(SANITIZER)
BUILDDIR = build/$(SANITIZER)
endif

##### Configuration for `main` target.

SOLUTION_DIR = solution

SRCDIR.main = $(SOLUTION_DIR)/src
INCDIR.main = $(SOLUTION_DIR)/include
OBJDIR.main = $(OBJDIR)/$(SOLUTION_DIR)

SOURCES.main += $(wildcard $(SRCDIR.main)/*.c) $(wildcard $(SRCDIR.main)/*/*.c)
TARGET.main  := $(BUILDDIR)/$(NAME)

CFLAGS.main += $(strip $(file < $(SOLUTION_DIR)/compile_flags.txt)) $(CFLAGS) -I$(INCDIR.main)

##### Configuration for `tester` target.

TESTER_DIR = tester
TESTER_SCRIPT = $(TESTER_DIR)/tester.sh

SRCDIR.tester = $(TESTER_DIR)/src
INCDIR.tester = $(TESTER_DIR)/include
OBJDIR.tester = $(OBJDIR)/$(TESTER_DIR)

SOURCES.tester += $(wildcard $(SRCDIR.tester)/*.c)
TARGET.tester  := $(BUILDDIR)/image-tester

CFLAGS.tester += $(strip $(file < $(TESTER_DIR)/compile_flags.txt)) $(CFLAGS) -I$(INCDIR.tester)

##### Rule templates. Should be instantiated with $(eval $(call template, ...))

# I use $$(var) in some variables to avoid variable expanding too early.
# I do not remember when $(var) is expanded in `define` rules, but $$(var)
# is expanded exactly at $(eval ...) call.

# Template for running submake on each SANITIZER value when SANITIZER=all is used. 
# $(1) - SANITIZER value (none/asan/lsan/msan/usan)
define make-sanitizer-rule

GOALS.$(1) := $$(patsubst %,%.$(1),$$(MAKECMDGOALS))

$$(MAKECMDGOALS): $$(GOALS.$(1))
.PHONY: $$(GOALS.$(1))

$$(GOALS.$(1)):
	@echo Running 'make $$(patsubst %.$(1),%,$$@)' with SANITIZER=$(1)
	@$(MAKE) $$(patsubst %.$(1),%,$$@) SANITIZER=$(1)

endef

# Template for compilation and linking rules + depfile generation and inclusion
# $(1) - target name (main/tester)
define make-compilation-rule

OBJECTS.$(1) := $$(SOURCES.$(1):$$(SRCDIR.$(1))/%.c=$$(OBJDIR.$(1))/%.o)
SRCDEPS.$(1) := $$(OBJECTS.$(1):%.o=%.o.d)

DIRS.$(1) := $$(sort $$(dir $$(OBJECTS.$(1)) $$(TARGET.$(1))))
DIRS += $$(DIRS.$(1))

.PHONY: build-$(1)

build-$(1): $$(TARGET.$(1))

$$(TARGET.$(1)): $$(OBJECTS.$(1)) | $$(DIRS.$(1))
	$(LINKER) $(LDFLAGS) $$(OBJECTS.$(1)) -o $$@

$$(OBJDIR.$(1))/%.o: $$(SRCDIR.$(1))/%.c | $$(DIRS.$(1))
	$(CC) $(CFLAGS.$(1)) -M -MP $$< >$$@.d
	$(CC) $(CFLAGS.$(1)) -c $$< -o $$@

-include $$(SRCDEPS.$(1))

endef

# Template for testing rules.
# $(1) - directory with test
define make-test-rule

TST_NAME.$(1) := $$(notdir $(1))

TST_INPUT.$(1) := $(1)/input.bmp
TST_OUTPUT.$(1) := $(OBJDIR.tester)/$$(TST_NAME.$(1)).bmp
TST_EXPECTED.$(1) := $(1)/output_expected.bmp

TST_LOG_OUT.$(1) := $(OBJDIR.tester)/$$(TST_NAME.$(1))_out.log
TST_LOG_ERR.$(1) := $(OBJDIR.tester)/$$(TST_NAME.$(1))_err.log

.PHONY: $$(TST_OUTPUT.$(1)) test-$$(TST_NAME.$(1))
test: test-$$(TST_NAME.$(1))

test-$$(TST_NAME.$(1)): build-main build-tester
	$(TESTER_SCRIPT) $$(TST_NAME.$(1)) \
		--main-cmd '$(TARGET.main) $$(TST_INPUT.$(1)) $$(TST_OUTPUT.$(1))' \
		--tester-cmd '$(TARGET.tester) $$(TST_OUTPUT.$(1)) $$(TST_EXPECTED.$(1))' \
		--log-dir '$(OBJDIR.tester)'

endef

##### Rules and targets.

.PHONY: all test clean check

ifeq ($(MAKECMDGOALS),)
MAKECMDGOALS := all
endif


ifeq ($(SANITIZER),all)
# Do all the work in sub-makes
$(foreach sanitizer,none asan lsan msan usan,$(eval $(call make-sanitizer-rule,$(sanitizer))))
else

all: build-main

check:
	$(CLANG_TIDY) $(CLANG_TIDY_ARGS) $(SOURCES.main)

$(foreach target,main tester,$(eval $(call make-compilation-rule,$(target))))
$(foreach test,$(sort $(wildcard $(TESTER_DIR)/tests/*)),$(eval $(call make-test-rule,$(test))))

clean:
	$(RM) $(OBJDIR) $(BUILDDIR)

$(sort $(DIRS)):
	$(MKDIR) $@

endif