# Compiler
COMPILER=gcc

# Build mode 0:dev, 1:prod
BUILD_MODE=1

# Version
VERSION="0.13.0"

# Compiler arguments depending on the architecture
ARCH=$(shell arch)
ifeq ($(ARCH), x86_64)
	BUILD_ARG_ARCH=-DCAPY_ARCH_X86=1 -mrdseed -fcf-protection=full
else
	BUILD_ARG_ARCH=-DCAPY_ARCH_X86=0
endif

# Compilation hardening options
HARDENED_BUILD=0
ifeq ($(HARDENED_BUILD), 1)
	BUILD_ARG_HARDENING=\
		-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 \
		-D_GLIBCXX_ASSERTIONS \
		-fstack-clash-protection -fstack-protector-strong \
		-fstrict-flex-arrays=3 \
		-Wl,-z,nodlopen -Wl,-z,noexecstack \
		-Wl,-z,relro -Wl,-z,now \
		-fPIC -shared
else
	BUILD_ARG_HARDENING=
endif

# Compiler arguments
# _XOPEN_SOURCE=700 is needed by ipc.h on my old 16bits/ubuntu16
# Set PNG_SKIP_SETJMP_CHECK to avoid
# "__pngconf.h__ in libpng already includes setjmp.h;"
ifeq ($(BUILD_MODE), 0)
	BUILD_ARG_MODE=-O0 -ggdb -g3
else ifeq ($(BUILD_MODE), 1)
	BUILD_ARG_MODE=\
		-Werror -Wfatal-errors -O3 --unroll-loops --omit-frame-pointer --inline
endif
GTK_PARAM=`pkg-config --cflags gtk+-3.0`
GTK_LINK=`pkg-config --libs gtk+-3.0`
BUILD_ARG=\
	-std=c18 -I./Src -Wall -Wextra -Wshadow -Wno-format-truncation -Wpadded \
	-Wpedantic -Wformat=2 -Wno-unused-parameter \
	-Wwrite-strings -Wstrict-prototypes -Wold-style-definition \
	-Wredundant-decls -Wnested-externs -Wmissing-include-dirs \
	-Wjump-misses-init -Wlogical-op \
	-Wconversion -Wtrampolines -Wimplicit-fallthrough \
	$(BUILD_ARG_MODE) $(BUILD_ARG_ARCH) $(BUILD_ARG_HARDENING) -fopenmp \
	-DBUILD_MODE=$(BUILD_MODE) -DVERSION=$(VERSION) -DCOMMIT=`cat commitId.txt` \
	-DPNG_SKIP_SETJMP_CHECK -D_XOPEN_SOURCE=700 $(GTK_PARAM) \
	-Wno-maybe-uninitialized -Wno-clobbered
LINK_ARG=-lm -fopenmp -lpng $(GTK_LINK) -lX11

# Variable to detect changes to the repo
REPO_CHANGES=`if [ -d .git ]; then git status --porcelain; fi`

# Commit id
COMMIT_ID=`if [ -d .git ]; then git rev-parse HEAD; fi`

# List of source files
SRCS=$(wildcard Src/*.c)

# List of header files
HEADERS=$(wildcard Src/*.h)

# List of object files
OBJS=$(patsubst Src/%.c, Obj/%.o, $(SRCS))

# List of unit tests
TEST_SRCS=$(wildcard UnitTests/test_*.c)
TESTS=$(sort $(patsubst UnitTests/test_%.c, UnitTests/test_%, $(TEST_SRCS)))

# Compile the obj files
obj: commitId_txt $(OBJS)

# Compile everything
all: obj $(TESTS)

# Include the dependencies rules
-include $(OBJS:.o=.d)

# Compile rule for obj files
Obj/%.o: Src/%.c Src/%.h Makefile
	@mkdir -p Obj
	$(COMPILER) $(BUILD_ARG) -c Src/$*.c -o $@ && cppcheck --check-level=exhaustive Src/$*.[ch]
	@echo -n "Obj/" > Obj/$*.d
	$(COMPILER) -MM Src/$*.c >> Obj/$*.d

# External resources
resources:
	mkdir -p Resources
	cd Resources; test -f cutest.c || wget https://baillehachepascal.dev/2022/Data/cutest_exception.c
	cd Resources; test -f cutest.c || mv cutest_exception.c cutest.c
	cd Resources; test -f openml_import.py || wget https://baillehachepascal.dev/2022/Data/OpenMLImport/openml_import.txt
	pip3 install pathlib argparse openml
	test -f Resources/iris.csv || python Resources/openml_import.py -o ./Resources/ -n iris -i 59 -s
	test -f Resources/abalone.csv || python Resources/openml_import.py -o ./Resources/ -n abalone -i 2075 -s
	sed -i 's/cat$$/num/' ./Resources/abalone.csv

# Run all the unit tests
unitTests: resources
	for f in ${TESTS}; do make --no-print-directory $$f; if [ $$? -ne 0 ]; then exit 1; fi; done

# Run one unit test
UnitTests/test_%: UnitTests/test_%.c obj Makefile
	@$(COMPILER) -o UnitTests/cutest Resources/cutest.c -DCUTEST_FILE=../UnitTests/test_$*.c $(OBJS) $(BUILD_ARG) $(LINK_ARG) && UnitTests/cutest -t $(test) && \rm -f UnitTests/cutest

# TDD
tdd:
	ls Src/* UnitTests/test_$(unit).c | entr make UnitTests/test_$(unit)

# Cleaning rule
clean:
	rm -rf Obj Dist $(TESTS) UnitTests/*.o *core*

# Installation rule
INSTALL_DIR=/usr/local
install: checkCommit clean obj
	@echo "Installing commit `cat commitId.txt` into $(INSTALL_DIR)"
	@sudo mkdir -p $(INSTALL_DIR)/include $(INSTALL_DIR)/lib
	@sudo rm -rf $(INSTALL_DIR)/include/LibCapy
	@sudo mkdir -p $(INSTALL_DIR)/include/LibCapy
	@sudo cp $(HEADERS) $(INSTALL_DIR)/include/LibCapy
	@sudo rm -f $(INSTALL_DIR)/lib/libcapy.a
	@sudo ar -r $(INSTALL_DIR)/lib/libcapy.a $(OBJS)

# Packaging rule
package: checkCommit cppcheck clean obj cbo all unitTests
	mkdir -p Dist
	rm -f Dist/*
	tar czvf Dist/libcapy.$(VERSION).tar.gz Src Makefile INSTALL.md LICENSE README.md VERSIONS.md commitId.txt UnitTests Pdf/libcapy.pdf

# Test rule
test:
	echo "#include <LibCapy/capy.h>" > /tmp/testLibCapy.c && echo "int main() {println(\"You are using LibCapy version %s commit id %s\",CapyGetVersion(),CapyGetCommitId());return 0;}" >> /tmp/testLibCapy.c && $(COMPILER) -o /tmp/testLibCapy /tmp/testLibCapy.c -lcapy $(BUILD_ARG) $(LINK_ARG) && /tmp/testLibCapy; rm -f /tmp/testLibCapy*

# Save the current commit id to a file to be able to use it in the code
commitId_txt:
	@if [ -z "${REPO_CHANGES}" ]; then echo ${COMMIT_ID} > commitId.txt; else echo "${COMMIT_ID}_locally_edited" > commitId.txt; fi

# Check that there are no uncommited change to the repository
checkCommit:
	@if [ -z "${REPO_CHANGES}" ]; then exit 0; else echo "Commit your changes first"; exit 1; fi

# Test the installation process
testInstall: testInstallUbuntu18 testInstallUbuntu20

testInstallUbuntu18:
	cd DockerFiles/Ubuntu18; docker image inspect capy_ubuntu18 >/dev/null 2>&1 && echo "" || docker build -t capy_ubuntu18 .
	xhost local:root && docker run --rm -e DISPLAY=${DISPLAY} -v /tmp/.X11-unix:/tmp/.X11-unix capy_ubuntu18

testInstallUbuntu20:
	cd DockerFiles/Ubuntu20; docker image inspect capy_ubuntu20 >/dev/null 2>&1 && echo "" || docker build -t capy_ubuntu20 .
	docker run --rm -e DISPLAY=${DISPLAY} -v /tmp/.X11-unix:/tmp/.X11-unix capy_ubuntu20

# Dynamic analysis, use as: make valgrind test=XXX where xx is the module to
# test
# About 'memory still reachable' due to Gtk and Glib:
# suppression files do not work
# https://github.com/GreenBeard/GNOME.supp
# https://wiki.gnome.org/Valgrind
# give up and ignore the 'possibly lost' and 'sill reachable' categories
# 'unset DEBUGINFOD_URLS' is said to avoid valgrind downloading debug info
# from the web, which makes it sluggish. However, using it return a fatal error
# due to 'strlen' not found. 
valgrind: obj
	@$(COMPILER) -o UnitTests/cutest Resources/cutest.c -DCUTEST_FILE=../UnitTests/test_$(test).c $(OBJS) $(BUILD_ARG) $(LINK_ARG) && \
	valgrind \
	-v --track-origins=yes --tool=memcheck \
	--leak-check=full \
	--show-leak-kinds=definite,indirect \
	UnitTests/cutest

# Open all files in the Geany editor
edit:
	geany Src/*.c Src/*.h Makefile *.md UnitTests/*.c &

# Open all headers and doc in the Geany editor
editHeader:
	geany Src/*.h &
	gio open Pdf/*.pdf &

# Run the preprocessor on a file, use as: make preprocessSrcFile file=XXX where
# XXX is the file to preprocess
preprocessSrcFile:
	$(COMPILER) $(BUILD_ARG) -E Src/$(file).c

# Static analysis
cppcheck:
	cppcheck --check-level=exhaustive Src/*.[ch]

# Code beautifier
cbo:
	cbo --check --files Src/*.[ch] UnitTests/*.c

# Doc generation in HTML format
docHtml:
	mkdir -p Html && cbo -f $(SRCS) $(HEADERS) -d html -o ./Html -p LibCapy -pr Html/pre.php -po Html/post.php

# Doc generation in PDF format
docPdf:
	mkdir -p Pdf && cbo -f $(SRCS) $(HEADERS) -d pdf -o ./Pdf && cd Pdf && make

# Add the boiler plate for a new class
# Execute `make addClass name=Toto` to create the class CapyToto
addClass:
	python Tools/AddClass/addClass.py $(name)
