# Functions and macros to generate upgrade and downgrade scripts

# concatenate_files(<output> <file> ...)
#
# Concatenate a list of files into <output>
function(concatenate_files OUTPUT_FILE FIRST_FILE)
  file(READ ${FIRST_FILE} _contents)
  file(WRITE ${OUTPUT_FILE} "${_contents}")
  foreach(_file ${ARGN})
    file(READ ${_file} _contents)
    file(APPEND ${OUTPUT_FILE} "${_contents}")
  endforeach()
endfunction()

# generate_script(...)
#
# Generate a script file and install it into the script directory.
#
# VERSION <version>
#
# Version to use for shared libraryx.
#
# SCRIPT <file>
#
# File name for script file.
#
# OUTPUT_DIRECTORY <dir>
#
# Directory for where script file should be written.  Defaults to
# CMAKE_CURRENT_BINARY_DIR.
#
# FILES <file> ...
#
# List of files to include, in order, in the script file.
function(generate_script)
  set(options)
  set(oneValueArgs VERSION SCRIPT OUTPUT_DIRECTORY)
  set(multiValueArgs FILES)
  cmake_parse_arguments(_generate "${options}" "${oneValueArgs}"
                        "${multiValueArgs}" ${ARGN})

  if(NOT _generate_OUTPUT_DIRECTORY)
    set(_generate_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
  endif()

  # Set the necessary variables, generate the real files, and append the output
  # to the final update script. Ideally, the template files should have a '.in'
  # suffix so that we can keep the template file and the final file available
  # for debugging, but for some reason it was decided to not use the '.in'
  # suffix for template files so now we're stuck with it.
  set(MODULE_PATHNAME "$libdir/timescaledb-${_generate_VERSION}")
  set(LOADER_PATHNAME "$libdir/timescaledb")

  # Process all files. They are template files and should end with '.in' but for
  # some reason they do not, so we append '.gen' instead to indicate generated
  # files.
  set(_result_files)
  foreach(_file ${_generate_FILES})
    configure_file(${_file} ${_file}.gen @ONLY)
    list(APPEND _result_files ${_file}.gen)
  endforeach()

  # Concatenate the real files into the update file.
  message(STATUS "Generating script ${_generate_SCRIPT}")
  concatenate_files(${_generate_OUTPUT_DIRECTORY}/${_generate_SCRIPT}
                    ${_result_files})

  install(FILES ${_generate_OUTPUT_DIRECTORY}/${_generate_SCRIPT}
          DESTINATION "${PG_SHAREDIR}/extension")
endfunction()

# generate_downgrade_script(<options>)
#
# Create a downgrade script from a source version to a target version. To figure
# out what files are necessary, the ScriptFiles.cmake manifest is read from the
# target version. If that file does not exist in the target version, the
# manifest in the current version is used, but it is assumed that files are only
# added. This situation only occur in the first version where we start to
# generate downgrade scripts and in this case files were only added, so we can
# safely ignore them.
#
# SOURCE_VERSION <version>
#
# Version to generate downgrade script from.
#
# TARGET_VERSION <version>
#
# Version to generate downgrade script to.
#
# OUTPUT_DIRECTORY <dir>
#
# Output directory for script file. Defaults to CMAKE_CURRENT_BINARY_DIR.
#
# INPUT_DIRECTORY <dir>
#
# Input directory for downgrade files. Defaults to CMAKE_CURRENT_SOURCE_DIR.
#
# FILES <file> ...
#
# Files to include, in order, when generating the downgrade script.
#
# The downgrade script is generated by creating a new file of the format
# "timescaledb--<current>--<version>.sql" consisting of the sequence of files:
#
# 1. Generated prolog from the target version
# 2. Generated downgrade files from the source version
# 3. Generated epilog from the target version
#
# The files provided are assumed to be configure templates and configure_files
# will be run on them with the following variables set:
#
# LOADER_PATHNAME: Pathname to loader shared library. This is the same as in the
# update.
#
# MODULE_PATHNAME: Pathname to timescale extension of the version being
# dowgraded to. This can be used to load "old" functions from the correct
# library.
function(generate_downgrade_script)
  set(options)
  set(oneValueArgs SOURCE_VERSION TARGET_VERSION OUTPUT_DIRECTORY
                   INPUT_DIRECTORY FILES)
  cmake_parse_arguments(_downgrade "${options}" "${oneValueArgs}"
                        "${multiValueArgs}" ${ARGN})

  if(NOT _downgrade_OUTPUT_DIRECTORY)
    set(_downgrade_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
  endif()

  if(NOT _downgrade_INPUT_DIRECTORY)
    set(_downgrade_INPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
  endif()

  foreach(_downgrade_file ${_downgrade_FILES})
    if(NOT EXISTS ${_downgrade_INPUT_DIRECTORY}/${_downgrade_file})
      message(FATAL_ERROR "No downgrade file ${_downgrade_file} found!")
    endif()
  endforeach()

  # Fetch manifest with list of files for the prolog and epilog from the target
  # version, if we are in a version that supports downgrades.  Otherwise, take
  # the one in the current version.
  #
  # We have a specific exception where we allow a missing manifest for the first
  # version that supports downgrades and assume that the files to include are
  # the same in the target version as the current one.
  if(_downgrade_TARGET_VERSION VERSION_GREATER 2.3)
    git_versioned_get(VERSION ${_downgrade_TARGET_VERSION} FILES
                      ${CMAKE_SOURCE_DIR}/cmake/ScriptFiles.cmake)
  else()
    file(MAKE_DIRECTORY
         "${CMAKE_BINARY_DIR}/v${_downgrade_TARGET_VERSION}/cmake")
    file(COPY "${CMAKE_SOURCE_DIR}/cmake/ScriptFiles.cmake"
         DESTINATION "${CMAKE_BINARY_DIR}/v${_downgrade_TARGET_VERSION}/cmake")
  endif()

  # This will include the variables in this scope, but not in the parent scope
  # so we can read them locally without affecting the parent scope.
  include(
    ${CMAKE_BINARY_DIR}/v${_downgrade_TARGET_VERSION}/cmake/ScriptFiles.cmake)

  set(_downgrade_PRE_FILES ${PRE_DOWNGRADE_FILES})
  set(_downgrade_POST_FILES "${PRE_INSTALL_FUNCTION_FILES};${SOURCE_FILES}" ${SET_POST_UPDATE_STAGE}
                            ${POST_UPDATE_FILES} ${UNSET_UPDATE_STAGE})

  # Fetch epilog from target version.
  git_versioned_get(
    VERSION
    ${_downgrade_TARGET_VERSION}
    FILES
    ${_downgrade_POST_FILES}
    RESULT_FILES
    _epilog_files
    IGNORE_ERRORS)

  foreach(_downgrade_file ${_downgrade_PRE_FILES})
    get_filename_component(_downgrade_filename ${_downgrade_file} NAME)
    configure_file(${_downgrade_file} ${_downgrade_INPUT_DIRECTORY}/${_downgrade_filename} COPYONLY)
    list(APPEND _files ${_downgrade_INPUT_DIRECTORY}/${_downgrade_filename})
  endforeach()
  foreach(_downgrade_file ${_downgrade_FILES})
    list(APPEND _files ${_downgrade_INPUT_DIRECTORY}/${_downgrade_file})
  endforeach()
  list(APPEND _files ${_epilog_files})

  # Save the current PROJECT_VERSION_MOD
  set(SAVED_PROJECT_VERSION_MOD ${PROJECT_VERSION_MOD})
  # To use PROJECT_VERSION_MOD variable as a target version in downgrade scripts
  # we should set it as the DOWNGRADE_TO_VERSION because it means the target version
  # when executing the downgrade scripts
  set(PROJECT_VERSION_MOD ${DOWNGRADE_TO_VERSION})
  generate_script(
    VERSION
    ${_downgrade_TARGET_VERSION}
    SCRIPT
    timescaledb--${_downgrade_SOURCE_VERSION}--${_downgrade_TARGET_VERSION}.sql
    OUTPUT_DIRECTORY
    ${_downgrade_OUTPUT_DIRECTORY}
    FILES
    ${_files})
  # Restore the original PROJECT_VERSION_MOD
  set(PROJECT_VERSION_MOD ${SAVED_PROJECT_VERSION_MOD})
endfunction()