# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Find Unity's managed assemblies and expose C# build targets for
# each assembly.
#
# Optional variable arguments for this module:
#  UNITY_ROOT_DIR: Unity install location.
#  UNITY_DISABLE_MONO_TOOLS_FALLBACK: Disables fallback to tools provided
#   by the system Mono install.
#
# Cache variables set by this module:
#  UNITY_MONO_EXE: Location of the Mono interpreter executable.
#  UNITY_CSHARP_BUILD_EXE: Location of the tool (e.g msbuild or xbuild) that
#    can build .csproj projects.
#  UNITY_ENGINE_DLL: Location of UnityEngine.dll
#  UNITY_ENGINE_*_MODULE_DLL: Location of UnityEngine*Module.dll if it
#   exists otherwise UnityEngine.dll. UnityEngine.dll was split into components
#   in Unity 2019+. "*" is the name of a module in UNITY_ENGINE_MODULES.
#  UNITY_EDITOR_DLL: Location of UnityEditor.dll
#  UNITY_ENGINE_TEST_RUNNER_DLL: Location of UnityEngine.TestRunner.dll
#
# Targets added by this module:
#  unity_engine: UnityEngine.dll assembly.
#  unity_editor: UnityEditor.dll assembly.
#  unity_engine_*_module: UnityEngine.*Module.dll or EngineEngine.dll.
#    "*" is the lower case name of a module in UNITY_ENGINE_MODULES.
#  unity_engine_test_runner: UnityEngine.TestRunner.dll

if(CMAKE_HOST_WIN32)
  set(UNITY_ROOT_PATH "C:/Program Files/Unity/Hub/Editor/*/Editor")
  set(UNITY_RELATIVE_DATA_DIR "Data")
elseif(CMAKE_HOST_APPLE)
  set(UNITY_ROOT_PATH "/Applications/Unity/Hub/Editor/*/Unity.app")
  set(UNITY_RELATIVE_DATA_DIR "Contents")
elseif(CMAKE_HOST_UNIX)  # Linux
  set(UNITY_ROOT_PATH "$ENV{HOME}/Unity/Hub/Editor/*/Editor")
  set(UNITY_RELATIVE_DATA_DIR "Data")
endif()
set(UNITY_RELATIVE_MANAGED_DIR "${UNITY_RELATIVE_DATA_DIR}/Managed")

# Override root directory hint path.
if(UNITY_ROOT_DIR)
  # Expand the home directory on *nix platforms otherwise GLOB_RECURSE does
  # not work below.
  get_filename_component(UNITY_ROOT_DIR_ABSOLUTE "${UNITY_ROOT_DIR}" ABSOLUTE)
  set(UNITY_ROOT_PATH "${UNITY_ROOT_DIR_ABSOLUTE}")
else()
  # Find the most recent version of Unity installed.
  file(GLOB UNITY_DIRS LIST_DIRECTORIES TRUE "${UNITY_ROOT_PATH}")
  # NOTE: COMPARE STRING will order numerical version numbers correctly.
  list(SORT UNITY_DIRS COMPARE STRING ORDER DESCENDING)
  list(GET UNITY_DIRS 0 UNITY_ROOT_PATH)
endif()

# Find UnityEngine.dll to find the first matching Unity directory.
find_file(UNITY_ENGINE_DLL UnityEngine.dll
  PATHS
    "${UNITY_ROOT_PATH}"
  PATH_SUFFIXES
    "${UNITY_RELATIVE_MANAGED_DIR}"
  NO_DEFAULT_PATH
  NO_CMAKE_FIND_ROOT_PATH
)

# If UnityEngine.dll is found, extract the Unity install directory from the
# path so that we're always searching the same directory for other components.
if(UNITY_ENGINE_DLL)
  string(FIND "${UNITY_ENGINE_DLL}" "/${UNITY_RELATIVE_MANAGED_DIR}"
    MANAGED_DIR_OFFSET REVERSE
  )
  string(SUBSTRING "${UNITY_ENGINE_DLL}" 0 "${MANAGED_DIR_OFFSET}"
    UNITY_ROOT_PATH
  )
endif()

# This list was generated by:
# ls ${UNITY_INSTALL}/Managed/UnityEngine/*Module.dll | \
#  sed 's@.*/UnityEngine/UnityEngine\.\(.*\)Module.dll@\1@' | \
#  tr '\n' ' ' | fold -w 60 -s
set(UNITY_ENGINE_MODULES
  AI AR Accessibility AndroidJNI Animation AssetBundle Audio
  Cloth ClusterInput ClusterRenderer Core CrashReporting
  DSPGraph Director GameCenter Grid HotReload IMGUI
  ImageConversion InputLegacy Input JSONSerialize
  Localization ParticleSystem PerformanceReporting Physics2D
  Physics Profiler ScreenCapture SharedInternals SpriteMask
  SpriteShape Streaming Substance Subsystems TLS Terrain
  TerrainPhysics TextCore TextRendering Tilemap UIElements UI
  UNET Umbra UnityAnalytics UnityConnect UnityTestProtocol
  UnityWebRequestAssetBundle UnityWebRequestAudio
  UnityWebRequest UnityWebRequestTexture UnityWebRequestWWW
  VFX VR Vehicles Video Wind XR
)

# Find UnityEngine modules.
if(UNITY_ENGINE_DLL)
  if(NOT UNITY_MANAGED_DIR)
    get_filename_component(UNITY_MANAGED_DIR "${UNITY_ENGINE_DLL}" DIRECTORY)
    set(UNITY_MANAGED_DIR "${UNITY_MANAGED_DIR}" CACHE STRING "" FORCE)
  endif()
  set(UNITY_ENGINE_DIR "${UNITY_MANAGED_DIR}/UnityEngine")
  if(EXISTS "${UNITY_ENGINE_DIR}")
    set(UNITY_ENGINE_DLL "${UNITY_ENGINE_DIR}/UnityEngine.dll"
      CACHE STRING "" FORCE
    )
  endif()
  foreach(MODULE ${UNITY_ENGINE_MODULES})
    set(MODULE_PATH "${UNITY_ENGINE_DIR}/UnityEngine.${MODULE}Module.dll")
    if(NOT EXISTS "${MODULE_PATH}")
      set(MODULE_PATH "${UNITY_ENGINE_DLL}")
    endif()
    set(UNITY_ENGINE_${MODULE}_MODULE_DLL "${MODULE_PATH}")
  endforeach()
endif()

find_file(UNITY_EDITOR_DLL UnityEditor.dll
  PATHS
    "${UNITY_ROOT_PATH}"
  PATH_SUFFIXES
    "${UNITY_RELATIVE_MANAGED_DIR}"
  NO_DEFAULT_PATH
  NO_CMAKE_FIND_ROOT_PATH
)

# Recursively search for the test runner DLL as it's located in a different
# folder across different versions of Unity.
# This isn't great as newer versions of Unity use the Unity Package Manager
# which results in multiple version of the test runner DLL being available so
# just naively select the first one available.
set(UNITY_ENGINE_TEST_RUNNER_DLL_NAME "UnityEngine.TestRunner.dll")
if(NOT UNITY_ENGINE_TEST_RUNNER_DLL)
  message(STATUS "Searching for ${UNITY_ENGINE_TEST_RUNNER_DLL_NAME}..")
  file(GLOB_RECURSE UNITY_ENGINE_TEST_RUNNER_DLLS
    "${UNITY_ROOT_PATH}/*/${UNITY_ENGINE_TEST_RUNNER_DLL_NAME}"
  )
  list(SORT UNITY_ENGINE_TEST_RUNNER_DLLS ORDER DESCENDING)
  if(UNITY_ENGINE_TEST_RUNNER_DLLS)
    list(GET UNITY_ENGINE_TEST_RUNNER_DLLS 0 UNITY_ENGINE_TEST_RUNNER_DLL)
    message(STATUS "Found ${UNITY_ENGINE_TEST_RUNNER_DLL}")
  else()
    set(UNITY_ENGINE_TEST_RUNNER_DLL "UNITY_ENGINE_TEST_RUNNER_DLL-NOTFOUND")
  endif()
  set(UNITY_ENGINE_TEST_RUNNER_DLL "${UNITY_ENGINE_TEST_RUNNER_DLL}"
    CACHE STRING "" FORCE
  )
endif()

include(csharp)
if(EXISTS "${UNITY_ENGINE_DLL}" AND
   EXISTS "${UNITY_EDITOR_DLL}")
  # Expose targets for Unity assemblies.
  CSharpAddExternalDll(unity_engine "${UNITY_ENGINE_DLL}")
  foreach(MODULE ${UNITY_ENGINE_MODULES})
    string(TOLOWER "${MODULE}" MODULE_LOWER)
    CSharpAddExternalDll(unity_engine_${MODULE_LOWER}_module
      "${UNITY_ENGINE_${MODULE}_MODULE_DLL}"
    )
  endforeach()
  CSharpAddExternalDll(unity_editor "${UNITY_EDITOR_DLL}")
else()
  message(FATAL_ERROR "Failed to find UnityEngine.dll or UnityEditor.dll. \
    Please set valid path with -DUNITY_MANAGED_DIR or check that Unity \
    is installed in \"${UNITY_ROOT_PATH}\".")
endif()

if(EXISTS "${UNITY_ENGINE_TEST_RUNNER_DLL}")
  CSharpAddExternalDll(unity_engine_test_runner
    "${UNITY_ENGINE_TEST_RUNNER_DLL}"
  )
else()
  message(WARNING "Failed to find ${UNITY_ENGINE_TEST_RUNNER_DLL_NAME}")
endif()

if(NOT UNITY_MONO_EXE OR
   NOT UNITY_CSHARP_BUILD_EXE)
  set(FIND_TOOL_OPTIONS
    PATHS
      "${UNITY_ROOT_PATH}"
    PATH_SUFFIXES
      "${UNITY_RELATIVE_DATA_DIR}/MonoBleedingEdge/bin"
      "${UNITY_RELATIVE_DATA_DIR}/Mono/bin"
    NO_DEFAULT_PATH
    NO_CMAKE_FIND_ROOT_PATH
  )
  find_program(UNITY_MONO_EXE mono
    ${FIND_TOOL_OPTIONS}
  )
  if(CMAKE_HOST_WIN32)
    set(BUILD_TOOL_EXTENSION ".bat")
  endif()
  find_program(UNITY_CSHARP_BUILD_EXE
    NAMES
      msbuild${BUILD_TOOL_EXTENSION}
      xbuild${BUILD_TOOL_EXTENSION}
    ${FIND_TOOL_OPTIONS}
  )

  # msbuild / xbuild do not work in some versions of Unity.
  if(EXISTS "${UNITY_CSHARP_BUILD_EXE}")
    execute_process(
      COMMAND "${UNITY_CSHARP_BUILD_EXE}" /version
      OUTPUT_VARIABLE VERSION_STRING
      RESULT_VARIABLE RESULT
    )
    if(NOT ${RESULT} EQUAL "0")
      message(STATUS "${UNITY_CSHARP_BUILD_EXE} /version returned ${RESULT}")
      set(UNITY_CSHARP_BUILD_EXE "" CACHE STRING "" FORCE)
    endif()
  endif()

  if(EXISTS "${UNITY_MONO_EXE}" AND
     EXISTS "${UNITY_CSHARP_BUILD_EXE}")
    get_filename_component(UNITY_CSHARP_BUILD_EXE_NAME
        "${UNITY_CSHARP_BUILD_EXE}" NAME
    )

    # On Linux, Unity's mono distribution can fail to execute build tools in a
    # non-clean environment so wrap in a script that clears the environment.
    if(CMAKE_HOST_UNIX AND NOT CMAKE_HOST_APPLE)
      set(UNITY_CSHARP_BUILD_WRAPPER_DIR
        "${CMAKE_CURRENT_BINARY_DIR}/unity_csharp_build_wrapper"
      )
      set(UNITY_CSHARP_BUILD_WRAPPER
        "${UNITY_CSHARP_BUILD_WRAPPER_DIR}/${UNITY_CSHARP_BUILD_EXE_NAME}"
      )
      file(MAKE_DIRECTORY "${UNITY_CSHARP_BUILD_WRAPPER_DIR}")
      # Copy the build script to preserve the executable bit.
      file(COPY "${UNITY_CSHARP_BUILD_EXE}"
        DESTINATION "${UNITY_CSHARP_BUILD_WRAPPER_DIR}"
      )
      file(WRITE "${UNITY_CSHARP_BUILD_WRAPPER}"
        "#!/bin/sh\nenv -i ${UNITY_CSHARP_BUILD_EXE} \$@\n"
      )
      set(UNITY_CSHARP_BUILD_EXE "${UNITY_CSHARP_BUILD_WRAPPER}"
        CACHE STRING "" FORCE
      )
    elseif(CMAKE_HOST_WIN32)
      # Mono that is packaged with unity has a bug in resgen.bat where it
      # incorrectly forwards arguments. Resgen.bat gets passed an argument
      # like 'a,b' and incorrectly forwards it as 'a b'. Copy the mono folder
      # from unity (less than 100 Mb) and then fix the resgen.bat files to
      # forward arguments properly (using '$*' instead of '$1 $2') thus allowing
      # resx files to compile correctly.
      get_filename_component(UNITY_MONO_EXE_NAME "${UNITY_MONO_EXE}" NAME)
      get_filename_component(MONO_BIN_DIR "${UNITY_CSHARP_BUILD_EXE}" DIRECTORY)

      # Read resgen2.bat to determine whether it's broken.
      file(READ "${MONO_BIN_DIR}/resgen2.bat" RESGEN)
      if("${RESGEN}" MATCHES "resgen\\.exe.* +%1 +%2")
        # Copy Mono.
        set(PATCHED_MONO_DIR
          "${CMAKE_CURRENT_BINARY_DIR}/patched_unity_mono/mono"
        )
        file(COPY "${MONO_BIN_DIR}/.."
          DESTINATION "${PATCHED_MONO_DIR}"
        )

        # Patch resgen.
        string(REGEX REPLACE " +%1 +%2[^\\n]" " %*" RESGEN)
        file(WRITE "${PATCHED_MONO_DIR}/bin/resgen.bat" "${RESGEN}")
        file(WRITE "${PATCHED_MONO_DIR}/bin/resgen2.bat" "${RESGEN}")

        # Redirect to the patched distribution.
        set(UNITY_CSHARP_BUILD_EXE
          "${PATCHED_MONO_DIR}/bin/${UNITY_CSHARP_BUILD_EXE_NAME}"
          CACHE STRING "" FORCE
        )
        set(UNITY_MONO_EXE "${PATCHED_MONO_DIR}/bin/${UNITY_MONO_EXE_NAME}"
          CACHE STRING "" FORCE
        )
      endif()
    endif()
  endif()

  # If Mono tools aren't found in Unity try using the vanilla Mono distribution.
  if(NOT UNITY_DISABLE_MONO_TOOLS_FALLBACK AND (
      NOT EXISTS "${UNITY_MONO_EXE}" OR
      NOT EXISTS "${UNITY_CSHARP_BUILD_EXE}"))
    message(STATUS "No working Mono tools not found in ${UNITY_ROOT_PATH} "
      "trying the system installation."
    )
    find_package(Mono REQUIRED)
    set(UNITY_MONO_EXE "${MONO_EXE}" CACHE STRING "" FORCE)
    set(UNITY_CSHARP_BUILD_EXE "${MONO_CSHARP_BUILD_EXE}"
      CACHE STRING "" FORCE
    )
  endif()
endif()

# If Mono tools still aren't found report an error.
if(NOT EXISTS "${UNITY_MONO_EXE}" OR
   NOT EXISTS "${UNITY_CSHARP_BUILD_EXE}")
  message(FATAL_ERROR "Mono tools not found.")
endif()

include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(
  Unity
  DEFAULT_MSG
  UNITY_ENGINE_DLL
  UNITY_EDITOR_DLL
  UNITY_MONO_EXE
  UNITY_CSHARP_BUILD_EXE
  UNITY_ENGINE_TEST_RUNNER_DLL
  UNITY_ENGINE_MODULES
)
mark_as_advanced(
  UNITY_ENGINE_DLL
  UNITY_EDITOR_DLL
  UNITY_MONO_EXE
  UNITY_CSHARP_BUILD_EXE
  UNITY_ENGINE_TEST_RUNNER_DLL
  UNITY_ENGINE_MODULES
)
