cmake_minimum_required(VERSION 3.15)
project(nanoprintf)

option(NPF_32BIT "Compile nanoprintf tests in 32-bit mode")
option(NPF_PALAND "Compile and run the mpaland printf test suite")
option(NPF_CLANG_ASAN "Compile and run tests with address sanitizer")
option(NPF_CLANG_UBSAN "Compile and run tests with undefined behavior sanitizer")

if (NPF_32BIT AND CMAKE_HOST_APPLE)
  message(FATAL_ERROR "Apple doesn't support 32-bit mode anymore.")
endif()

if ((NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang") AND (NPF_CLANG_ASAN OR NPF_CLANG_UBSAN))
  message(FATAL_ERROR "Can only use asan/ubsan on Clang configurations.")
endif()

if (NPF_CLANG_ASAN AND NPF_CLANG_UBSAN)
  message(FATAL_ERROR "Can only use one of asan/ubsan per configuration.")
endif()

if (NPF_32BIT AND NOT MSVC)
  set(NPF_32BIT_FLAG -m32)
  set(CMAKE_C_FLAGS ${CMAKE_C_FLAGS} ${NPF_32BIT_FLAG})
  set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} ${NPF_32BIT_FLAG})
endif()

################ Common compile flags

set(CMAKE_C_STANDARD 99)
set(CMAKE_CXX_STANDARD 17)

if (MSVC)
  set(nanoprintf_common_flags /Wall /WX)
else()
  set(CMAKE_C_FLAGS_DEBUG "-O0 -g3")
  set(CMAKE_C_FLAGS_RELWITHDEBINFO "-Os -g3")
  set(CMAKE_C_FLAGS_RELEASE "-Os")
  set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g3")
  set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-Os -g3")
  set(CMAKE_CXX_FLAGS_RELEASE "-Os")

  set(nanoprintf_common_flags -pedantic -Wall -Wextra -Wundef -Werror)
  set(nanoprintf_link_flags "")

  if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
    list(APPEND nanoprintf_common_flags -Weverything)
    if (NPF_CLANG_ASAN)
      list(APPEND nanoprintf_common_flags -fsanitize=address)
      list(APPEND nanoprintf_link_flags -fsanitize=address)
    elseif (NPF_CLANG_UBSAN)
      list(APPEND nanoprintf_common_flags -fsanitize=undefined)
      list(APPEND nanoprintf_link_flags -fsanitize=undefined)
    endif()
    if (CMAKE_HOST_APPLE)
      list(APPEND nanoprintf_common_flags -Wno-poison-system-directories)
    endif()
  else()
    list(APPEND nanoprintf_common_flags
         -Wconversion
         -Wshadow
         -Wfloat-equal
         -Wsign-conversion
         -Wswitch-enum
         -Wswitch-default)
  endif()
endif()

################ Doctest

add_library(libdoctest_main OBJECT tests/doctest_main.cc)
target_compile_options(libdoctest_main PRIVATE ${nanoprintf_common_flags})

function(npf_test name files)
  add_executable(${name} ${files})
    target_compile_definitions(${name} PRIVATE DOCTEST_CONFIG_SUPER_FAST_ASSERTS)
    target_compile_options(${name} PRIVATE ${nanoprintf_common_flags})
    target_link_options(${name} PRIVATE ${nanoprintf_link_flags})
    target_link_libraries(${name} libdoctest_main) # Doctest is slow, only build once.

  # Set up a target that automatically runs + timestamps successful tests.
  set(timestamp "${CMAKE_CURRENT_BINARY_DIR}/${name}.timestamp")
  add_custom_target(run_${name} ALL DEPENDS ${timestamp})
  add_custom_command(OUTPUT ${timestamp}
                     COMMAND ${name} -m && ${CMAKE_COMMAND} -E touch ${timestamp}
                     DEPENDS ${name}
                     COMMENT "Running ${name}")
endfunction()

################ Language compilation tests

function(npf_compilation_c_test target)
  add_library(${target} tests/compilation_c.c)
  target_compile_options(${target} PRIVATE ${nanoprintf_common_flags})
endfunction()

# Test every combination of compatible flags.
foreach(fw 0 1)
  foreach(precision 0 1)
    foreach(large 0 1)
      foreach(float 0 1)
        foreach(binary 0 1)
          foreach(wb 0 1)
            if ((precision EQUAL 0) AND (float EQUAL 1))
              continue()
            endif()

            set(test_name "")
            if (fw EQUAL 1)
              string(APPEND test_name "_fieldwidth")
            endif()
            if (precision EQUAL 1)
              string(APPEND test_name "_precision")
            endif()
            if (large EQUAL 1)
              string(APPEND test_name "_large")
            endif()
            if (float EQUAL 1)
              string(APPEND test_name "_float")
            endif()
            if (binary EQUAL 1)
              string(APPEND test_name "_binary")
            endif()
            if (wb EQUAL 1)
              string(APPEND test_name "_writeback")
            endif()

            # Run a simple compilation test
            set(compilation_test_name "npf_compile${test_name}_c")
            npf_compilation_c_test(${compilation_test_name})
              target_compile_definitions(
                ${compilation_test_name}
                PRIVATE
                NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS=${fw}
                NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS=${precision}
                NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=${large}
                NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS=${float}
                NANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=${binary}
                NANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=${wb})

            # Run conformance tests (c++)
            set(conformance_test_name "npf_conform${test_name}")
            npf_test(${conformance_test_name} tests/conformance.cc)
              target_compile_definitions(
                ${conformance_test_name}
                PRIVATE
                NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS=${fw}
                NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS=${precision}
                NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=${large}
                NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS=${float}
                NANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=${binary}
                NANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=${wb})

            if (NPF_PALAND)
              set(paland_test_name "npf_paland${test_name}")
              npf_test(${paland_test_name} tests/mpaland-conformance/paland.cc)
                target_compile_definitions(
                  ${paland_test_name}
                  PRIVATE
                  NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS=${fw}
                  NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS=${precision}
                  NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=${large}
                  NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS=${float}
                  NANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=${binary}
                  NANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=${wb})
            endif()
          endforeach()
        endforeach()
      endforeach()
    endforeach()
  endforeach()
endforeach()

# Test that nanoprintf compiles when no flags are set.
npf_compilation_c_test(npf_c_default_flags)

################ Static compilation test

add_executable(npf_static tests/static_nanoprintf.c tests/static_main.c)
  target_link_options(npf_static PRIVATE ${nanoprintf_link_flags})

################# Examples

add_executable(use_npf_directly
               examples/use_npf_directly/your_project_nanoprintf.cc
               examples/use_npf_directly/main.cc)
  target_link_options(use_npf_directly PRIVATE ${nanoprintf_link_flags})

add_executable(wrap_npf
               examples/wrap_npf/your_project_printf.h
               examples/wrap_npf/your_project_printf.cc
               examples/wrap_npf/main.cc)
  target_link_options(wrap_npf PRIVATE ${nanoprintf_link_flags})

add_executable(npf_include_multiple tests/include_multiple.c)
  target_compile_options(npf_include_multiple PRIVATE ${nanoprintf_common_flags})
  target_link_options(npf_include_multiple PRIVATE ${nanoprintf_link_flags})

############### Unit tests

set(unit_test_files
    nanoprintf.h
    tests/unit_parse_format_spec.cc
    tests/unit_binary.cc
    tests/unit_bufputc.cc
    tests/unit_ftoa_rev.cc
    tests/unit_ftoa_rev_08.cc
    tests/unit_ftoa_rev_16.cc
    tests/unit_ftoa_rev_32.cc
    tests/unit_ftoa_rev_64.cc
    tests/unit_utoa_rev.cc
    tests/unit_snprintf.cc
    tests/unit_snprintf_safe_empty.cc
    tests/unit_vpprintf.cc)

npf_test(unit_tests_normal_sized_formatters "${unit_test_files}")
  target_compile_definitions(unit_tests_normal_sized_formatters
                             PRIVATE
                             NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=0)
  if (NPF_32BIT)
  target_compile_definitions(unit_tests_normal_sized_formatters
                             PRIVATE
                             NANOPRINTF_32_BIT_TESTS)
  endif()

npf_test(unit_tests_large_sized_formatters "${unit_test_files}")
  target_compile_definitions(unit_tests_large_sized_formatters
                             PRIVATE
                             NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=1)
  if (NPF_32BIT)
  target_compile_definitions(unit_tests_normal_sized_formatters
                             PRIVATE
                             NANOPRINTF_32_BIT_TESTS)
  endif()
