cmake_minimum_required(VERSION 3.21)
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
project(embed)

# Options
option(BUILD_EXAMPLES "Build examples" ${PROJECT_IS_TOP_LEVEL})

# Remember the binary dir for later
set(EMBED_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/embed CACHE INTERNAL "binary directory of the battery::embed library" FORCE)
set(EMBED_HPP "${EMBED_BINARY_DIR}/include/battery/embed.hpp" CACHE INTERNAL "path to the battery/embed.hpp header file" FORCE)

# The template for how to generate the .cpp file
set(EMBED_SOURCE_FILE_TEMPLATE [==[
// File generated using battery::embed (https://github.com/batterycenter/embed)
// Embedded file: ${FILENAME} as '${IDENTIFIER}'
// Filesize: ${FILESIZE} bytes
// DO NOT EDIT THIS FILE!!!

#include <cinttypes>
#include <cstddef>
#include <string>
#include <vector>
#include "battery/embed.hpp"

namespace b {

    embed_internal::embedded_file embed_internal::${IDENTIFIER} = {
        std::string_view(
        ${GENERATED_BYTE_ARRAY}
        , ${FILESIZE})
        , "${FILENAME}" };

} // namespace EmbedInternal
]==])
file(WRITE ${EMBED_BINARY_DIR}/embed_source_file_template.cpp "${EMBED_SOURCE_FILE_TEMPLATE}")

# The common battery::embed header and source files
set(EMBED_HEADER_FILE [=[
// File generated by battery::embed
// DO NOT EDIT THIS FILE!!!
#ifndef BATTERY_EMBED_HPP
#define BATTERY_EMBED_HPP

#include <vector>
#include <string>
#include <string_view>
#include <stdexcept>
#include <sstream>

#ifndef __cpp_constexpr_dynamic_alloc
#   error "battery::embed requires C++20"
#endif

namespace b {

    struct embed_internal {

        class embedded_file {
        public:
            constexpr embedded_file() = default;
            constexpr embedded_file(const std::string_view& data, const std::string_view& filename)
                : m_data(data), m_filename(filename) {};

            std::string str() const {
                return m_data.data();
            }

            const char* data() const {
                return m_data.data();
            }

            std::vector<uint8_t> vec() const {
                return std::vector<uint8_t>(m_data.begin(), m_data.end());
            }

            size_t length() const {
                return m_data.size();
            }

            size_t size() const {
                return m_data.size();
            }

            operator std::string() {
                return str();
            }

            operator std::vector<uint8_t>() {
                return vec();
            }

        private:
            std::string_view m_data;
            std::string_view m_filename;
        }; // class embedded_file

        ${EMBEDDED_FILES_DECLARATIONS}
    };   // struct embedded_files

    template<size_t N>
    struct embed_string_literal {
        constexpr embed_string_literal(const char (&str)[N]) {
            std::copy_n(str, N, value);
        }
        constexpr bool operator!=(const embed_string_literal& other) const {
            return std::equal(value, value + N, other.value);
        }
        std::string str() const {
            return std::string(value, N);
        }
        constexpr bool _false() const {
            return false;
        }
        char value[N];
    };

    template<size_t N, size_t M>
    constexpr bool operator==(const embed_string_literal<N>& left, const char (&right)[M]) {
        return std::equal(left.value, left.value + N, right);
    }

    template<embed_string_literal identifier>
    constexpr embed_internal::embedded_file embed() {
        ${EMBEDDED_FILES_RETURNS}{
            static_assert(identifier._false(), "[b::embed<>] No such file or directory");
        }
    }

} // namespace b

inline std::ostream& operator<<(std::ostream& stream, const b::embed_internal::embedded_file& file) {
    stream << file.str();
    return stream;
}

#endif // BATTERY_EMBED_HPP
]=])
file(WRITE ${EMBED_BINARY_DIR}/embed_header_file_template.hpp "${EMBED_HEADER_FILE}")

# The cmake script to embed the files. This is called later on-demand, in a separate process
set(EMBED_GENERATE_SCRIPT [=[
file(READ ${FULL_PATH} GENERATED_BYTE_ARRAY HEX)
string(LENGTH "${GENERATED_BYTE_ARRAY}" FILESIZE)
math(EXPR FILESIZE "${FILESIZE} / 2")

string(REPEAT "[0-9a-f]" 32 PATTERN)
set(GENERATED_BYTE_ARRAY "\"${GENERATED_BYTE_ARRAY}")
string(REGEX REPLACE "${PATTERN}" "\\0\"\n        \"" GENERATED_BYTE_ARRAY ${GENERATED_BYTE_ARRAY})
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "\\\\x\\1" GENERATED_BYTE_ARRAY ${GENERATED_BYTE_ARRAY})
set(GENERATED_BYTE_ARRAY "${GENERATED_BYTE_ARRAY}\"")
configure_file(${INFILE} ${OUTFILE})
]=])
file(WRITE ${EMBED_BINARY_DIR}/generate.cmake ${EMBED_GENERATE_SCRIPT})

# A private file to remember what identifiers are already applied to the header
file(WRITE ${EMBED_BINARY_DIR}/identifiers.cache "56")

set(EMBED_IDENTIFIERS "" CACHE INTERNAL "list of all identifiers used by battery::embed")
set(EMBED_FILENAMES "" CACHE INTERNAL "list of all filenames used by battery::embed")

# Defer the function call until the end of the configure step
cmake_language(DEFER DIRECTORY ${CMAKE_SOURCE_DIR} CALL _embed_generate_hpp())
function(_embed_generate_hpp)
    set(EMBEDDED_FILES_DECLARATIONS "")
    list(LENGTH EMBED_IDENTIFIERS num_identifiers)
    foreach (IDENTIFIER IN LISTS EMBED_IDENTIFIERS)
        set(EMBEDDED_FILES_DECLARATIONS "${EMBEDDED_FILES_DECLARATIONS}static embedded_file ${IDENTIFIER};\n        ")
    endforeach()
    set(EMBEDDED_FILES_RETURNS "")
    math(EXPR num_identifiers "${num_identifiers} - 1")
    foreach (INDEX RANGE ${num_identifiers})
        list(GET EMBED_IDENTIFIERS ${INDEX} IDENTIFIER)
        list(GET EMBED_FILENAMES ${INDEX} FILENAME)
        set(EMBEDDED_FILES_RETURNS "${EMBEDDED_FILES_RETURNS}if constexpr (identifier == \"${FILENAME}\") { return embed_internal::${IDENTIFIER}; }\n        else ")
    endforeach()
    file(READ ${EMBED_BINARY_DIR}/embed_header_file_template.hpp EMBED_HEADER_FILE)
    string(CONFIGURE "${EMBED_HEADER_FILE}" EMBED_HEADER_FILE_GENERATED)
    file(WRITE ${EMBED_HPP} "${EMBED_HEADER_FILE_GENERATED}")
endfunction(_embed_generate_hpp)

# Internal function for validating an identifier
function(embed_validate_identifier IDENTIFIER)  # Validate the identifier against C variable naming rules
    if (NOT IDENTIFIER MATCHES "^[a-zA-Z_][a-zA-Z0-9_]*$")
        message(FATAL_ERROR "embed: Identifier contains invalid characters: '${IDENTIFIER}'")
    endif()
endfunction()

# The main function for embedding files
function(b_embed TARGET FILENAME)

    if (IS_ABSOLUTE "${FILENAME}")
        message(FATAL_ERROR "embed: File name must be relative to the current source directory: '${FILENAME}'")
    endif()

    # Make the identifier
    string(REGEX REPLACE "[^a-zA-Z0-9_]" "_" IDENTIFIER "${FILENAME}") # Replace all invalid characters with underscores
    string(TOLOWER "${IDENTIFIER}" IDENTIFIER) # Make the identifier all lowercase
    embed_validate_identifier("${IDENTIFIER}") # Validate the identifier against C variable naming rules

    # Set up paths
    get_filename_component(FULL_PATH "${FILENAME}" ABSOLUTE) # Make the file path absolute
    set(GENERATED_FILES "${EMBED_BINARY_DIR}/autogen/${IDENTIFIER}.cpp" "${EMBED_HPP_FILE}")

    # If identifier already in use
    list(FIND EMBED_IDENTIFIERS ${IDENTIFIER} EMBED_USED_IDENTIFIERS_INDEX)
    if (NOT EMBED_USED_IDENTIFIERS_INDEX EQUAL -1)
        message(FATAL_ERROR "embed: Identifier already in use: '${IDENTIFIER}'")
    endif()
    set(EMBED_IDENTIFIERS ${EMBED_IDENTIFIERS} ${IDENTIFIER} CACHE INTERNAL "list of all identifiers used by the embed library")
    set(EMBED_FILENAMES ${EMBED_FILENAMES} ${FILENAME} CACHE INTERNAL "list of all filenames used by the embed library")

    # In Visual Studio, add the resource file as a source file, and glob it in the folder structure
    if (MSVC)
        target_sources(${TARGET} PUBLIC ${FULL_PATH})
        get_filename_component(PARENT_FOLDER "${FULL_PATH}" DIRECTORY)  # Get the resource file's parent directory
        source_group(TREE "${PARENT_FOLDER}" PREFIX "${CMAKE_CURRENT_SOURCE_DIR}" FILES ${FULL_PATH})
    endif()

    # This action generates both files and is called on-demand whenever the resource file changes
    add_custom_command(
            COMMAND ${CMAKE_COMMAND}
                    -DEMBED_HPP="${EMBED_HPP}"
                    -DINFILE="${EMBED_BINARY_DIR}/embed_source_file_template.cpp"
                    -DEMBED_ADDITIONAL_OPERATORS="${EMBED_ADDITIONAL_OPERATORS}"
                    -DOUTFILE="${EMBED_BINARY_DIR}/autogen/${IDENTIFIER}.cpp"
                    -DEMBED_BINARY_DIR="${EMBED_BINARY_DIR}"
                    -DIDENTIFIER="${IDENTIFIER}"
                    -DFULL_PATH="${FULL_PATH}"
                    -DFILENAME="${FILENAME}"
                    -DFILESIZE=${FILESIZE}
                    -P "${EMBED_BINARY_DIR}/generate.cmake"
            DEPENDS "${FULL_PATH}"
                    "${EMBED_HPP}"
                    "${EMBED_BINARY_DIR}/generate.cmake"
                    "${EMBED_BINARY_DIR}/embed_source_file_template.cpp"
            OUTPUT ${GENERATED_FILES}
    )

    # Add the generated files to the target
    target_include_directories(${TARGET} PUBLIC ${EMBED_BINARY_DIR}/include)
    target_sources(${TARGET} PRIVATE ${GENERATED_FILES})
    source_group(TREE ${EMBED_BINARY_DIR}/autogen PREFIX "autogen" FILES ${GENERATED_FILES})

endfunction()


# Build all examples
if (BUILD_EXAMPLES)
    add_subdirectory(examples)      # Set Example 'simple' as the default project in Visual Studio
    set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT simple)
endif()

# Set the predefined targets folder for Visual Studio
if (PROJECT_IS_TOP_LEVEL)
    set(PREDEFINED_TARGETS_FOLDER "CMakePredefinedTargets")
endif()
