# SPDX-FileCopyrightText: 2016 Gleb Popov <6yearold@gmail.com>
#
# SPDX-License-Identifier: BSD-3-Clause

#[=======================================================================[.rst:
ECMWinResolveSymlinks
--------------------------

Resolve pseudo-symlinks created by git when cloning on Windows.

::

  ecm_win_resolve_symlinks(<dir>)

When git checks out a repository with UNIX symlinks on Windows machine,
it creates a text file for each symlink, containing a relative path to the
real file.
This function would recursively walk over specified directory and replace
pseudo-symlinks with corresponding real file's contents. It would then run
git update-index --assume-unchanged on them to trick git.

This is useful for projects like "breeze-icons" that contain many identical
icons implemented as symlinks.

Since 5.28
#]=======================================================================]

function(ECM_WIN_RESOLVE_SYMLINKS _dir)
  get_filename_component(dir ${_dir} ABSOLUTE)
  find_program(GIT_EXECUTABLE git)
  if(NOT GIT_EXECUTABLE)
    message(SEND_ERROR "Git executable not found!")
  endif()

  message(STATUS "Resolving symlinks in ${dir}...")
  _find_symlinks(${dir} symlinks)
  _portioned_list(symlinks ${symlinks})
  foreach(s IN LISTS symlinks)
    string(REPLACE ":" ";" s ${s})
    _assume_unchanged(NO ${dir} "${s}")
    _checkout_symlinks(${dir} "${s}")
    _resolve_symlinks(${dir} "${s}")
    _assume_unchanged(YES ${dir} "${s}")
  endforeach()
  message(STATUS "Resolving symlinks in ${dir}... Done.")

  # touch cache every build to force CMake to re-run these functions every time
  if(NOT TARGET wrs_touch_cache)
    add_custom_target(wrs_touch_cache ALL
      COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_BINARY_DIR}/CMakeCache.txt
    )
  endif()
endfunction()

function(_assume_unchanged mode dir symlinks)
  if(mode)
    set(flag --assume-unchanged)
  else()
    set(flag --no-assume-unchanged)
  endif()

  execute_process(COMMAND ${GIT_EXECUTABLE} update-index ${flag} ${symlinks}
    WORKING_DIRECTORY ${dir}
  )
endfunction()

function(_find_symlinks dir outvar)
  execute_process(COMMAND ${GIT_EXECUTABLE} ls-files -s
    WORKING_DIRECTORY ${dir}
    OUTPUT_VARIABLE out
    OUTPUT_STRIP_TRAILING_WHITESPACE
  )

  set(${outvar})
  if(out)
    string(REPLACE "\n" ";" out ${out})

    foreach(f IN LISTS out)
      # 120000 0db97052931e18484b6705f3bc644484ef9403b0 0 <tab> icons-dark/actions/16/CVnamespace.svg
      string(REPLACE "\t" ";" f "${f}")
      string(REPLACE " " ";" f "${f}")
      list(GET f 0 mode)
      if(mode STREQUAL "120000")
        list(GET f 3 symlink)
        list(APPEND ${outvar} ${symlink})
      endif()
    endforeach()
  endif()
  set(${outvar} ${${outvar}} PARENT_SCOPE)
endfunction()

# In functions like _checkout_symlinks() the command line can become too lengthy for Windows.
# So we partition it, but in a hacky way due to CMake doesn't have list of lists.
function(_portioned_list outvar)
  list(LENGTH ARGN arglen)

  if(arglen EQUAL 0)
    set(${outvar} "" PARENT_SCOPE)
    return()
  endif()

  set(init)
  set(tail)
  math(EXPR range "${arglen} - 1")
  foreach(i RANGE ${range})
    list(GET ARGN ${i} v)
    string(LENGTH "${init}" initlen)
    string(LENGTH ${v} vlen)
    math(EXPR sumlen "${initlen} + ${vlen}")
    if(sumlen LESS 8192)
      list(APPEND init ${v})
    else()
      list(APPEND tail ${v})
    endif()
  endforeach()

  _portioned_list(tail_portioned ${tail})
  string(REPLACE ";" ":" init "${init}") # Generally this is not safe, because filepath can contain ':' character. But not on Windows. Phew.
  set(${outvar} ${init} ${tail_portioned} PARENT_SCOPE)
endfunction()

function(_checkout_symlinks dir symlinks)
  execute_process(COMMAND ${GIT_EXECUTABLE} checkout ${symlinks}
    WORKING_DIRECTORY ${dir}
  )
endfunction()

function(_resolve_symlinks dir symlinks)
  foreach(s IN LISTS symlinks)
    file(READ ${dir}/${s} sdata)
    get_filename_component(sdir ${dir}/${s} DIRECTORY)
    set(f "${sdir}/${sdata}")
    file(READ ${f} fdata)
    file(WRITE ${dir}/${s} ${fdata})
  endforeach()
endfunction()
