# This Makefile has the rules necessary for making the custom version of
# CoreNEURON executable called "special-core" from the provided mod files.
# Mod files are looked up in the MODS_PATH directory.

# Current system OS
OS_NAME := $(shell uname)

# ","" is an argument separator, never as a literal for Makefile rule
COMMA_OP =,

# Default variables for various targets
MECHLIB_SUFFIX =
MODS_PATH = .
OUTPUT_DIR = arm64
DESTDIR =
TARGET_LIB_TYPE = $(BUILD_TYPE)

# required for OSX to execute nrnivmodl-core
ifeq ($(OS_NAME), Darwin)
  ifeq ($(origin SDKROOT), undefined)
    export SDKROOT := $(shell xcrun --sdk macosx --show-sdk-path)
  endif
endif

# CoreNEURON installation directories
CORENRN_BIN_DIR := $(ROOT)/bin
CORENRN_LIB_DIR := $(ROOT)/lib
CORENRN_INC_DIR := $(ROOT)/include
CORENRN_SHARE_CORENRN_DIR:= $(ROOT)/share/coreneuron
CORENRN_SHARE_MOD2CPP_DIR := $(ROOT)/share/mod2c

# name of the CoreNEURON binary
SPECIAL_EXE  = $(OUTPUT_DIR)/special-core

# Directory where cpp files are generated for each mod file
MOD_TO_CPP_DIR = $(OUTPUT_DIR)/corenrn/mod2c

# Directory where cpp files are compiled
MOD_OBJS_DIR = $(OUTPUT_DIR)/corenrn/build

# Linked libraries gathered by CMake
LDFLAGS = $(LINKFLAGS) 
CORENRNLIB_FLAGS = -L$(CORENRN_LIB_DIR) -lcoreneuron
CORENRNLIB_FLAGS += $(if , -W$(subst ;, -W,l,-rpath,),)
CORENRNLIB_FLAGS += $(if , -W$(subst ;, -W,l,-rpath,),)
CORENRNLIB_FLAGS += $(if , -W$(subst ;, -W,l,-rpath,),)
CORENRNLIB_FLAGS += $(if ,-L,)

# Includes paths gathered by CMake
# coreneuron/utils/randoms goes first because it needs to override the NEURON
# directory in INCFLAGS
INCLUDES = -I$(CORENRN_INC_DIR)/coreneuron/utils/randoms $(INCFLAGS) -I$(CORENRN_INC_DIR)
ifeq (ON, OFF)
  INCLUDES += $(if /opt/homebrew/Cellar/open-mpi/4.1.5/include, -I$(subst ;, -I,/opt/homebrew/Cellar/open-mpi/4.1.5/include),)
endif
INCLUDES += $(if , -I$(subst ;, -I,),)

# CXX is always defined. If the definition comes from default change it
ifeq ($(origin CXX), default)
    CXX = /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++
endif

ifeq (OFF, ON)
  ifneq ($(shell $(CXX) --version | grep -o nvc++), nvc++)
    $(error GPU wheels are only compatible with the NVIDIA C++ compiler nvc++, but CXX=$(CXX) and --version gives $(shell $(CXX) --version))
  endif
  # nvc++ -dumpversion is simpler, but only available from 22.2
  ifeq ($(findstring nvc++ , $(shell $(CXX) --version)),)
    $(error GPU wheels are currently not compatible across NVIDIA HPC SDK versions. You have $(shell $(CXX) -V | grep nvc++) but this wheel was built with .)
  endif
endif

# In case of wheel, python and perl exe paths are from the build machine.
# First prefer env variables set by neuron's nrnivmodl wrapper then check
# binary used during build. If they don't exist then simply use python and
# perl as the name of binaries.
CORENRN_PYTHONEXE ?= /Users/hines/neuron/8.2/nrn_build_venv312_15347763/bin/python
CORENRN_PERLEXE ?= /usr/bin/perl
ifeq ($(wildcard $(CORENRN_PYTHONEXE)),)
  CORENRN_PYTHONEXE=python
endif
ifeq ($(wildcard $(CORENRN_PERLEXE)),)
  CORENRN_PERLEXE=perl
endif

CXXFLAGS = -DVERSION_INFO='8.2.7' -O2 -Wno-writable-strings -std=c++14
CXX_COMPILE_CMD = $(CXX) $(CXXFLAGS) -fPIC  -DMPI_NO_CPPBIND=1 -DOMPI_SKIP_MPICXX=1 -DMPICH_SKIP_MPICXX=1 -DHAVE_CONFIG_H -DCORENEURON_BUILD -DNRNMPI=1 -DMPI_NO_CPPBIND=1 -DOMPI_SKIP_MPICXX=1 -DMPICH_SKIP_MPICXX=1 -DLAYOUT=0 -DDISABLE_HOC_EXP -DENABLE_SPLAYTREE_QUEUING -DCORENRN_ENABLE_MPI_DYNAMIC $(INCLUDES)
CXX_LINK_EXE_CMD = $(CXX) $(CXXFLAGS) 
CXX_SHARED_LIB_CMD = $(CXX) $(CXXFLAGS) -dynamiclib -Wl,-headerpad_max_install_names -undefined dynamic_lookup -fPIC 

# ISPC compilation and link commands
ISPC = 
ISPC_COMPILE_CMD = $(ISPC)  --pic -I$(CORENRN_INC_DIR)

# env variables required for mod2c or nmodl
MOD2CPP_ENV_VAR =  PYTHONPATH=:${CORENRN_LIB_DIR}/python MODLUNIT=$(CORENRN_SHARE_MOD2CPP_DIR)/nrnunits.lib

# nmodl options
ifeq (OFF, ON)
    ifeq (OFF, ON)
        nmodl_arguments_c=host --c acc --oacc passes --inline
    else
        nmodl_arguments_c=host --c passes --inline
    endif
    nmodl_arguments_ispc=host --ispc passes --inline
endif

# name of the mechanism library with suffix if provided
COREMECH_LIB_NAME = corenrnmech$(if $(MECHLIB_SUFFIX),_$(MECHLIB_SUFFIX),)
COREMECH_LIB_PATH = $(OUTPUT_DIR)/lib$(COREMECH_LIB_NAME)$(LIB_SUFFIX)

# Various header and C++/Object file
MOD_FUNC_CPP = $(MOD_TO_CPP_DIR)/_mod_func.cpp
MOD_FUNC_OBJ = $(MOD_OBJS_DIR)/_mod_func.o
ENGINEMECH_OBJ = $(MOD_OBJS_DIR)/enginemech.o

# Depending on static/shared build, determine library name and it's suffix
ifeq ($(TARGET_LIB_TYPE), STATIC)
    LIB_SUFFIX = .a
    corenrnmech_lib_target = coremech_lib_static
else
    LIB_SUFFIX = .dylib
    corenrnmech_lib_target = coremech_lib_shared
endif

# Binary of MOD2C/NMODL depending on CMake option activated
ifeq (, TRUE)
    MOD2CPP_BINARY_PATH = $(if $(MOD2CPP_BINARY),$(MOD2CPP_BINARY), /Users/hines/neuron/8.2/build/temp.macosx-10.9-arm64-cpython-312/bin/mod2c_core)
    INCLUDES += -I/Users/hines/neuron/8.2/build/temp.macosx-10.9-arm64-cpython-312/include
    ISPC_COMPILE_CMD += -I/Users/hines/neuron/8.2/build/temp.macosx-10.9-arm64-cpython-312/include
else
    MOD2CPP_BINARY_PATH = $(if $(MOD2CPP_BINARY),$(MOD2CPP_BINARY), $(CORENRN_BIN_DIR)/mod2c_core)
endif

# MOD files with full path, without path and names without .mod extension
mod_files_paths = $(sort $(wildcard $(MODS_PATH)/*.mod))
mod_files_names = $(sort $(notdir $(wildcard $(MODS_PATH)/*.mod)))
mod_files_no_ext = $(mod_files_names:.mod=)

# Find out artificial and non-artificial cells for ISPC backend
define check_file
	# if ISPC is active then only separate mod files
	ifeq (OFF, ON)
		ifeq ($(shell grep -q $(2) $(1); echo $$?), 0)
			mod_art_files += $(MOD_TO_CPP_DIR)/$(notdir $(1))
		else
			mod_non_art_files += $(MOD_TO_CPP_DIR)/$(notdir $(1))
		endif
	else
		mod_all_files += $(MOD_TO_CPP_DIR)/$(notdir $(1))
	endif
endef

# Iterate over all available mod files and make group
$(foreach mod_file, $(mod_files_paths), $(eval $(call check_file, $(mod_file), ARTIFICIAL_CELL)))

# With ispc, artificial cells get translated as regular CPP backend
# Otherwise, all mod files are compiled as CPP backend
ifeq (OFF, ON)
	mod_files_for_cpp_backend = $(mod_art_files)
else
	mod_files_for_cpp_backend = $(mod_all_files)
endif

# CPP files and their obkects
mod_cpp_files = $(patsubst %.mod,%.cpp,$(mod_files_for_cpp_backend))
mod_cpp_objs = $(addprefix $(MOD_OBJS_DIR)/,$(addsuffix .o,$(basename $(mod_files_no_ext))))

# For ISPC backend, we use all non-artificial cells
mod_ispc_srcs_names = $(notdir $(mod_non_art_files))
mod_ispc_files = $(patsubst %.mod,%.ispc,$(mod_non_art_files))
mod_ispc_cpp_files = $(patsubst %.mod,%.cpp,$(mod_non_art_files))
mod_ispc_objs = $(addprefix $(MOD_OBJS_DIR)/,$(addsuffix .obj,$(basename $(mod_ispc_srcs_names))))

# We use $ORIGIN (@loader_path in OSX)
ORIGIN_RPATH := $(if $(filter Darwin,$(OS_NAME)),@loader_path,$$ORIGIN)
SONAME_OPTION := -Wl,$(if $(filter Darwin,$(OS_NAME)),-install_name${COMMA_OP}@rpath/,-soname${COMMA_OP})$(notdir ${COREMECH_LIB_PATH})
LIB_RPATH = $(if $(DESTDIR),$(DESTDIR)/lib,$(ORIGIN_RPATH))

# When special-core is installed, it needs to find library in the
# lib folder of install prefix. We use relative path in order it
# to be portable when files are moved (e.g. python wheel)
INSTALL_LIB_RPATH = $(ORIGIN_RPATH)/../lib

# All objects used during build
ALL_OBJS = $(MOD_FUNC_OBJ) $(mod_cpp_objs) $(mod_ispc_objs)

# Colors for pretty printing
C_RESET := \033[0m
C_GREEN := \033[32m

# Default nmodl flags. Override if MOD2CPP_RUNTIME_FLAGS is not empty
ifeq (OFF, ON)
    MOD2CPP_FLAGS_ISPC = $(if $(MOD2CPP_RUNTIME_FLAGS),$(MOD2CPP_RUNTIME_FLAGS),$(nmodl_arguments_ispc))
    MOD2CPP_FLAGS_C = $(if $(MOD2CPP_RUNTIME_FLAGS),$(MOD2CPP_RUNTIME_FLAGS),$(nmodl_arguments_c))
endif

ifeq (OFF, ON)
    $(info Default NMODL flags: )
else
    $(info Default NMODL flags: )
endif

ifneq ($(MOD2CPP_RUNTIME_FLAGS),)
    $(warning Runtime nmodl flags (they replace the default ones): $(MOD2CPP_RUNTIME_FLAGS))
endif

# ======== MAIN BUILD RULES ============


# main target to build binary
$(SPECIAL_EXE): coremech_lib_target
	@printf " => $(C_GREEN)Binary$(C_RESET) creating $(SPECIAL_EXE)\n"
	$(CXX_LINK_EXE_CMD) -o $(SPECIAL_EXE) $(CORENRN_SHARE_CORENRN_DIR)/coreneuron.cpp \
	  -I$(CORENRN_INC_DIR) $(INCFLAGS) \
	  -L$(OUTPUT_DIR) -l$(COREMECH_LIB_NAME) $(CORENRNLIB_FLAGS) $(LDFLAGS) \
	  -Wl,-rpath,'$(LIB_RPATH)' -Wl,-rpath,$(CORENRN_LIB_DIR) -Wl,-rpath,'$(INSTALL_LIB_RPATH)'

coremech_lib_target: $(corenrnmech_lib_target)
	rm -rf $(OUTPUT_DIR)/.libs/lib$(COREMECH_LIB_NAME)$(LIB_SUFFIX); \
	mkdir -p $(OUTPUT_DIR)/.libs; \
	ln -s ../lib$(COREMECH_LIB_NAME)$(LIB_SUFFIX) $(OUTPUT_DIR)/.libs/lib$(COREMECH_LIB_NAME)$(LIB_SUFFIX)

$(ENGINEMECH_OBJ): $(CORENRN_SHARE_CORENRN_DIR)/enginemech.cpp | $(MOD_OBJS_DIR)
	$(CXX_COMPILE_CMD) -c -DADDITIONAL_MECHS $(CORENRN_SHARE_CORENRN_DIR)/enginemech.cpp -o $(ENGINEMECH_OBJ)

# build shared library of mechanisms
coremech_lib_shared: $(ALL_OBJS) $(ENGINEMECH_OBJ) build_always
	$(CXX_SHARED_LIB_CMD) $(ENGINEMECH_OBJ) -o ${COREMECH_LIB_PATH} $(ALL_OBJS) \
	  -I$(CORENRN_INC_DIR) $(INCFLAGS) \
	  $(LDFLAGS) $(CORENRN_LIB_DIR)/libscopmath.a \
	  ${SONAME_OPTION} $(CORENRNLIB_FLAGS) -Wl,-rpath,$(CORENRN_LIB_DIR);

# build static library of mechanisms
coremech_lib_static: $(ALL_OBJS) $(ENGINEMECH_OBJ) build_always
	mkdir -p $(MOD_OBJS_DIR)/scopmath; \
	cd $(MOD_OBJS_DIR)/scopmath && ar -x $(CORENRN_LIB_DIR)/libscopmath.a && cd -;\
	rm -f ${COREMECH_LIB_PATH}; \
	ar cq ${COREMECH_LIB_PATH} $(ENGINEMECH_OBJ) $(ALL_OBJS) $(MOD_OBJS_DIR)/scopmath/*.o;

# compile cpp files to .o
$(MOD_OBJS_DIR)/%.o: $(MOD_TO_CPP_DIR)/%.cpp | $(MOD_OBJS_DIR)
	$(CXX_COMPILE_CMD) -c $< -o $@ -DNRN_PRCELLSTATE=$(NRN_PRCELLSTATE) 

# compile ispc files to .obj
$(MOD_OBJS_DIR)/%.obj: $(MOD_TO_CPP_DIR)/%.ispc | $(MOD_OBJS_DIR)
	$(ISPC_COMPILE_CMD) $< -o $@

# translate MOD files to ISPC using NMODL
$(mod_ispc_files): $(MOD_TO_CPP_DIR)/%.ispc: $(MODS_PATH)/%.mod | $(MOD_TO_CPP_DIR)
	$(MOD2CPP_ENV_VAR) $(MOD2CPP_BINARY_PATH) $< -o $(MOD_TO_CPP_DIR)/ $(MOD2CPP_FLAGS_ISPC)

# translate MOD files to CPP using mod2c/NMODL
$(mod_cpp_files): $(MOD_TO_CPP_DIR)/%.cpp: $(MODS_PATH)/%.mod | $(MOD_TO_CPP_DIR)
	$(MOD2CPP_ENV_VAR) $(MOD2CPP_BINARY_PATH) $< -o $(MOD_TO_CPP_DIR)/ $(MOD2CPP_FLAGS_C)

# static pattern to set up the dependencies for the previous recipe
$(mod_ispc_cpp_files): $(MOD_TO_CPP_DIR)/%.cpp: $(MOD_TO_CPP_DIR)/%.ispc

# generate mod registration function. Dont overwrite if it's not changed
$(MOD_FUNC_CPP): build_always | $(MOD_TO_CPP_DIR)
	$(CORENRN_PERLEXE) $(CORENRN_SHARE_CORENRN_DIR)/mod_func.c.pl $(mod_files_names) > $(MOD_FUNC_CPP).tmp
	diff -q $(MOD_FUNC_CPP).tmp $(MOD_FUNC_CPP) || \
	mv $(MOD_FUNC_CPP).tmp $(MOD_FUNC_CPP)

# symlink to cpp files provided by coreneuron
$(MOD_TO_CPP_DIR)/%.cpp: $(CORENRN_SHARE_MOD2CPP_DIR)/%.cpp | $(MOD_TO_CPP_DIR)
	ln -s $< $@

# create directories needed
$(MOD_TO_CPP_DIR):
	mkdir -p $(MOD_TO_CPP_DIR)

$(MOD_OBJS_DIR):
	mkdir -p $(MOD_OBJS_DIR)

# install binary and libraries
install: $(SPECIAL_EXE) coremech_lib_target
	install -d $(DESTDIR)/bin $(DESTDIR)/lib
	install ${COREMECH_LIB_PATH} $(DESTDIR)/lib
	install $(SPECIAL_EXE) $(DESTDIR)/bin

.PHONY: build_always

$(VERBOSE).SILENT:

# delete cpp files if mod2c error, otherwise they are not generated again
.DELETE_ON_ERROR:
