include(GetArchitecture)
include(RequireCXXStd)

# On Windows, shell scripts (.sh) need to be invoked through bash.
if (WIN32 AND NOT ZEEK_PLUGIN_SHELL_PREFIX)
    find_program(_bash_exe bash)
    if (_bash_exe)
        set(ZEEK_PLUGIN_SHELL_PREFIX "${_bash_exe}" CACHE INTERNAL "Shell prefix for .sh scripts")
    endif ()
endif ()
if (NOT DEFINED ZEEK_PLUGIN_SHELL_PREFIX)
    set(ZEEK_PLUGIN_SHELL_PREFIX "" CACHE INTERNAL "Shell prefix for .sh scripts")
endif ()

# Sets `target` to contain the CMake target name for a dynamic plugin.
macro (zeek_get_dynamic_plugin_target target ns name)
    set(${target} "${ns}_${name}")
endmacro ()

# Implements the dynamically linked version of zeek_add_plugin.
function (zeek_add_dynamic_plugin ns name)
    # Sanity check: need ZEEK_PLUGIN_SCRIPTS_PATH.
    if (NOT EXISTS "${ZEEK_PLUGIN_SCRIPTS_PATH}")
        message(
            FATAL_ERROR
                "Cannot build dynamic plugins: ZEEK_PLUGIN_SCRIPTS_PATH is undefined or invalid")
    endif ()

    # Helper variables.
    zeek_get_dynamic_plugin_target(target_name ${ns} ${name})
    set(full_name "${ns}::${name}")
    set(canon_name "${ns}_${name}")
    set(base_dir "${CMAKE_CURRENT_BINARY_DIR}")
    set(lib_dir "${base_dir}/lib")
    set(bif_dir "${lib_dir}/bif")
    set(readme "${base_dir}/README")
    set(scripts_bin "${base_dir}/scripts")
    set(scripts_src "${CMAKE_CURRENT_SOURCE_DIR}/scripts")

    # Create the target if no begin function has been used.
    if (NOT TARGET ${target_name})
        add_library(${target_name} MODULE)

        target_compile_features(${target_name} PRIVATE ${ZEEK_CXX_STD})
        set_target_properties(${target_name} PROPERTIES CXX_EXTENSIONS OFF)
    endif ()

    # On MSVC with static CRT, each DLL has its own stdout buffer.
    # Generate a DllMain that disables stdout buffering so plugin
    # printf output appears in correct order when stdout is redirected.
    if (MSVC)
        set(_dllmain_src "${CMAKE_CURRENT_BINARY_DIR}/zeek_plugin_dllmain.cc")
        if (NOT EXISTS "${_dllmain_src}")
            file(
                WRITE "${_dllmain_src}"
                "#include <cstdio>\n"
                "// With static CRT (/MT), each DLL has its own stdout.\n"
                "// Set it to unbuffered so output is correctly ordered,\n"
                "// and close it at shutdown to prevent the DLL CRT's\n"
                "// atexit handler from corrupting the shared fd.\n"
                "namespace { struct ZeekPluginStdoutInit {\n"
                "    ZeekPluginStdoutInit() {\n"
                "        setvbuf(stdout, nullptr, _IONBF, 0);\n"
                "    }\n"
                "    ~ZeekPluginStdoutInit() {\n"
                "        fclose(stdout);\n"
                "        fclose(stderr);\n"
                "    }\n"
                "} zeek_plugin_stdout_init; }\n")
        endif ()
        target_sources(${target_name} PRIVATE "${_dllmain_src}")
    endif ()

    # Place library file into the 'lib' directory, drop default-generated file
    # prefix and override the default file name to include the architecture.
    set_target_properties(
        ${target_name} PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${lib_dir}" PREFIX ""
                                  LIBRARY_OUTPUT_NAME "${ns}-${name}.${HOST_ARCHITECTURE}")

    # Parse arguments (note: DIST_FILES and SCRIPT_FILES are ignored in static builds).
    set(fn_varargs
        INCLUDE_DIRS
        DEPENDENCIES
        SOURCES
        BIFS
        DIST_FILES
        SCRIPT_FILES
        PAC)
    cmake_parse_arguments(FN_ARGS "" "" "${fn_varargs}" ${ARGN})

    # Take care of compiling BIFs.
    if (FN_ARGS_BIFS)
        # Generate the targets and add the .cc files.
        foreach (bif ${FN_ARGS_BIFS})
            bif_target(${bif} "plugin" ${full_name} ${canon_name} OFF)
            target_sources(${target_name} PRIVATE ${BIF_OUTPUT_CC})
        endforeach ()
        # Generate __load__.zeek when building the plugin outside of Zeek.
        if (ZEEK_PLUGIN_INTERNAL_BUILD)
            set(loader_target ${target_name}_bif_loader)
            bro_bif_create_loader(${loader_target} ${FN_ARGS_BIFS})
            add_dependencies(${target_name} ${loader_target})
        else ()
            set(load_script "${bif_dir}/__load__.zeek")
            file(WRITE ${load_script} "# Warning, this is an autogenerated file!\n")
            foreach (bif ${FN_ARGS_BIFS})
                get_filename_component(file_name ${bif} NAME)
                file(APPEND ${load_script} "@load ./${file_name}.zeek\n")
            endforeach ()
        endif ()
    endif ()

    # Take care of PAC files.
    zeek_next_pac_block(at_end pacInputs pacRemainder ${ARGN})
    while (NOT at_end)
        binpac_target(${pacInputs})
        target_sources(${target_name} PRIVATE ${BINPAC_OUTPUT_CC})
        zeek_next_pac_block(at_end pacInputs pacRemainder ${pacRemainder})
    endwhile ()

    # Add user-defined extra include directories.
    if (FN_ARGS_INCLUDE_DIRS)
        target_include_directories(${target_name} PRIVATE ${FN_ARGS_INCLUDE_DIRS})
    endif ()

    # Add user-defined extra dependencies.
    if (FN_ARGS_DEPENDENCIES)
        target_link_libraries(${target_name} PRIVATE ${FN_ARGS_DEPENDENCIES})
    endif ()

    # Add the sources for the plugin.
    if (FN_ARGS_SOURCES)
        target_sources(${target_name} PRIVATE ${FN_ARGS_SOURCES})
    endif ()

    # Add extra dependencies when compiling with MSVC.
    if (MSVC)
        target_link_libraries(${target_name} PRIVATE ws2_32)
        # Force the plugin DLL to import CRT allocation functions from
        # zeek.exe. With /MT, each module has its own heap; importing
        # operator new/delete ensures all allocations use the same heap,
        # preventing cross-module heap mismatches when objects are created
        # in plugin code and freed by zeek or vice versa.
        target_link_options(
            ${target_name}
            PRIVATE
            "/INCLUDE:??2@YAPEAX_K@Z"
            "/INCLUDE:??_U@YAPEAX_K@Z"
            "/INCLUDE:??3@YAXPEAX@Z"
            "/INCLUDE:??3@YAXPEAX_K@Z"
            "/INCLUDE:??_V@YAXPEAX@Z"
            "/INCLUDE:??_V@YAXPEAX_K@Z")
    endif ()

    # Pass compiler flags, paths and dependencies to the target.
    target_link_libraries(${target_name} PRIVATE Zeek::DynamicPluginBase)
    target_include_directories(${target_name} PRIVATE ${CMAKE_CURRENT_BINARY_DIR})

    # Per convention, plugins have their headers and sources under src/ and
    # legacy/external plugins expect this to auto-magically be available as
    # include path.
    if (EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/src)
        target_include_directories(${target_name} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
    endif ()

    # Link scripts into the build directory (if present).
    if (IS_DIRECTORY "${scripts_src}")
        set(symlink_target ${target_name}_symlink)
        add_custom_target(${symlink_target} COMMAND "${CMAKE_COMMAND}" -E create_symlink
                                                    "${scripts_src}" "${scripts_bin}")
        add_dependencies(${target_name} ${symlink_target})
    endif ()

    # Add BinPAC_INCLUDE_DIR for picking up paths from FindBinPAC.cmake.
    if (BinPAC_INCLUDE_DIR)
        target_include_directories(${target_name} PRIVATE ${BinPAC_INCLUDE_DIR})
    endif ()

    # Write the 'magic' __zeek_plugin__ file. We can do that once during CMake
    # invocation since it won't change.
    file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/__zeek_plugin__" "${full_name}\n")

    # Stop here unless building 3rd party plugins.
    if (ZEEK_PLUGIN_INTERNAL_BUILD)
        return()
    endif ()

    # Plugins may set BRO_PLUGIN_INSTALL_ROOT to override the default
    # installation directory. Otherwise, we use ZEEK_PLUGIN_DIR from Zeek.
    if (BRO_PLUGIN_INSTALL_ROOT)
        set(install_dir "${BRO_PLUGIN_INSTALL_ROOT}")
    else ()
        set(install_dir ${ZEEK_PLUGIN_DIR})
    endif ()

    # Create the binary install package.
    set(dist_tarball_dir ${CMAKE_CURRENT_BINARY_DIR}/dist/${canon_name})
    set(dist_tarball_name ${canon_name}.tgz)
    set(dist_tarball_path ${CMAKE_CURRENT_BINARY_DIR}/${dist_tarball_name})
    message(STATUS "Install prefix for plugin ${canon_name}: ${install_dir}")
    message(STATUS "Tarball path for plugin ${canon_name}: ${dist_tarball_path}")
    add_custom_command(OUTPUT ${dist_tarball_path} WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
                       COMMENT "Building binary plugin package: ${dist_tarball_path}")

    # Handle dist files: Pre-place user provided dist files into the
    # directory that zeek-plugin-create-package.sh will tar up. Previously,
    # the zeek-plugin-create-package.sh script had done this itself.
    #
    # This computes the relative path of individual DIST_FILES to the
    # PROJECT_SOURCE_DIR (last project() invocation) and uses the
    # resulting path in the tarball.
    foreach (df ${FN_ARGS_DIST_FILES})
        get_filename_component(df_src "${df}" ABSOLUTE)

        if (NOT EXISTS "${df_src}")
            # This was silently ignored previously.
            message(WARNING "The file ${df} (${df_src}) given to DIST_FILES does not exist")
            continue()
        endif ()

        file(RELATIVE_PATH df_dist ${PROJECT_SOURCE_DIR} ${df_src})

        add_custom_command(
            OUTPUT ${dist_tarball_path}
            COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/${df}
                    ${dist_tarball_dir}/${df_dist}
            DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${df}
            APPEND)
    endforeach ()

    add_custom_command(
        OUTPUT ${dist_tarball_path}
        COMMAND ${ZEEK_PLUGIN_SHELL_PREFIX}
                ${ZEEK_PLUGIN_SCRIPTS_PATH}/zeek-plugin-create-package.sh ${canon_name}
        WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
        DEPENDS ${target_name} ${FN_ARGS_SCRIPT_FILES}
        APPEND)

    add_custom_target(${target_name}_tarball ALL DEPENDS ${dist_tarball_path})

    # Tell CMake to install our tarball. Note: This usually runs from our
    # plugin-support skeleton.
    if (ZEEK_PLUGIN_SHELL_PREFIX)
        install(
            CODE "
            if (DEFINED ENV{DESTDIR})
                set(_inst_dir \"\$ENV{DESTDIR}/${install_dir}\")
            else ()
                set(_inst_dir \"${install_dir}\")
            endif ()
            execute_process(
                COMMAND \"${ZEEK_PLUGIN_SHELL_PREFIX}\" \"${ZEEK_PLUGIN_SCRIPTS_PATH}/zeek-plugin-install-package.sh\" ${canon_name} \"\${_inst_dir}\"
                WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
                COMMAND_ECHO STDOUT
            )")
    else ()
        install(
            CODE "execute_process(
            COMMAND ${ZEEK_PLUGIN_SCRIPTS_PATH}/zeek-plugin-install-package.sh ${canon_name} \$ENV{DESTDIR}/${install_dir}
            WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
            COMMAND_ECHO STDOUT
        )")
    endif ()
endfunction ()
