cmake_minimum_required(VERSION 3.9)

find_package(Git)

function(yap_escape_key PPATH EPATH)
    string(REPLACE ":" "__x3a" _EPATH ${PPATH})
    string(REPLACE "/" "__x2f" _EPATH ${_EPATH})
    string(REPLACE "@" "__x40" _EPATH ${_EPATH})    
    string(REPLACE "+" "__x2b" _EPATH ${_EPATH})
    string(REPLACE "-" "__x2d" _EPATH ${_EPATH})
    set(${EPATH} ${_EPATH} PARENT_SCOPE)
endfunction()

function(yap_init)
    get_property(YAP_ROOT GLOBAL PROPERTY YAP_ROOT)
    if(NOT YAP_ROOT)
        set_property(GLOBAL PROPERTY YAP_ROOT ${CMAKE_CURRENT_SOURCE_DIR})
        set_property(GLOBAL PROPERTY YAP_CACHEDIR ${CMAKE_CURRENT_SOURCE_DIR}/.yap)
        set_property(GLOBAL PROPERTY YAP_OUTCACHEDIR_REMOTE ${CMAKE_CURRENT_BINARY_DIR}/.yap/remote)
        set_property(GLOBAL PROPERTY YAP_OUTCACHEDIR_LOCAL ${CMAKE_CURRENT_BINARY_DIR}/.yap/local)
    endif()
endfunction()

function(yap_alias SRC_PATH DST_PATH)
    yap_escape_key(${SRC_PATH} key)
    get_property(YAP_ALIAS_${key} GLOBAL PROPERTY YAP_ALIAS_${key})
    if(YAP_ALIAS_${key})
        message(FATAL_ERROR "Alias for ${SRC_PATH} has already been defined")
    endif()
    set_property(GLOBAL PROPERTY YAP_ALIAS_${key} ${DST_PATH})
endfunction()

function(yap_retrieve_git GIT_PATH TAG OUTDIR)
    # OUTDIR is relative to ${YAP_OUTCACHEDIR_REMOTE}
    if(NOT GIT_FOUND)
        message(FATAL_ERROR "Git package has not been found in the system, but is required for dependency: ${GIT_PATH}")
    endif()

    if(${GIT_PATH} MATCHES "^git\\+https?://")
        string(REGEX REPLACE "^git\\+(https?://.*)$" "\\1" URL ${GIT_PATH})
        string(REGEX REPLACE "^git\\+https?://[^/]*/(.*)$" "\\1" SUBDIR ${GIT_PATH})        
    elseif(${PPATH} MATCHES "^git\\+ssh://")
        string(REGEX REPLACE "^git\\+(ssh://.*)$" "\\1" URL ${GIT_PATH})
        string(REGEX REPLACE "^git\\+ssh://[^/]*/(.*)$" "\\1" SUBDIR ${GIT_PATH})        
    else()
        message(FATAL_ERROR "Unsupported Git path: ${GIT_PATH}")
    endif()

    set(SUBDIR ${SUBDIR}/${TAG})

    if(${TAG} STREQUAL _)
        message(WARNING "Git source used without TAG. Using master/HEAD instead. This may cause slow and inconsistent builds")
    endif()

    get_property(YAP_CACHEDIR GLOBAL PROPERTY YAP_CACHEDIR)
    if(IS_DIRECTORY ${YAP_CACHEDIR}/${SUBDIR})
        if(${TAG} STREQUAL _)
            message(STATUS "Checkout: ${URL}")
            execute_process(
                COMMAND ${GIT_EXECUTABLE} pull
                WORKING_DIRECTORY ${YAP_CACHEDIR}/${SUBDIR}
            )    
        endif()
    else()
        file(MAKE_DIRECTORY ${YAP_CACHEDIR}/${SUBDIR})
        if(${TAG} STREQUAL _)
            message(STATUS "Checkout: ${URL}")
            execute_process(
                COMMAND ${GIT_EXECUTABLE} clone -q ${URL} ${YAP_CACHEDIR}/${SUBDIR}
            )
        else()
            message(STATUS "Checkout: ${URL}")
            execute_process(
                COMMAND ${GIT_EXECUTABLE} clone -c advice.detachedHead=false --branch ${TAG} --depth 1 ${URL} ${YAP_CACHEDIR}/${SUBDIR}
            )
        endif()
    endif()

    set(${OUTDIR} ${SUBDIR} PARENT_SCOPE)
endfunction()

function(yap_retrieve PPATH)
    get_property(YAP_ROOT GLOBAL PROPERTY YAP_ROOT)
    get_property(YAP_CACHEDIR GLOBAL PROPERTY YAP_CACHEDIR)
    get_property(YAP_OUTCACHEDIR_REMOTE GLOBAL PROPERTY YAP_OUTCACHEDIR_REMOTE)
    get_property(YAP_OUTCACHEDIR_LOCAL GLOBAL PROPERTY YAP_OUTCACHEDIR_LOCAL)

    if(NOT YAP_ROOT)
        message(FATAL_ERROR "YAP not initialized. Call yap_init() before retrieving packages")
    endif()

    if("${YAP_ROOT}" STREQUAL "${CMAKE_CURRENT_SOURCE_DIR}")
        # Only execute in the root project
        set(oneValueArgs VERSION GIT_TAG)
        CMAKE_PARSE_ARGUMENTS(YAP_RD "" "${oneValueArgs}" "" ${ARGN})

        yap_escape_key( ${PPATH} key)
        get_property(YAP_${key}_DIR GLOBAL PROPERTY YAP_${key}_DIR)
        get_property(YAP_${key}_VER GLOBAL PROPERTY YAP_${key}_VER)
        if(YAP_${key}_DIR OR YAP_${key}_VER)
            message(FATAL_ERROR "The package ${PPATH} has already been imported")
        endif()

        if(NOT YAP_RD_VERSION)
            set(YAP_RD_VERSION _)
        endif()        

        if(${PPATH} MATCHES "^git\\+https?://.*$")
            # github.com/...
            if(NOT YAP_RD_GIT_TAG)
                set(YAP_RD_GIT_TAG ${YAP_RD_VERSION})
            endif()
            yap_retrieve_git(${PPATH} ${YAP_RD_GIT_TAG} dir)
            set_property(GLOBAL PROPERTY "YAP_${key}_DIR" ${YAP_CACHEDIR}/${dir})
            set_property(GLOBAL PROPERTY "YAP_${key}_OUTDIR" ${YAP_OUTCACHEDIR_REMOTE}/${dir})
        elseif(${PPATH} MATCHES "^git\\+ssh://.*$")
            if(NOT YAP_RD_GIT_TAG)
                set(YAP_RD_GIT_TAG ${YAP_RD_VERSION})
            endif()
            yap_retrieve_git(${PPATH} ${YAP_RD_GIT_TAG} dir)
            set_property(GLOBAL PROPERTY "YAP_${key}_DIR" ${YAP_CACHEDIR}/${dir})
            set_property(GLOBAL PROPERTY "YAP_${key}_OUTDIR" ${YAP_OUTCACHEDIR_REMOTE}/${dir})
        else()
            # local path
            set_property(GLOBAL PROPERTY "YAP_${key}_DIR" ${CMAKE_CURRENT_SOURCE_DIR}/${PPATH})
            set_property(GLOBAL PROPERTY "YAP_${key}_OUTDIR" ${YAP_OUTCACHEDIR_LOCAL}/${PPATH})
        endif()
        set_property(GLOBAL PROPERTY "YAP_${key}_VER" ${YAP_RD_VERSION})        
    endif()
endfunction()

function(yap_next_digit DIGIT RESULT)
    if(${DIGIT} EQUAL 0)
        set(${RESULT} 1 PARENT_SCOPE)
    elseif(${DIGIT} EQUAL 1)
        set(${RESULT} 2 PARENT_SCOPE)
    elseif(${DIGIT} EQUAL 2)
        set(${RESULT} 3 PARENT_SCOPE)
    elseif(${DIGIT} EQUAL 3)
        set(${RESULT} 4 PARENT_SCOPE)
    elseif(${DIGIT} EQUAL 4)
        set(${RESULT} 5 PARENT_SCOPE)
    elseif(${DIGIT} EQUAL 5)
        set(${RESULT} 6 PARENT_SCOPE)
    elseif(${DIGIT} EQUAL 6)
        set(${RESULT} 7 PARENT_SCOPE)
    elseif(${DIGIT} EQUAL 7)
        set(${RESULT} 8 PARENT_SCOPE)
    elseif(${DIGIT} EQUAL 8)
        set(${RESULT} 9 PARENT_SCOPE)
    elseif(${DIGIT} EQUAL 9)
        set(${RESULT} 0 PARENT_SCOPE)
    else()
        set(${RESULT} 0 PARENT_SCOPE)
    endif()
endfunction()

function(yap_version_match SEMVER_REQ RESULT)
    unset(SEMVER_RX)
    if(${SEMVER_REQ} MATCHES "^0|[1-9][0-9]*$")
        set(SEMVER_RX "${SEMVER_REQ}")
    elseif(${SEMVER_REQ} MATCHES "^(0|[1-9][0-9]*)\\+$")
        string(REGEX REPLACE "^(0|[1-9][0-9]*)\\+$" "\\1" MAJ_REQ ${SEMVER_REQ})
        string(REGEX MATCHALL "[0-9]" DIGITS ${MAJ_REQ})

        set(SEMVER_RX "[1-9]")
        foreach(DIGIT ${DIGITS})
            set(SEMVER_RX "${SEMVER_RX}[0-9]")
        endforeach()
        set(SEMVER_RX "${SEMVER_RX}[0-9]*")

        list(REVERSE DIGITS)
        list(LENGTH DIGITS NUM_DIG)            
        math(EXPR PREFIX_LEN "${NUM_DIG}-1")
        string(SUBSTRING ${MAJ_REQ} 0 ${PREFIX_LEN} DIGITS_PREFIX)
        list(GET DIGITS 0 DIGIT)
        set(SEMVER_RX "${SEMVER_RX}|${DIGITS_PREFIX}[${DIGIT}-9]")
        list(REMOVE_AT DIGITS 0)
        list(LENGTH DIGITS NUM_DIG)
        set(REM "[0-9]")            

        while(${NUM_DIG} GREATER 0)
            math(EXPR PREFIX_LEN "${NUM_DIG}-1")
            string(SUBSTRING ${MAJ_REQ} 0 ${PREFIX_LEN} DIGITS_PREFIX)
            list(GET DIGITS 0 DIGIT)
            yap_next_digit(${DIGIT} DIGIT)
            if(${DIGIT} GREATER 0)
                set(SEMVER_RX "${SEMVER_RX}|${DIGITS_PREFIX}[${DIGIT}-9]${REM}")
            endif()
            list(REMOVE_AT DIGITS 0)
            list(LENGTH DIGITS NUM_DIG)            
            set(REM "${REM}[0-9]")
        endwhile()        
    else()
        message(FATAL_ERROR "Invalid integer version requirement spec: ${SEMVER_REQ}. Expected: (0|[1-9][0-9]*)(\\+)?")
    endif()

    set(${RESULT} ${SEMVER_RX} PARENT_SCOPE)
endfunction()

function(yap_parse_semver_requirements SEMVER_REQTS RESULT)
# semver_reqts ::= semver_req (',' semver_req) *
# semver_req ::= maj_req ['+'] | maj_req '.' min_req ['+'] | maj_req '.' min_req '.' patch_req [ '-' ext_req ]
# maj_req ::= ['1'..'9']['0'..'9']*
# min_req ::= ['1'..'9']['0'..'9']*
# patch_req ::= ['1'..'9']['0'..'9']*
# ext_req ::= .+
    string(REGEX MATCHALL "[^,]+" SEMVER_REQLIST "${SEMVER_REQTS}")
    unset(SEMVER_RX)
    foreach(SEMVER_REQ ${SEMVER_REQLIST})
        if(${SEMVER_REQ} MATCHES "^(0|[1-9][0-9]*)(\\+)?$")
            yap_version_match(${SEMVER_REQ} SEMVER_SUB_RX)
            set(SEMVER_SUB_RX "^(${SEMVER_SUB_RX})(\\..+)?$")
            if(NOT SEMVER_RX)
                set(SEMVER_RX "${SEMVER_SUB_RX}")
            else()
                set(SEMVER_RX "${SEMVER_RX}|${SEMVER_SUB_RX}")
            endif()
        elseif(${SEMVER_REQ} MATCHES "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(\\+)?$")
            string(REGEX REPLACE "^(0|[1-9][0-9]*)\\.((0|[1-9][0-9]*)(\\+)?)$" "\\1" MAJ_REQ ${SEMVER_REQ})
            string(REGEX REPLACE "^(0|[1-9][0-9]*)\\.((0|[1-9][0-9]*)(\\+)?)$" "\\2" MIN_REQ ${SEMVER_REQ})
            yap_version_match(${MIN_REQ} SEMVER_SUB_RX)
            set(SEMVER_SUB_RX "^${MAJ_REQ}\\.(${SEMVER_SUB_RX})(\\..+)?$")
            if(NOT SEMVER_RX)
                set(SEMVER_RX "${SEMVER_SUB_RX}")
            else()
                set(SEMVER_RX "${SEMVER_RX}|${SEMVER_SUB_RX}")
            endif()
        elseif(${SEMVER_REQ} MATCHES "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(\\+)?$")
            string(REGEX REPLACE "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.((0|[1-9][0-9]*)(\\+)?)$" "\\1" MAJ_REQ ${SEMVER_REQ})
            string(REGEX REPLACE "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.((0|[1-9][0-9]*)(\\+)?)$" "\\2" MIN_REQ ${SEMVER_REQ})
            string(REGEX REPLACE "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.((0|[1-9][0-9]*)(\\+)?)$" "\\3" PATCH_REQ ${SEMVER_REQ})
            yap_version_match(${PATCH_REQ} SEMVER_SUB_RX)
            set(SEMVER_SUB_RX "^${MAJ_REQ}\\.${MIN_REQ}\\.(${SEMVER_SUB_RX})(-.+)?$")
            if(NOT SEMVER_RX)
                set(SEMVER_RX "${SEMVER_SUB_RX}")
            else()
                set(SEMVER_RX "${SEMVER_RX}|${SEMVER_SUB_RX}")
            endif()            
        elseif(${SEMVER_REQ} MATCHES "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)-[0-9a-zA-Z]+$")
            string(REGEX REPLACE "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)-([0-9a-zA-Z]+)$" "\\1" MAJ_REQ ${SEMVER_REQ})
            string(REGEX REPLACE "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)-([0-9a-zA-Z]+)$" "\\2" MIN_REQ ${SEMVER_REQ})
            string(REGEX REPLACE "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)-([0-9a-zA-Z]+)$" "\\3" PATCH_REQ ${SEMVER_REQ})
            string(REGEX REPLACE "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)-([0-9a-zA-Z]+)$" "\\4" EXT_REQ ${SEMVER_REQ})
            set(SEMVER_SUB_RX "^${MAJ_REQ}\\.${MIN_REQ}\\.${PATCH_REQ}-${EXT_REQ}.*$")
            if(NOT SEMVER_RX)
                set(SEMVER_RX "${SEMVER_SUB_RX}")
            else()
                set(SEMVER_RX "${SEMVER_RX}|${SEMVER_SUB_RX}")
            endif()            
        else()
            message(FATAL_ERROR "Invalid requirement spec: ${SEMVER_REQ}.")
        endif()
    endforeach()    
    set(${RESULT} ${SEMVER_RX} PARENT_SCOPE)
endfunction()

function(yap_require PPATH)
    set(options OPTIONAL EXCLUDE_FROM_ALL)
    set(oneValueArgs "VERSION" "NAME")
    set(multiValueArgs "LIBRARIES" "INCLUDE_DIRECTORIES" "TESTS")
    CMAKE_PARSE_ARGUMENTS(YAP_RQ "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})

    if(YAP_RQ_OPTIONAL)
        if(NOT YAP_RQ_NAME)
            message(FATAL_ERROR "OPTIONAL can only be used when NAME is provided.")
        endif()
    endif()

    yap_escape_key(${PPATH} key)
    get_property(ALIAS GLOBAL PROPERTY YAP_ALIAS_${key})
    if(ALIAS)
        set(PPATH ${ALIAS})
        yap_escape_key(${PPATH} key)
    endif()

    get_property(DIR GLOBAL PROPERTY YAP_${key}_DIR)
    get_property(VER GLOBAL PROPERTY YAP_${key}_VER)
    get_property(OUTDIR GLOBAL PROPERTY YAP_${key}_OUTDIR)

    if(DIR AND VER AND OUTDIR)
        if(YAP_RQ_VERSION)
            yap_parse_semver_requirements(${YAP_RQ_VERSION} RX)
            if(NOT ${VER} MATCHES ${RX})
                if(YAP_RQ_OPTIONAL)
                    set(${NAME}_NOTFOUND TRUE)            
                else()
                    message(SEND_ERROR "Requirement ${PPATH}/${YAP_RQ_VERSION} not satisfied: version mismatch: ${VER}")
                endif()    
                return()
            endif()
        endif()

        set(INC_FILE ${DIR}/yap-config.cmake)
        if(EXISTS ${INC_FILE})
            set(YAP_PACKAGE_VERSION ${VER})
            if(YAP_RQ_NAME)
                set(YAP_PACKAGE_NAME ${YAP_RQ_NAME})
            endif()
            include(${INC_FILE})
            if(YAP_RQ_EXCLUDE_FROM_ALL)
                add_subdirectory(${DIR} ${OUTDIR} EXCLUDE_FROM_ALL)
            else()
                add_subdirectory(${DIR} ${OUTDIR})
            endif()
            unset(YAP_PACKAGE_VERSION)
            unset(YAP_PACKAGE_NAME)
        else()
            if(YAP_RQ_NAME)
                if(YAP_RQ_LIBRARIES)
                    set(${YAP_RQ_NAME}_LIBRARIES ${YAP_RQ_LIBRARIES} PARENT_SCOPE)
                endif()
                if(YAP_RQ_TESTS)
                    set(${YAP_RQ_NAME}_TESTS ${YAP_RQ_TESTS} PARENT_SCOPE)
                endif()
                
                if(YAP_RQ_INCLUDE_DIRECTORIES)
                    set(RES_INCDIRS "")
                    foreach(D ${YAP_RQ_INCLUDE_DIRECTORIES})
                        list(APPEND RES_INCDIRS ${DIR}/${D})
                    endforeach()
                    set(${YAP_RQ_NAME}_INCLUDE_DIRECTORIES ${RES_INCDIRS} PARENT_SCOPE)
                endif()
                set(YAP_PACKAGE_VERSION ${VER})
                if(YAP_RQ_NAME)
                    set(YAP_PACKAGE_NAME ${YAP_RQ_NAME})
                endif()
                if(EXISTS ${DIR}/CMakeLists.txt)                
                    if(YAP_RQ_EXCLUDE_FROM_ALL)
                        add_subdirectory(${DIR} ${OUTDIR} EXCLUDE_FROM_ALL)
                    else()
                        add_subdirectory(${DIR} ${OUTDIR})
                    endif()
                endif()
                unset(YAP_PACKAGE_VERSION)
                unset(YAP_PACKAGE_NAME)
                set(${YAP_RQ_NAME}_SOURCE ${DIR} PARENT_SCOPE)
            else()
                message(FATAL_ERROR "Since ${DIR}/yap-config.cmake not provided, NAME parameter has been expected but not found")
            endif()
        endif()
    else()
        if(YAP_RQ_OPTIONAL)
            set(${NAME}_NOTFOUND TRUE PARENT_SCOPE)            
        else()
            message(SEND_ERROR "Requirement ${PPATH} not satisfied: the package has not been retrieved")
        endif()
    endif()
endfunction()