# Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
# file Copyright.txt or https://cmake.org/licensing for details.

cmake_policy(PUSH)
cmake_policy(SET CMP0057 NEW) # if IN_LIST
cmake_policy(SET CMP0054 NEW)

# Function to print messages of this module
function(_ios_install_combined_message)
  message(STATUS "[iOS combined] " ${ARGN})
endfunction()

# Get build settings for the current target/config/SDK by running
# `xcodebuild -sdk ... -showBuildSettings` and parsing it's output
function(_ios_install_combined_get_build_setting sdk variable resultvar)
  if("${sdk}" STREQUAL "")
    message(FATAL_ERROR "`sdk` is empty")
  endif()

  if("${variable}" STREQUAL "")
    message(FATAL_ERROR "`variable` is empty")
  endif()

  if("${resultvar}" STREQUAL "")
    message(FATAL_ERROR "`resultvar` is empty")
  endif()

  set(
      cmd
      xcodebuild -showBuildSettings
      -sdk "${sdk}"
      -target "${CURRENT_TARGET}"
      -config "${CURRENT_CONFIG}"
  )

  execute_process(
      COMMAND ${cmd}
      WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
      RESULT_VARIABLE result
      OUTPUT_VARIABLE output
  )

  if(NOT result EQUAL 0)
    message(FATAL_ERROR "Command failed (${result}): ${cmd}")
  endif()

  if(NOT output MATCHES " ${variable} = ([^\n]*)")
    if("${variable}" STREQUAL "VALID_ARCHS")
      # VALID_ARCHS may be unset by user for given SDK
      # (e.g. for build without simulator).
      set("${resultvar}" "" PARENT_SCOPE)
      return()
    else()
      message(FATAL_ERROR "${variable} not found.")
    endif()
  endif()

  set("${resultvar}" "${CMAKE_MATCH_1}" PARENT_SCOPE)
endfunction()

# Get architectures of given SDK (iphonesimulator/iphoneos)
function(_ios_install_combined_get_valid_archs sdk resultvar)
  cmake_policy(PUSH)
  cmake_policy(SET CMP0007 NEW)

  if("${resultvar}" STREQUAL "")
    message(FATAL_ERROR "`resultvar` is empty")
  endif()

  _ios_install_combined_get_build_setting("${sdk}" "VALID_ARCHS" valid_archs)

  separate_arguments(valid_archs)
  list(REMOVE_ITEM valid_archs "") # remove empty elements
  list(REMOVE_DUPLICATES valid_archs)

  string(REPLACE ";" " " printable "${valid_archs}")
  _ios_install_combined_message("Architectures (${sdk}): ${printable}")

  set("${resultvar}" "${valid_archs}" PARENT_SCOPE)

  cmake_policy(POP)
endfunction()

# Make both arch lists a disjoint set by preferring the current SDK
# (starting with Xcode 12 arm64 is available as device and simulator arch on iOS)
function(_ios_install_combined_prune_common_archs corr_sdk corr_archs_var this_archs_var)
  list(REMOVE_ITEM ${corr_archs_var} ${${this_archs_var}})

  string(REPLACE ";" " " printable "${${corr_archs_var}}")
  _ios_install_combined_message("Architectures (${corr_sdk}) after pruning: ${printable}")

  set("${corr_archs_var}" "${${corr_archs_var}}" PARENT_SCOPE)
endfunction()

# Final target can contain more architectures that specified by SDK. This
# function will run 'lipo -info' and parse output. Result will be returned
# as a CMake list.
function(_ios_install_combined_get_real_archs filename resultvar)
  set(cmd "${_lipo_path}" -info "${filename}")
  execute_process(
      COMMAND ${cmd}
      RESULT_VARIABLE result
      OUTPUT_VARIABLE output
      ERROR_VARIABLE output
      OUTPUT_STRIP_TRAILING_WHITESPACE
      ERROR_STRIP_TRAILING_WHITESPACE
  )
  if(NOT result EQUAL 0)
    message(
        FATAL_ERROR "Command failed (${result}): ${cmd}\n\nOutput:\n${output}"
    )
  endif()

  if(NOT output MATCHES "(Architectures in the fat file: [^\n]+ are|Non-fat file: [^\n]+ is architecture): ([^\n]*)")
    message(FATAL_ERROR "Could not detect architecture from: ${output}")
  endif()

  separate_arguments(CMAKE_MATCH_2)
  set(${resultvar} ${CMAKE_MATCH_2} PARENT_SCOPE)
endfunction()

# Run build command for the given SDK
function(_ios_install_combined_build sdk)
  if("${sdk}" STREQUAL "")
    message(FATAL_ERROR "`sdk` is empty")
  endif()

  _ios_install_combined_message("Build `${CURRENT_TARGET}` for `${sdk}`")

  execute_process(
      COMMAND
      "${CMAKE_COMMAND}"
      --build
      .
      --target "${CURRENT_TARGET}"
      --config ${CURRENT_CONFIG}
      --
      -sdk "${sdk}"
      WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
      RESULT_VARIABLE result
  )

  if(NOT result EQUAL 0)
    message(FATAL_ERROR "Build failed")
  endif()
endfunction()

# Remove given architecture from file. This step needed only in rare cases
# when target was built in "unusual" way. Emit warning message.
function(_ios_install_combined_remove_arch lib arch)
  _ios_install_combined_message(
    "Warning! Unexpected architecture `${arch}` detected and will be removed "
    "from file `${lib}`")
  set(cmd "${_lipo_path}" -remove ${arch} -output ${lib} ${lib})
  execute_process(
      COMMAND ${cmd}
      RESULT_VARIABLE result
      OUTPUT_VARIABLE output
      ERROR_VARIABLE output
      OUTPUT_STRIP_TRAILING_WHITESPACE
      ERROR_STRIP_TRAILING_WHITESPACE
  )
  if(NOT result EQUAL 0)
    message(
        FATAL_ERROR "Command failed (${result}): ${cmd}\n\nOutput:\n${output}"
    )
  endif()
endfunction()

# Check that 'lib' contains only 'archs' architectures (remove others).
function(_ios_install_combined_keep_archs lib archs)
  _ios_install_combined_get_real_archs("${lib}" real_archs)
  set(archs_to_remove ${real_archs})
  list(REMOVE_ITEM archs_to_remove ${archs})
  foreach(x ${archs_to_remove})
    _ios_install_combined_remove_arch("${lib}" "${x}")
  endforeach()
endfunction()

function(_ios_install_combined_detect_associated_sdk corr_sdk_var)
  if("${PLATFORM_NAME}" STREQUAL "")
    message(FATAL_ERROR "PLATFORM_NAME should not be empty")
  endif()

  set(all_platforms "$ENV{SUPPORTED_PLATFORMS}")
  if("${SUPPORTED_PLATFORMS}" STREQUAL "")
    _ios_install_combined_get_build_setting(
      ${PLATFORM_NAME} SUPPORTED_PLATFORMS all_platforms)
    if("${all_platforms}" STREQUAL "")
      message(FATAL_ERROR
        "SUPPORTED_PLATFORMS not set as an environment variable nor "
        "able to be determined from project")
    endif()
  endif()

  separate_arguments(all_platforms)
  if(NOT PLATFORM_NAME IN_LIST all_platforms)
    message(FATAL_ERROR "`${PLATFORM_NAME}` not found in `${all_platforms}`")
  endif()

  list(REMOVE_ITEM all_platforms "" "${PLATFORM_NAME}")
  list(LENGTH all_platforms all_platforms_length)
  if(NOT all_platforms_length EQUAL 1)
    message(FATAL_ERROR "Expected one element: ${all_platforms}")
  endif()

  set(${corr_sdk_var} "${all_platforms}" PARENT_SCOPE)
endfunction()

# Create combined binary for the given target.
#
# Preconditions:
#  * Target already installed at ${destination}
#    for the ${PLATFORM_NAME} platform
#
# This function will:
#  * Run build for the lacking platform, i.e. opposite to the ${PLATFORM_NAME}
#  * Fuse both libraries by running lipo
function(ios_install_combined target destination)
  if("${target}" STREQUAL "")
    message(FATAL_ERROR "`target` is empty")
  endif()

  if("${destination}" STREQUAL "")
    message(FATAL_ERROR "`destination` is empty")
  endif()

  if(NOT IS_ABSOLUTE "${destination}")
    message(FATAL_ERROR "`destination` is not absolute: ${destination}")
  endif()

  if(IS_DIRECTORY "${destination}" OR IS_SYMLINK "${destination}")
    message(FATAL_ERROR "`destination` is no regular file: ${destination}")
  endif()

  if("${CMAKE_BINARY_DIR}" STREQUAL "")
    message(FATAL_ERROR "`CMAKE_BINARY_DIR` is empty")
  endif()

  if(NOT IS_DIRECTORY "${CMAKE_BINARY_DIR}")
    message(FATAL_ERROR "Is not a directory: ${CMAKE_BINARY_DIR}")
  endif()

  if("${CMAKE_INSTALL_CONFIG_NAME}" STREQUAL "")
    message(FATAL_ERROR "CMAKE_INSTALL_CONFIG_NAME is empty")
  endif()

  set(cmd xcrun -f lipo)

  # Do not merge OUTPUT_VARIABLE and ERROR_VARIABLE since latter may contain
  # some diagnostic information even for the successful run.
  execute_process(
      COMMAND ${cmd}
      RESULT_VARIABLE result
      OUTPUT_VARIABLE output
      ERROR_VARIABLE error_output
      OUTPUT_STRIP_TRAILING_WHITESPACE
      ERROR_STRIP_TRAILING_WHITESPACE
  )
  if(NOT result EQUAL 0)
    message(
        FATAL_ERROR "Command failed (${result}): ${cmd}\n\nOutput:\n${output}\nOutput(error):\n${error_output}"
    )
  endif()
  set(_lipo_path ${output})
  list(LENGTH _lipo_path len)
  if(NOT len EQUAL 1)
    message(FATAL_ERROR "Unexpected xcrun output: ${_lipo_path}")
  endif()
  if(NOT EXISTS "${_lipo_path}")
    message(FATAL_ERROR "File not found: ${_lipo_path}")
  endif()

  set(CURRENT_CONFIG "${CMAKE_INSTALL_CONFIG_NAME}")
  set(CURRENT_TARGET "${target}")

  _ios_install_combined_message("Target: ${CURRENT_TARGET}")
  _ios_install_combined_message("Config: ${CURRENT_CONFIG}")
  _ios_install_combined_message("Destination: ${destination}")

  # Get SDKs
  _ios_install_combined_detect_associated_sdk(corr_sdk)

  # Get architectures of the target
  _ios_install_combined_get_valid_archs("${PLATFORM_NAME}" this_valid_archs)
  _ios_install_combined_get_valid_archs("${corr_sdk}" corr_valid_archs)
  _ios_install_combined_prune_common_archs("${corr_sdk}" corr_valid_archs this_valid_archs)

  # Return if there are no valid architectures for the SDK.
  # (note that library already installed)
  if("${corr_valid_archs}" STREQUAL "")
    _ios_install_combined_message(
        "No architectures detected for `${corr_sdk}` (skip)"
    )
    return()
  endif()

  # Trigger build of corresponding target
  _ios_install_combined_build("${corr_sdk}")

  # Get location of the library in build directory
  _ios_install_combined_get_build_setting(
    "${corr_sdk}" "CONFIGURATION_BUILD_DIR" corr_build_dir)
  _ios_install_combined_get_build_setting(
    "${corr_sdk}" "EXECUTABLE_PATH" corr_executable_path)
  set(corr "${corr_build_dir}/${corr_executable_path}")

  _ios_install_combined_keep_archs("${corr}" "${corr_valid_archs}")
  _ios_install_combined_keep_archs("${destination}" "${this_valid_archs}")

  _ios_install_combined_message("Current: ${destination}")
  _ios_install_combined_message("Corresponding: ${corr}")

  set(cmd "${_lipo_path}" -create ${corr} ${destination} -output ${destination})

  execute_process(
      COMMAND ${cmd}
      WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
      RESULT_VARIABLE result
  )

  if(NOT result EQUAL 0)
    message(FATAL_ERROR "Command failed: ${cmd}")
  endif()

  _ios_install_combined_message("Install done: ${destination}")
endfunction()

cmake_policy(POP)
