cmake_minimum_required(VERSION 3.21)
project(em400
	DESCRIPTION "MERA-400 emulator"
	LANGUAGES C CXX
)

# silence the per-DLL dev warning file(GET_RUNTIME_DEPENDENCIES) emits about
# normalizing '\' to '/' in resolved paths before matching exclude regexes
if(POLICY CMP0207)
	cmake_policy(SET CMP0207 NEW)
endif()

include(GNUInstallDirs)
include(cmake/appversion.cmake)

# ---- Target: em400 -----------------------------------------------------

find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBUV REQUIRED IMPORTED_TARGET libuv)
find_package(BISON REQUIRED)
find_package(FLEX REQUIRED)
find_package(Threads REQUIRED)
find_package(emdas REQUIRED)
find_package(emcrk REQUIRED)
find_package(emawp 3.0 REQUIRED)
if(NOT WIN32)
	find_library(MATH_LIBRARY m)
	find_library(RT_LIBRARY rt)
endif()

# --- IPO support

include(CheckIPOSupported)
check_ipo_supported(RESULT IPO_SUPPORTED OUTPUT IPO_ERROR)
if(IPO_SUPPORTED)
	message(STATUS "LTO supported and enabled")
else()
	message(STATUS "LTO disabled: ${IPO_ERROR}")
endif()

# --- Sound (miniaudio, vendored)

if(NOT WIN32)
	find_library(DL_LIBRARY dl)
endif()

# --- QT

set(UI_QT OFF)
find_package(Qt6 COMPONENTS Widgets)
find_package(Qt6 COMPONENTS Multimedia)
find_package(Qt6 COMPONENTS Network)
find_package(Qt6 COMPONENTS LinguistTools)

if (Qt6Widgets_FOUND)
	message(STATUS "Found QT: ${Qt6Widgets_VERSION_STRING}")
	set(CMAKE_CXX_STANDARD 11)
	set(CMAKE_CXX_STANDARD_REQUIRED ON)
	set(UI_QT ON)
else()
	message(STATUS "QT not found")
endif()

# --- EM400

set(SOURCES_EM400
	src/em400.c
	src/appcfg.c
	src/appcfg.h
	src/log.c
	src/log.h
	src/log_io.h
	src/log_crk.c
	src/log_crk.h
	src/cfg.c
	src/cfg.h

	src/utils/utils.c
	src/utils/utils.h
	src/utils/elst.c
	src/utils/elst.h
	#src/utils/serial.c
	#src/utils/serial.h

	src/cpu/instructions.c
	src/cpu/instructions.h
	src/cpu/clock.c
	src/cpu/clock.h
	src/cpu/cpu.c
	src/cpu/cpu.h
	src/cpu/interrupts.c
	src/cpu/interrupts.h
	src/cpu/iset.c
	src/cpu/iset.h
	src/cpu/alu.c
	src/cpu/alu.h

	src/mem/mem.c
	src/mem/mem.h
	src/mem/mega.c
	src/mem/mega.h
	src/mem/mega_prom.c
	src/mem/mega_prom.h
	src/mem/elwro.c
	src/mem/elwro.h

	#	src/fpga/iobus.c
	#	src/fpga/iobus.h

	src/io/defs.h
	src/io/io.c
	src/io/io.h
	src/io/chan.c
	src/io/chan.h
	src/io/cchar/cchar.c
	src/io/cchar/cchar.h
	src/io/cchar/uzdat.c
	src/io/cchar/uzdat.h
	src/io/cchar/uzfx.c
	src/io/cchar/uzfx.h
	src/io/cchar/meclo.c
	src/io/cchar/meclo.h
	src/io/cchar/meclo_prom.c
	src/io/cchar/meclo_prom.h
	src/io/iotester.c
	src/io/iotester.h

	src/io/dev/dev.h
	src/io/dev/e4image.c
	src/io/dev/e4image.h
	src/io/dev/terminal.c
	src/io/dev/terminal.h
	src/io/dev/sp45de.c
	src/io/dev/sp45de.h
	src/io/dev/winchester.c
	src/io/dev/winchester.h
	src/io/dev/flop5.c
	src/io/dev/flop5.h
	src/io/dev/rtclock.h
	src/io/dev/rtclock.c

	src/io/mx/mx.c
	src/io/mx/mx.h
	src/io/mx/cmds.c
	src/io/mx/cmds.h
	src/io/mx/irq.c
	src/io/mx/irq.h
	src/io/mx/event.c
	src/io/mx/event.h
	src/io/mx/line.c
	src/io/mx/line.h
	src/io/mx/proto_common.c
	src/io/mx/proto_common.h
	src/io/mx/proto_winchester.c
	src/io/mx/proto_floppy.c
	src/io/mx/proto_tape.c
	src/io/mx/proto_puncher.c
	src/io/mx/proto_punchrd.c
	src/io/mx/proto_terminal.c
	src/io/mx/proto_som_puncher.c
	src/io/mx/proto_som_punchrd.c
	src/io/mx/proto_som_terminal.c

	src/ui/ui.c
	src/ui/ui.h

	src/ui/cmd/cmd.c
	src/ui/cmd/commands.c
	src/ui/cmd/commands.h
	src/ui/cmd/utils.c
	src/ui/cmd/utils.h

	src/cp/cp.c
	src/cp/cp.h
	src/cp/eval.c
	src/cp/eval.h
	src/cp/eval_parser.y
	src/cp/eval_scanner.l
	src/cp/brk.c
	src/cp/brk.h
	src/cp/watch.c
	src/cp/watch.h

	src/sound/sound.c
	src/sound/sound.h
	src/external/miniaudio/miniaudio.c
	src/external/miniaudio/miniaudio.h
	src/cpu/buzzer.c
	src/cpu/buzzer.h

	src/external/iniparser/dictionary.c
	src/external/iniparser/dictionary.h
	src/external/iniparser/iniparser.c
	src/external/iniparser/iniparser.h

	src/libem400.c
	src/libem400.h
)

if(WIN32)
	list(APPEND SOURCES_EM400 src/utils/compat_time.h)
	list(APPEND SOURCES_EM400 src/utils/compat_time.c)
	enable_language(RC)
	set(EM400_ICO ${CMAKE_SOURCE_DIR}/packaging/windows/em400.ico)
	configure_file(packaging/windows/em400.rc.in ${CMAKE_BINARY_DIR}/em400.rc @ONLY)
	list(APPEND SOURCES_EM400 ${CMAKE_BINARY_DIR}/em400.rc)
endif()

if (UI_QT)
	list(APPEND SOURCES_EM400 src/ui/qt6/qt6.cpp)
	list(APPEND SOURCES_EM400 src/ui/qt6/resources.qrc)

	# Translations: compile each .ts to a .qm and embed it via a build-dir qrc
	# (configured from translations.qrc.in). English is the source language and
	# needs no .ts. The .qm is built before the target's AUTORCC runs - enforced
	# by the `translations` target dependency added with the em400 target below.
	set(EM400_TS_FILES src/ui/qt6/i18n/em400-qt_pl.ts)
	qt6_add_translation(EM400_QM_FILES ${EM400_TS_FILES})
	set(QM_PL_PATH "${CMAKE_BINARY_DIR}/em400-qt_pl.qm")
	configure_file(
		${CMAKE_SOURCE_DIR}/src/ui/qt6/i18n/translations.qrc.in
		${CMAKE_BINARY_DIR}/translations.qrc @ONLY)
	list(APPEND SOURCES_EM400 ${CMAKE_BINARY_DIR}/translations.qrc)

	# Convenience target to refresh the .ts catalogs from the current sources:
	# `make lupdate`. Deliberately NOT part of the default build - it rewrites
	# source-tree .ts files, which should be an explicit, reviewed step. After
	# running it, fill in the new <translation> entries in the .ts.
	add_custom_target(lupdate
		COMMAND Qt6::lupdate -target-language pl -locations none
			${CMAKE_SOURCE_DIR}/src/ui/qt6
			-ts ${CMAKE_SOURCE_DIR}/${EM400_TS_FILES}
		WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
		COMMENT "Refreshing translation catalogs (.ts) from sources"
		VERBATIM)

	# application shell + shared infrastructure
	list(APPEND SOURCES_EM400 src/ui/qt6/app/mainwindow.ui)
	list(APPEND SOURCES_EM400 src/ui/qt6/app/mainwindow.h src/ui/qt6/app/mainwindow.cpp)
	list(APPEND SOURCES_EM400 src/ui/qt6/app/emumodel.h src/ui/qt6/app/emumodel.cpp)
	list(APPEND SOURCES_EM400 src/ui/qt6/app/theme.h src/ui/qt6/app/theme.cpp)
	list(APPEND SOURCES_EM400 src/ui/qt6/app/configdialog.h src/ui/qt6/app/configdialog.cpp)
	list(APPEND SOURCES_EM400 src/ui/qt6/app/configcontroller.h src/ui/qt6/app/configcontroller.cpp)
	list(APPEND SOURCES_EM400 src/ui/qt6/app/terminalwindow.h src/ui/qt6/app/terminalwindow.cpp)

	# dockable debugger views
	list(APPEND SOURCES_EM400 src/ui/qt6/views/dasmview.h src/ui/qt6/views/dasmview.cpp)
	list(APPEND SOURCES_EM400 src/ui/qt6/views/dasmlisting.h src/ui/qt6/views/dasmlisting.cpp)
	list(APPEND SOURCES_EM400 src/ui/qt6/views/asmline.h src/ui/qt6/views/asmline.cpp)
	list(APPEND SOURCES_EM400 src/ui/qt6/views/memview.h src/ui/qt6/views/memview.cpp)
	list(APPEND SOURCES_EM400 src/ui/qt6/views/memlisting.h src/ui/qt6/views/memlisting.cpp)
	list(APPEND SOURCES_EM400 src/ui/qt6/views/memsearch.h src/ui/qt6/views/memsearch.cpp)
	list(APPEND SOURCES_EM400 src/ui/qt6/views/regview.h src/ui/qt6/views/regview.cpp)
	list(APPEND SOURCES_EM400 src/ui/qt6/views/intview.h src/ui/qt6/views/intview.cpp)
	list(APPEND SOURCES_EM400 src/ui/qt6/views/mapview.h src/ui/qt6/views/mapview.cpp)
	list(APPEND SOURCES_EM400 src/ui/qt6/views/brkview.h src/ui/qt6/views/brkview.cpp)
	list(APPEND SOURCES_EM400 src/ui/qt6/views/watchview.h src/ui/qt6/views/watchview.cpp)
	list(APPEND SOURCES_EM400 src/ui/qt6/views/stackview.h src/ui/qt6/views/stackview.cpp)

	# physical control panel (pulpit) replica
	list(APPEND SOURCES_EM400 src/ui/qt6/panel/controlpanel.h src/ui/qt6/panel/controlpanel.cpp)
	list(APPEND SOURCES_EM400 src/ui/qt6/panel/switch.h src/ui/qt6/panel/switch.cpp)
	list(APPEND SOURCES_EM400 src/ui/qt6/panel/led.h src/ui/qt6/panel/led.cpp)
	list(APPEND SOURCES_EM400 src/ui/qt6/panel/buswleds.h src/ui/qt6/panel/buswleds.cpp)
	list(APPEND SOURCES_EM400 src/ui/qt6/panel/binarykeys.h src/ui/qt6/panel/binarykeys.cpp)
	list(APPEND SOURCES_EM400 src/ui/qt6/panel/rotary.h src/ui/qt6/panel/rotary.cpp)
	list(APPEND SOURCES_EM400 src/ui/qt6/panel/ignition.h src/ui/qt6/panel/ignition.cpp)
	list(APPEND SOURCES_EM400 src/ui/qt6/panel/psu.h src/ui/qt6/panel/psu.cpp)
endif()

# --- embedded default config (cfg/em400.ini as a NUL-terminated byte array)

set(EMBED_CONFIG_SRC ${CMAKE_BINARY_DIR}/default_config.c)
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
	${CMAKE_SOURCE_DIR}/cfg/em400.ini)
file(READ ${CMAKE_SOURCE_DIR}/cfg/em400.ini EMBED_CONFIG_HEX HEX)
string(REGEX REPLACE "(..)" "0x\\1," EMBED_CONFIG_BYTES "${EMBED_CONFIG_HEX}")
file(WRITE ${EMBED_CONFIG_SRC}
	"#include \"default_config.h\"\n\nconst char default_config_ini[] = {\n${EMBED_CONFIG_BYTES}0x00\n};\n")
list(APPEND SOURCES_EM400 src/default_config.h ${EMBED_CONFIG_SRC})

# --- internal parser

bison_target(eval_parser
	src/cp/eval_parser.y
	${CMAKE_CURRENT_BINARY_DIR}/eval_parser.c
	DEFINES_FILE ${CMAKE_CURRENT_BINARY_DIR}/eval_parser.h
	COMPILE_FLAGS "-p eval_yy"
)
flex_target(eval_scanner
	src/cp/eval_scanner.l
	${CMAKE_CURRENT_BINARY_DIR}/eval_scanner.c
	COMPILE_FLAGS "-P eval_yy --never-interactive"
)
add_flex_bison_dependency(eval_scanner eval_parser)

# "%define parse.error custom" makes bison emit a yypcontext_expected_tokens()
# helper for building "expecting ..." lists; our custom handler reports by token
# kind only and leaves it unused. Silence the resulting -Wunused-function for
# this generated file alone (we do not control its contents).
set_source_files_properties(
	${CMAKE_CURRENT_BINARY_DIR}/eval_parser.c
	PROPERTIES COMPILE_OPTIONS "-Wno-unused-function"
)

# --- em400 target

add_executable(em400
	${SOURCES_EM400}
	${BISON_eval_parser_OUTPUTS}
	${FLEX_eval_scanner_OUTPUTS}
)

set_property(TARGET em400 PROPERTY C_STANDARD 23)
target_compile_options(em400 PRIVATE -Wall)
target_include_directories(em400 PRIVATE ${CMAKE_SOURCE_DIR}/src)
target_include_directories(em400 PRIVATE ${CMAKE_SOURCE_DIR}/include)
target_include_directories(em400 PRIVATE ${CMAKE_BINARY_DIR})
target_compile_definitions(em400 PRIVATE EM400_VERSION="${APP_VERSION}")

if(IPO_SUPPORTED)
	set_property(TARGET em400 PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)
endif()

if (UI_QT)
	target_compile_definitions(em400 PRIVATE $<$<CONFIG:Release>:QT_NO_DEBUG_OUTPUT>)
	set_property(TARGET em400 PROPERTY AUTOMOC ON)
	set_property(TARGET em400 PROPERTY AUTOUIC ON)
	set_property(TARGET em400 PROPERTY AUTORCC ON)
	set_property(SOURCE ${CMAKE_CURRENT_BINARY_DIR}/eval_parser.c PROPERTY SKIP_AUTOGEN ON)
	set_property(SOURCE ${CMAKE_CURRENT_BINARY_DIR}/eval_parser.h PROPERTY SKIP_AUTOGEN ON)
	set_property(SOURCE ${CMAKE_CURRENT_BINARY_DIR}/eval_scanner.c PROPERTY SKIP_AUTOGEN ON)
		set_property(SOURCE ${EMBED_CONFIG_SRC} PROPERTY SKIP_AUTOGEN ON)
	target_include_directories(em400 PRIVATE ${Qt6Widgets_INCLUDE_DIRS})
	# qt6 sources live in subdirs (app/, panel/, views/) but include each
	# other with bare "foo.h" includes; expose the subdirs on the search path
	target_include_directories(em400 PRIVATE
		${CMAKE_SOURCE_DIR}/src/ui/qt6
		${CMAKE_SOURCE_DIR}/src/ui/qt6/app
		${CMAKE_SOURCE_DIR}/src/ui/qt6/panel
		${CMAKE_SOURCE_DIR}/src/ui/qt6/views)
	target_compile_definitions(em400 PRIVATE UI_QT)
	target_link_libraries(em400 ${Qt6Widgets_LIBRARIES})
	target_link_libraries(em400 ${Qt6Multimedia_LIBRARIES})
	target_link_libraries(em400 ${Qt6Network_LIBRARIES})

	# Build the .qm files before em400's AUTORCC embeds them via translations.qrc.
	add_custom_target(em400_translations DEPENDS ${EM400_QM_FILES})
	add_dependencies(em400 em400_translations)
endif()

target_link_libraries(em400
	emdas
	emcrk
	emawp
	${BISON_LIBRARIES}
	Threads::Threads
	PkgConfig::LIBUV
)

if(WIN32)
	target_link_libraries(em400 ws2_32 winmm)
	# windows GUI: no console window behind the Qt UI
	set_property(TARGET em400 PROPERTY WIN32_EXECUTABLE ON)
else()
	target_link_libraries(em400
		${RT_LIBRARY}
		${MATH_LIBRARY}
	)
endif()

# miniaudio: silence -Wall noise in the vendored implementation TU, and link
# libdl on Linux so its runtime loader for ALSA/PulseAudio/JACK works.
set_source_files_properties(src/external/miniaudio/miniaudio.c
	PROPERTIES COMPILE_FLAGS "-w")
if(NOT WIN32 AND DL_LIBRARY)
	target_link_libraries(em400 ${DL_LIBRARY})
endif()

# Off by default so a plain `install` stays lightweight (as on Linux). The
# Windows Installer workflow turns it on to vendor dependency DLLs and produce
# the NSIS package; a bare install into a mingw prefix resolves those DLLs via PATH.
option(EM400_BUILD_INSTALLER "Build the self-contained Windows installer" OFF)

# collects each exe's DLL dependencies for the Windows package
if(WIN32 AND EM400_BUILD_INSTALLER)
	set(DEP_SET RUNTIME_DEPENDENCY_SET em400-deps)
endif()

install(TARGETS em400 ${DEP_SET}
	RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

# ---- Target: emitool ---------------------------------------------------

add_executable(emitool
	src/emitool.c
	src/io/dev/e4image.c
)
if(WIN32)
	target_link_libraries(emitool ws2_32)
endif()
set_property(TARGET emitool PROPERTY C_STANDARD 23)
target_include_directories(emitool PRIVATE ${CMAKE_SOURCE_DIR}/src)
target_compile_options(emitool PUBLIC -Wall)

install(TARGETS emitool ${DEP_SET}
	RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

# ---- Target: embin -----------------------------------------------------

add_executable(embin
	src/embin.c
	src/utils/utils.c
)
if(NOT WIN32)
	target_sources(embin PRIVATE src/utils/serial.c)
else()
	target_link_libraries(embin ws2_32)
endif()
set_property(TARGET embin PROPERTY C_STANDARD 23)
target_include_directories(embin PRIVATE ${CMAKE_SOURCE_DIR}/src)
target_compile_options(embin PUBLIC -Wall)

install(TARGETS embin ${DEP_SET}
	RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

# ---- Target: emterm ----------------------------------------------------

if(NOT WIN32)
	add_executable(emterm src/emterm.c)
	set_property(TARGET emterm PROPERTY C_STANDARD 23)
	target_compile_options(emterm PUBLIC -Wall)

	install(TARGETS emterm
		RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
	)
endif()

# ---- Packaging: Windows installer (CPack/NSIS) ---------------------------

if(WIN32 AND EM400_BUILD_INSTALLER)
	get_filename_component(MINGW_BINDIR ${CMAKE_C_COMPILER} DIRECTORY)
	install(RUNTIME_DEPENDENCY_SET em400-deps
		PRE_EXCLUDE_REGEXES "api-ms-" "ext-ms-" "hvsifiletrust" "pdmutilities"
		POST_EXCLUDE_REGEXES ".*system32/.*\\.dll"
		DIRECTORIES ${MINGW_BINDIR}
		RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
	)

	if(UI_QT)
		find_program(WINDEPLOYQT_EXECUTABLE NAMES windeployqt-qt6 windeployqt)
		if(WINDEPLOYQT_EXECUTABLE)
			install(CODE "execute_process(COMMAND \"${WINDEPLOYQT_EXECUTABLE}\" --release --no-translations \"\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}/em400.exe\" COMMAND_ERROR_IS_FATAL ANY)")
			# ship the Windows-native (WMF) media backend only: em400 uses
			# QSoundEffect, and MSYS2's ffmpeg is a GPL-3 build incompatible
			# with em400's GPL-2. Dropping its plugin leaves WMF as the sole
			# backend, which Qt then picks automatically.
			install(CODE "set(bindir \"\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}\")
file(REMOVE \"\${bindir}/multimedia/ffmpegmediaplugin.dll\")
file(GLOB ffmpeg_dlls \"\${bindir}/av*.dll\" \"\${bindir}/sw*.dll\")
if(ffmpeg_dlls)
	file(REMOVE \${ffmpeg_dlls})
endif()")
		else()
			message(WARNING "windeployqt not found - installed package will lack the Qt runtime")
		endif()
	endif()

	# must stay the last install rule: it scans the fully staged bin dir
	configure_file(packaging/windows/licenses.cmake.in ${CMAKE_BINARY_DIR}/licenses.cmake @ONLY)
	install(SCRIPT ${CMAKE_BINARY_DIR}/licenses.cmake)

	set(CPACK_GENERATOR "NSIS")
	set(CPACK_PACKAGE_NAME "em400")
	set(CPACK_PACKAGE_VENDOR "mera400.pl")
	set(CPACK_PACKAGE_HOMEPAGE_URL "http://mera400.pl")
	set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "${PROJECT_DESCRIPTION}")
	set(CPACK_PACKAGE_VERSION "${APP_VERSION}")
	set(CPACK_PACKAGE_VERSION_MAJOR ${APP_VERSION_MAJOR})
	set(CPACK_PACKAGE_VERSION_MINOR ${APP_VERSION_MINOR})
	set(CPACK_PACKAGE_VERSION_PATCH ${APP_VERSION_PATCH})
	set(CPACK_PACKAGE_INSTALL_DIRECTORY "em400")
	set(CPACK_PACKAGE_FILE_NAME "em400-${APP_VERSION}-win64")
	set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/COPYING")
	set(CPACK_PACKAGE_EXECUTABLES "em400" "EM400")
	set(CPACK_CREATE_DESKTOP_LINKS "em400")
	set(CPACK_NSIS_DISPLAY_NAME "EM400 - MERA-400 emulator")
	set(CPACK_NSIS_PACKAGE_NAME "EM400")
	set(CPACK_NSIS_URL_INFO_ABOUT "http://mera400.pl")
	set(CPACK_NSIS_MUI_ICON "${EM400_ICO}")
	set(CPACK_NSIS_MUI_UNIICON "${EM400_ICO}")
	set(CPACK_NSIS_INSTALLED_ICON_NAME "bin\\\\em400.exe")
	set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON)
	set(CPACK_NSIS_MODIFY_PATH ON)
	set(CPACK_NSIS_MANIFEST_DPI_AWARE ON)
	include(CPack)
endif()

# vim: tabstop=4
