cmake_minimum_required(VERSION 3.15)

project(SlugHorn LANGUAGES C CXX VERSION 0.0.2)

# =============================================================================
# Compile options
# =============================================================================

if(WIN32)
	set(SLUGHORN_COMPILE_OPTIONS
		/W4
		/WX
		/wd4100
		/analyze
		/analyze:external-
		# The MSVC STL/SDK headers are reached via the INCLUDE env var, not -I,
		# so they aren't automatically treated as external; without this,
		# /analyze produces false positives (e.g. C6031) deep in <xutility>
		# et al. when heavily-templated code (pybind11) instantiates them.
		/external:env:INCLUDE
	)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
	set(SLUGHORN_COMPILE_OPTIONS
		-W
		-Wall
		-Wshadow
		-Wconversion
		-Wsign-conversion
		-Wfloat-conversion
		-Wpedantic
		-Wextra
		-Werror
		-Wno-unused-parameter
	)
else()
	set(SLUGHORN_COMPILE_OPTIONS)
endif()

# =============================================================================
# Core library target
# =============================================================================

option(SLUGHORN_SERIAL "Include serialization support (requires nlohmann)" OFF)
option(SLUGHORN_NANOSVG "Include NanoSVG support (requires nanosvg)" OFF)
option(SLUGHORN_FREETYPE "Include FreeType support (requires freetype2)" OFF)
option(SLUGHORN_MSDF "Include MSDF support (requires msdfgen)" OFF)
option(SLUGHORN_TESSELLATE "Include polygon triangulation/extrusion support (requires earcut.hpp)" OFF)
option(SLUGHORN_BLEND2D "Include Blend2D backend (requires blend2d)" OFF)
option(SLUGHORN_RENDER_PARALLEL "Enable std::execution parallel pixel rendering (requires TBB on Linux)" OFF)
# option(SLUGHORN_GLFW "Include GLFW support (requires glfw3)" OFF)

add_library(slughorn STATIC "slughorn/slughorn.cpp")

target_include_directories(slughorn
	PUBLIC
		# When used as a submodule, consumers get our headers via this path
		$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
		$<INSTALL_INTERFACE:include>
)

target_compile_features(slughorn PUBLIC cxx_std_20)
target_compile_options(slughorn PRIVATE ${SLUGHORN_COMPILE_OPTIONS})
set_target_properties(slughorn PROPERTIES POSITION_INDEPENDENT_CODE ON)

if(SLUGHORN_SERIAL)
	if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/ext/json")
		message(FATAL_ERROR
			"ext/json not found. Run:\n"
			"  git submodule update --init --recursive"
		)
	endif()

	add_subdirectory("ext/json")

	target_compile_definitions(slughorn PRIVATE SLUGHORN_SERIAL_IMPLEMENTATION)
	target_compile_definitions(slughorn PUBLIC SLUGHORN_HAS_SERIAL)
	target_link_libraries(slughorn PRIVATE nlohmann_json::nlohmann_json)
endif()

if(SLUGHORN_NANOSVG)
	if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/ext/nanosvg")
		message(FATAL_ERROR
			"ext/nanosvg not found. Run:\n"
			"  git submodule update --init --recursive"
		)
	endif()

	find_package(Git REQUIRED)

	# NanoSVG upstream is missing a few fixes we depend on (NSVGgradient::units,
	# nsvg__pushAttr id inheritance, OBB linear gradient coordinate handling).
	# Apply them here so a fresh submodule checkout builds without manual steps.
	# --reverse --check detects an already-patched tree so reconfiguring never
	# double-applies or errors out.
	set(SLUGHORN_NANOSVG_PATCHES
		"${CMAKE_CURRENT_SOURCE_DIR}/ext/nanosvg-00-gradient-units-field.diff"
		"${CMAKE_CURRENT_SOURCE_DIR}/ext/nanosvg-01-push-attr-id-inherit.diff"
		"${CMAKE_CURRENT_SOURCE_DIR}/ext/nanosvg-02-obb-linear-gradient-coords.diff"
	)

	# --ignore-whitespace guards against CRLF-checked-out submodules (e.g. Windows
	# with core.autocrlf=true) not matching these LF patch files context-for-context.
	foreach(_slughorn_nanosvg_patch ${SLUGHORN_NANOSVG_PATCHES})
		execute_process(
			COMMAND "${GIT_EXECUTABLE}" apply --reverse --check --ignore-whitespace "${_slughorn_nanosvg_patch}"
			WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/ext/nanosvg"
			RESULT_VARIABLE _slughorn_nanosvg_patch_applied
			OUTPUT_QUIET
			ERROR_QUIET
		)

		if(NOT _slughorn_nanosvg_patch_applied EQUAL 0)
			message(STATUS "Applying nanosvg patch: ${_slughorn_nanosvg_patch}")

			execute_process(
				COMMAND "${GIT_EXECUTABLE}" apply --ignore-whitespace "${_slughorn_nanosvg_patch}"
				WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/ext/nanosvg"
				RESULT_VARIABLE _slughorn_nanosvg_patch_result
			)

			if(NOT _slughorn_nanosvg_patch_result EQUAL 0)
				message(FATAL_ERROR "Failed to apply nanosvg patch: ${_slughorn_nanosvg_patch}")
			endif()
		endif()
	endforeach()

	target_compile_definitions(slughorn PRIVATE SLUGHORN_NANOSVG_IMPLEMENTATION)
	target_compile_definitions(slughorn PUBLIC SLUGHORN_HAS_NANOSVG)
	target_include_directories(slughorn
		SYSTEM PUBLIC
			$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/ext/nanosvg/src>
	)
endif()

if(SLUGHORN_FREETYPE)
	# if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/ext/nanosvg")
	# 	message(FATAL_ERROR
	# 		"ext/nanosvg not found. Run:\n"
	# 		"  git submodule update --init --recursive"
	# 	)
	# endif()

	find_package(Freetype REQUIRED)

	target_compile_definitions(slughorn PRIVATE SLUGHORN_FREETYPE_IMPLEMENTATION)
	target_compile_definitions(slughorn PUBLIC SLUGHORN_HAS_FREETYPE)
	# target_include_directories(slughorn
	# 	PUBLIC
	# 		$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
	# 		$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/ext/nanosvg/src>
	# 		$<INSTALL_INTERFACE:include>
	# )
	target_link_libraries(slughorn PUBLIC Freetype::Freetype)
endif()

# https://github.com/Chlumsky/msdfgen.git
if(SLUGHORN_MSDF)
	if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/ext/msdfgen")
		message(FATAL_ERROR
			"ext/msdfgen not found. Run:\n"
			"  git submodule update --init --recursive"
		)
	endif()

	set(MSDFGEN_CORE_ONLY ON CACHE BOOL "" FORCE)
	set(MSDFGEN_BUILD_STANDALONE OFF CACHE BOOL "" FORCE)
	set(MSDFGEN_USE_VCPKG OFF CACHE BOOL "" FORCE)
	set(MSDFGEN_USE_OPENMP ${SLUGHORN_RENDER_PARALLEL} CACHE BOOL "" FORCE)
	set(MSDFGEN_USE_SKIA OFF CACHE BOOL "" FORCE)
	set(MSDFGEN_DISABLE_SVG OFF CACHE BOOL "" FORCE)
	set(MSDFGEN_DISABLE_PNG OFF CACHE BOOL "" FORCE)
	# The rest of the project (and the vcpkg triplet's VCPKG_CRT_LINKAGE) links
	# the dynamic CRT; msdfgen's own default of static breaks the MSVC link
	# with a RuntimeLibrary mismatch (MDd vs MTd). This property is a no-op
	# outside MSVC, so only override it there.
	if(MSVC)
		set(MSDFGEN_DYNAMIC_RUNTIME ON CACHE BOOL "" FORCE)
	else()
		set(MSDFGEN_DYNAMIC_RUNTIME OFF CACHE BOOL "" FORCE)
	endif()
	set(MSDFGEN_INSTALL OFF CACHE BOOL "" FORCE)

	add_subdirectory("ext/msdfgen")
	set_target_properties(msdfgen-core PROPERTIES POSITION_INDEPENDENT_CODE ON)

	# target_compile_definitions(slughorn PRIVATE SLUGHORN_MSDF_IMPLEMENTATION)
	target_compile_definitions(slughorn PUBLIC SLUGHORN_HAS_MSDF)
	target_include_directories(slughorn
		SYSTEM PUBLIC
			$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/ext/msdfgen>
	)
	target_link_libraries(slughorn PUBLIC msdfgen-core)
endif()

if(SLUGHORN_TESSELLATE)
	if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/ext/earcut")
		message(FATAL_ERROR
			"ext/earcut not found. Run:\n"
			"  git submodule update --init --recursive"
		)
	endif()

	target_compile_definitions(slughorn PUBLIC SLUGHORN_HAS_TESSELLATE)
	target_include_directories(slughorn
		SYSTEM PUBLIC
			$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/ext/earcut/include>
	)
endif()

if(SLUGHORN_BLEND2D)
	find_package(blend2d REQUIRED)
	target_compile_definitions(slughorn PUBLIC SLUGHORN_HAS_BLEND2D)
	target_link_libraries(slughorn PUBLIC blend2d::blend2d)
endif()

if(SLUGHORN_RENDER_PARALLEL)
	find_package(OpenMP QUIET)

	if(OpenMP_CXX_FOUND)
		target_compile_definitions(slughorn PUBLIC SLUGHORN_HAS_PARALLEL)
		target_link_libraries(slughorn PUBLIC OpenMP::OpenMP_CXX)
		message(STATUS "SLUGHORN_RENDER_PARALLEL: using OpenMP")
	else()
		message(WARNING "SLUGHORN_RENDER_PARALLEL=ON but OpenMP not found; parallel rendering disabled")
	endif()
endif()

# =============================================================================
# Python bindings (optional - only when building slughorn standalone)
# =============================================================================

# When slughorn is included as a submodule, the parent project controls
# whether Python bindings are built. We only auto-configure them when
# slughorn is the top-level project.

if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
	# An extension module needs Python headers but must not link libpython. manylinux
	# deliberately omits the embeddable Development.Embed library, so request the
	# module-development component rather than the aggregate "Development" one (same fix as
	# OpenSceneGraph.py's CMakeLists.txt:10-11).
	find_package(Python3 COMPONENTS Interpreter Development.Module REQUIRED)

	if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/ext/pybind11/CMakeLists.txt")
		message(FATAL_ERROR
			"pybind11 submodule is missing. Run:\n"
			"  git submodule update --init --recursive"
		)
	endif()

	add_subdirectory(ext/pybind11)

	# The bindings are split across ext/python/slughorn-*.cpp -- one translation unit per
	# slughorn:: submodule (see ext/python/slughorn-python.hpp for the bind_*() declarations) --
	# instead of one ~2900-line monolithic ext/slughorn-python.cpp. slughorn_python_bindings is a
	# plain STATIC lib, not itself a Python module: touching one submodule's bindings only
	# recompiles that one file plus a relink, a from-scratch build parallelizes across `make -j#`,
	# and pybind11_add_module below only ever has to compile the thin ext/python/slughorn.cpp
	# PYBIND11_MODULE entry point.
	add_library(slughorn_python_bindings STATIC
		"ext/python/slughorn-core.cpp"
		"ext/python/slughorn-render.cpp"
		"ext/python/slughorn-canvas.cpp"
		"ext/python/slughorn-emoji.cpp"
	)

	# PUBLIC, not PRIVATE: ext/python/slughorn.cpp (compiled into the separate slughorn_python
	# target below, which links this one) also includes slughorn-python.hpp and therefore needs
	# every one of these same requirements -- PRIVATE here would resolve fine for this target's
	# own sources but leave slughorn.cpp's own #ifdef SLUGHORN_HAS_FREETYPE/etc. guards (and
	# #include "slughorn-python.hpp") unable to see the same defines/link libraries.
	target_link_libraries(slughorn_python_bindings PUBLIC
		slughorn
		pybind11::module
	)
	target_compile_features(slughorn_python_bindings PUBLIC cxx_std_20)
	target_compile_options(slughorn_python_bindings PRIVATE ${SLUGHORN_COMPILE_OPTIONS})
	# pybind11's per-function registration code pushes these heavily-templated
	# binding TUs past MSVC's default object-file section limit (C1128).
	target_compile_options(slughorn_python_bindings PRIVATE $<$<CXX_COMPILER_ID:MSVC>:/bigobj>)
	set_target_properties(slughorn_python_bindings PROPERTIES POSITION_INDEPENDENT_CODE ON)

	if(SLUGHORN_SERIAL)
		target_link_libraries(slughorn_python_bindings PRIVATE nlohmann_json::nlohmann_json)
	endif()

	if(SLUGHORN_FREETYPE)
		target_sources(slughorn_python_bindings PRIVATE "ext/python/slughorn-freetype.cpp")
	endif()

	if(SLUGHORN_NANOSVG)
		target_sources(slughorn_python_bindings PRIVATE "ext/python/slughorn-nanosvg.cpp")
	endif()

	if(SLUGHORN_TESSELLATE)
		target_sources(slughorn_python_bindings PRIVATE "ext/python/slughorn-tessellate.cpp")
	endif()

	pybind11_add_module(slughorn_python "ext/python/slughorn.cpp")

	set_target_properties(slughorn_python PROPERTIES
		OUTPUT_NAME "slughorn"
		# On Windows, a MODULE target's companion import library defaults to the same
		# OUTPUT_NAME - colliding with the unrelated `slughorn` STATIC library target's own
		# slughorn.lib in the same output directory under the Visual Studio generator. Worse,
		# slughorn_python links slughorn (via slughorn_python_bindings) PUBLIC, so slughorn.lib
		# is a link INPUT to this exact step, and the linker refuses to overwrite a file it's
		# simultaneously reading (LNK1114, "cannot overwrite the original file", error code 5).
		# Give the import lib its own name; OUTPUT_NAME above still governs the actual .pyd
		# Python imports.
		ARCHIVE_OUTPUT_NAME "slughorn_python"
		POSITION_INDEPENDENT_CODE ON
	)
	target_link_libraries(slughorn_python PRIVATE slughorn_python_bindings)
	target_compile_features(slughorn_python PUBLIC cxx_std_20)
	target_compile_options(slughorn_python PRIVATE ${SLUGHORN_COMPILE_OPTIONS})

	# No separate Python package directory -- the compiled module IS the top-level `slughorn`
	# import. DESTINATION "." puts it at the wheel root (scikit-build-core installs
	# CMAKE_INSTALL_PREFIX as the wheel's site-packages root), so `import slughorn` resolves
	# straight to this .so/.pyd with no wrapper package.
	install(TARGETS slughorn_python DESTINATION .)

	add_custom_target(pytest
		COMMAND ${CMAKE_COMMAND} -E env PYTHONPATH=${CMAKE_BINARY_DIR}
			${Python3_EXECUTABLE} -m pytest
		WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
		DEPENDS slughorn_python
	)
endif()

# =============================================================================
# Tests (optional, enable with -DSLUGHORN_BUILD_TESTS=ON)
# =============================================================================
option(SLUGHORN_BUILD_TESTS "Compile testing files" OFF)

if(SLUGHORN_BUILD_TESTS)
	enable_testing()

	find_package(PkgConfig REQUIRED)

	# Cairo (optional)
	pkg_check_modules(CAIRO IMPORTED_TARGET cairo)

	if(CAIRO_FOUND)
		add_executable(slughorn-test-cairo "test/slughorn-test-cairo.cpp")
		target_link_libraries(slughorn-test-cairo PRIVATE slughorn PkgConfig::CAIRO)
		add_test(NAME cairo COMMAND slughorn-test-cairo)
		message(STATUS "slughorn tests: Cairo found, building slughorn-test-cairo")
	else()
		message(STATUS "slughorn tests: Cairo not found, skipping slughorn-test-cairo")
	endif()

	# Blend2D (optional - requires SLUGHORN_BLEND2D=ON)
	if(SLUGHORN_BLEND2D)
		add_executable(slughorn-test-blend2d "test/slughorn-test-blend2d.cpp")
		target_link_libraries(slughorn-test-blend2d PRIVATE slughorn)
		add_test(NAME blend2d COMMAND slughorn-test-blend2d)
		message(STATUS "slughorn tests: Blend2D enabled, building slughorn-test-blend2d")
	else()
		message(STATUS "slughorn tests: SLUGHORN_BLEND2D=OFF, skipping slughorn-test-blend2d")
	endif()

	# NanoSVG (optional - requires SLUGHORN_NANOSVG=ON)
	if(SLUGHORN_NANOSVG)
		add_executable(slughorn-test-nanosvg "test/slughorn-test-nanosvg.cpp")

		target_include_directories(slughorn-test-nanosvg PRIVATE
			"${CMAKE_CURRENT_SOURCE_DIR}/ext/nanosvg/src"
		)

		target_link_libraries(slughorn-test-nanosvg PRIVATE slughorn)
		add_test(NAME nanosvg COMMAND slughorn-test-nanosvg)
		message(STATUS "slughorn tests: NanoSVG enabled, building slughorn-test-nanosvg")
	else()
		message(STATUS "slughorn tests: SLUGHORN_NANOSVG=OFF, skipping slughorn-test-nanosvg")
	endif()

	# Tessellate (optional - requires SLUGHORN_TESSELLATE=ON)
	if(SLUGHORN_TESSELLATE)
		add_executable(slughorn-test-tessellate "test/slughorn-test-tessellate.cpp")
		target_link_libraries(slughorn-test-tessellate PRIVATE slughorn)
		add_test(NAME tessellate COMMAND slughorn-test-tessellate)
		message(STATUS "slughorn tests: Tessellate enabled, building slughorn-test-tessellate")
	else()
		message(STATUS "slughorn tests: SLUGHORN_TESSELLATE=OFF, skipping slughorn-test-tessellate")
	endif()

	if(SLUGHORN_SERIAL)
		add_executable(slughorn-test-canvas "test/slughorn-test-canvas.cpp")
		target_link_libraries(slughorn-test-canvas PRIVATE slughorn)
		add_test(NAME canvas COMMAND slughorn-test-canvas)
		message(STATUS "slughorn tests: serial found, building slughorn-test-canvas")

		# Convenience targets: regenerate canvas.slug when the binary changes,
		# then inspect it via the CLI without needing to type paths manually.
		#
		# make canvas-list - list all shape + composite keys
		# make canvas-info - atlas summary (dimensions, packing stats)
		set(_CANVAS_SLUG "${CMAKE_CURRENT_BINARY_DIR}/canvas.slug")

		add_custom_command(
			OUTPUT ${_CANVAS_SLUG}
			COMMAND $<TARGET_FILE:slughorn-test-canvas> ${_CANVAS_SLUG}
			DEPENDS slughorn-test-canvas
			COMMENT "Generating canvas.slug"
		)

		add_custom_target(canvas-list
			COMMAND ${CMAKE_SOURCE_DIR}/bin/slughorn list ${_CANVAS_SLUG}
			DEPENDS ${_CANVAS_SLUG}
			USES_TERMINAL
		)

		add_custom_target(canvas-info
			COMMAND ${CMAKE_SOURCE_DIR}/bin/slughorn info ${_CANVAS_SLUG}
			DEPENDS ${_CANVAS_SLUG}
			USES_TERMINAL
		)
		add_executable(slughorn-test-render "test/slughorn-test-render.cpp")
		target_link_libraries(slughorn-test-render PRIVATE slughorn)
		add_test(NAME render COMMAND slughorn-test-render)
		message(STATUS "slughorn tests: building slughorn-test-render")
	else()
		message(STATUS "slughorn tests: SLUGHORN_SERIAL=OFF, skipping slughorn-test-canvas and slughorn-test-render")
	endif()

	# if(SLUGHORN_GLFW)
	# 	find_package(glfw3 REQUIRED)
	# 	add_executable(slughorn-test-glfw "test/slughorn-test-glfw.cpp")
	# 	target_link_libraries(slughorn-test-glfw PRIVATE slughorn glfw glad)
	# 	message(STATUS "slughorn tests: GLFW found, building slughorn-test-glfw")
	# else()
	# 	message(STATUS "slughorn tests: SLUGHORN_GLFW=OFF, skipping slughorn-test-glfw")
	# endif()

	# add_executable(slughorn-test-mvt "test/slughorn-test-mvt.cpp")
	# target_link_libraries(slughorn-test-mvt PRIVATE slughorn)
	# target_compile_options(slughorn-test-mvt PRIVATE ${SLUGHORN_COMPILE_OPTIONS})
	# target_include_directories(slughorn-test-mvt PRIVATE
	# 	"${CMAKE_CURRENT_SOURCE_DIR}/../protozero/include"
	# 	"${CMAKE_CURRENT_SOURCE_DIR}/../vtzero/include"
	# )

	# GDAL (optional)
	# pkg_check_modules(GDAL IMPORTED_TARGET gdal)
	#
	# if(GDAL_FOUND AND SLUGHORN_SERIAL)
	# 	add_executable(slughorn-test-gdal "test/slughorn-test-gdal.cpp")
	# 	target_link_libraries(slughorn-test-gdal PRIVATE slughorn PkgConfig::GDAL)
	# 	target_compile_options(slughorn-test-gdal PRIVATE ${SLUGHORN_COMPILE_OPTIONS})
	# 	message(STATUS "slughorn tests: GDAL found, building slughorn-test-gdal")
	# elseif(NOT GDAL_FOUND)
	# 	message(STATUS "slughorn tests: GDAL not found, skipping slughorn-test-gdal")
	# else()
	# 	message(STATUS "slughorn tests: SLUGHORN_SERIAL=OFF, skipping slughorn-test-gdal")
	# endif()

	# find_package(SQLite3 REQUIRED)
	# find_package(ZLIB REQUIRED)

	# add_executable(slughorn-test-mapbox "test/slughorn-test-mapbox.cpp")
	# target_link_libraries(slughorn-test-mapbox PRIVATE slughorn SQLite::SQLite3 ZLIB::ZLIB)
	# target_compile_options(slughorn-test-mapbox PRIVATE ${SLUGHORN_COMPILE_OPTIONS})
	# target_include_directories(slughorn-test-mapbox PRIVATE
	# 	"${CMAKE_CURRENT_SOURCE_DIR}/../protozero/include"
	# 	"${CMAKE_CURRENT_SOURCE_DIR}/../vtzero/include"
	# )

	if(SLUGHORN_SERIAL)
		add_executable(slughorn-test-bands "test/slughorn-test-bands.cpp")
		target_link_libraries(slughorn-test-bands PRIVATE slughorn)
		target_compile_options(slughorn-test-bands PRIVATE ${SLUGHORN_COMPILE_OPTIONS})
		message(STATUS "slughorn tests: serial found, building slughorn-test-bands")
	else()
		message(STATUS "slughorn tests: SLUGHORN_SERIAL=OFF, skipping slughorn-test-bands")
	endif()

	add_executable(slughorn-test-scanlinesweep "test/slughorn-test-scanlinesweep.cpp")
	target_link_libraries(slughorn-test-scanlinesweep PRIVATE slughorn)
	target_compile_options(slughorn-test-scanlinesweep PRIVATE ${SLUGHORN_COMPILE_OPTIONS})
	add_test(NAME scanlinesweep COMMAND slughorn-test-scanlinesweep)
endif()

# Let's be honest... I mostly care about the frontends I care about! So, these
# will all likely be AI-generated things (for now).
option(SLUGHORN_BUILD_EXAMPLES "Build example programs" OFF)

if(SLUGHORN_BUILD_EXAMPLES)
	add_subdirectory("example")
endif()
