#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you 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
#
#     http://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.
#

# If cmake variable HDFSPP_LIBRARY_ONLY is set, then tests, examples, and
# tools will not be built. This allows for faster builds of the libhdfspp
# library alone, avoids looking for a JDK, valgrind, and gmock, and
# prevents the generation of multiple binaries that might not be relevant
# to other projects during normal use.
# Example of cmake invocation with HDFSPP_LIBRARY_ONLY enabled:
# cmake -DHDFSPP_LIBRARY_ONLY=1

project (libhdfspp)

cmake_minimum_required(VERSION 2.8)

find_package (Boost 1.72.0 REQUIRED COMPONENTS date_time)

enable_testing()
set(CMAKE_CXX_STANDARD 17)
include (CTest)

SET(BUILD_SHARED_HDFSPP TRUE CACHE STRING "BUILD_SHARED_HDFSPP defaulting to 'TRUE'")
SET(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMake" ${CMAKE_MODULE_PATH})

# If there's a better way to inform FindCyrusSASL.cmake, let's make this cleaner:
SET(CMAKE_PREFIX_PATH "${CMAKE_PREFIX_PATH};${CYRUS_SASL_DIR};${GSASL_DIR};$ENV{PROTOBUF_HOME}")

# Specify PROTOBUF_HOME so that find_package picks up the correct version
SET(CMAKE_PREFIX_PATH "${CMAKE_PREFIX_PATH};$ENV{PROTOBUF_HOME}")

find_package(Doxygen)
find_package(OpenSSL REQUIRED)
find_package(Protobuf REQUIRED)
find_package(CyrusSASL)
find_package(GSasl)
find_package(Threads)

include(CheckCXXSourceCompiles)
include(CheckSymbolExists)
include(FetchContent)

# Install googletest
# As per the approach documented in the GoogleTest repo -
# https://github.com/google/googletest/blob/e649993a402d96afe25fbf3413749adf0f2947f6/googletest/README.md#incorporating-into-an-existing-cmake-project
FetchContent_Declare(
  googletest
  URL https://github.com/google/googletest/archive/703bd9caab50b139428cea1aaff9974ebee5742e.zip
)
# The commit SHA 703bd9caab50b139428cea1aaff9974ebee5742e in the URL above corresponds to the
# tag release-1.10.0 in the googletest repository.
# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)

# Check if thread_local is supported
unset (THREAD_LOCAL_SUPPORTED CACHE)
set (CMAKE_CXX_STANDARD_REQUIRED ON)
set (CMAKE_REQUIRED_LIBRARIES ${CMAKE_THREAD_LIBS_INIT})
check_cxx_source_compiles(
    "#include <thread>
    int main(void) {
      thread_local int s;
      return 0;
    }"
    THREAD_LOCAL_SUPPORTED)
if (NOT THREAD_LOCAL_SUPPORTED)
  message(FATAL_ERROR
  "FATAL ERROR: The required feature thread_local storage is not supported by your compiler. \
  Known compilers that support this feature: GCC 4.8+, Visual Studio 2015+, Clang (community \
  version 3.3+), Clang (version for Xcode 8+ and iOS 9+).")
endif (NOT THREAD_LOCAL_SUPPORTED)

# Check if PROTOC library was compiled with the compatible compiler by trying
# to compile some dummy code
unset (PROTOC_IS_COMPATIBLE CACHE)
set (CMAKE_REQUIRED_INCLUDES ${PROTOBUF_INCLUDE_DIRS})
set (CMAKE_REQUIRED_LIBRARIES ${PROTOBUF_LIBRARY} ${PROTOBUF_PROTOC_LIBRARY})
check_cxx_source_compiles(
    "#include <google/protobuf/io/printer.h>
    #include <string>
    int main(void) {
      ::google::protobuf::io::ZeroCopyOutputStream *out = NULL;
      ::google::protobuf::io::Printer printer(out, '$');
      printer.PrintRaw(std::string(\"test\"));
      return 0;
    }"
    PROTOC_IS_COMPATIBLE)
if (NOT PROTOC_IS_COMPATIBLE)
  message(WARNING
  "WARNING: the Protocol Buffers Library and the Libhdfs++ Library must both be compiled \
  with the same (or compatible) compiler. Normally only the same major versions of the same \
  compiler are compatible with each other.")
endif (NOT PROTOC_IS_COMPATIBLE)

find_program(MEMORYCHECK_COMMAND valgrind HINTS ${VALGRIND_DIR} )
set(MEMORYCHECK_COMMAND_OPTIONS "--trace-children=no --leak-check=full --error-exitcode=1 --suppressions=${PROJECT_SOURCE_DIR}/tests/memcheck.supp")
message(STATUS "valgrind location: ${MEMORYCHECK_COMMAND}")

if (REQUIRE_VALGRIND AND MEMORYCHECK_COMMAND MATCHES "MEMORYCHECK_COMMAND-NOTFOUND" )
  message(FATAL_ERROR "valgrind was required but not found.  "
                      "The path can be included via a -DVALGRIND_DIR=... flag passed to CMake.")
endif (REQUIRE_VALGRIND AND MEMORYCHECK_COMMAND MATCHES "MEMORYCHECK_COMMAND-NOTFOUND" )

# Find the SASL library to use.  If you don't want to require a sasl library,
#    define -DNO_SASL=1 in your cmake call
# Prefer Cyrus SASL, but use GSASL if it is found
# Note that the packages can be disabled by setting CMAKE_DISABLE_FIND_PACKAGE_GSasl or
#    CMAKE_DISABLE_FIND_PACKAGE_CyrusSASL, respectively (case sensitive)
set (SASL_LIBRARIES)
set (SASL_INCLUDE_DIR)
if (NOT NO_SASL)
    if (CYRUS_SASL_FOUND)
        message(STATUS "Using Cyrus SASL; link with ${CYRUS_SASL_SHARED_LIB}")
        set (SASL_INCLUDE_DIR ${CYRUS_SASL_INCLUDE_DIR})
        set (SASL_LIBRARIES ${CYRUS_SASL_SHARED_LIB})
        set (CMAKE_USING_CYRUS_SASL 1)
        add_definitions(-DUSE_SASL -DUSE_CYRUS_SASL)
    else (CYRUS_SASL_FOUND)
        if (REQUIRE_CYRUS_SASL)
          message(FATAL_ERROR "Cyrus SASL was required but not found.  "
                                "The path can be included via a -DCYRUS_SASL_DIR=... flag passed to CMake.")
        endif (REQUIRE_CYRUS_SASL)

        # If we didn't pick Cyrus, use GSASL instead
        if (GSASL_FOUND)
          message(STATUS "Using GSASL; link with ${GSASL_LIBRARIES}")
          set (SASL_INCLUDE_DIR ${GSASL_INCLUDE_DIR})
          set (SASL_LIBRARIES ${GSASL_LIBRARIES})
          set (CMAKE_USING_GSASL 1)
          add_definitions(-DUSE_SASL -DUSE_GSASL)
        else (GSASL_FOUND)
          if (REQUIRE_GSASL)
            message(FATAL_ERROR "GSASL was required but not found.  "
                                "The path can be included via a -DGSASL_DIR=... flag passed to CMake.")
          endif (REQUIRE_GSASL)

          # No SASL was found, but NO_SASL was not defined
          message(FATAL_ERROR "Cound not find a SASL library (GSASL (gsasl) or Cyrus SASL (libsasl2).  "
                            "Install/configure one of them or define NO_SASL=1 in your cmake call")
        endif (GSASL_FOUND)
    endif (CYRUS_SASL_FOUND)
else (NOT NO_SASL)
    message(STATUS "Compiling with NO SASL SUPPORT")
endif (NOT NO_SASL)

check_symbol_exists(explicit_bzero "string.h" HAVE_EXPLICIT_BZERO)
if(HAVE_EXPLICIT_BZERO)
    add_definitions(-DHAVE_EXPLICIT_BZERO)
endif()

add_definitions(-DASIO_STANDALONE -DASIO_CPP11_DATE_TIME)

# Disable optimizations if compiling debug
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O0")
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -O0")

if(UNIX)
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pedantic -g -fPIC -fno-strict-aliasing")
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g -fPIC -fno-strict-aliasing")
endif()

if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
    add_definitions(-DASIO_HAS_STD_ADDRESSOF -DASIO_HAS_STD_ARRAY -DASIO_HAS_STD_ATOMIC -DASIO_HAS_CSTDINT -DASIO_HAS_STD_SHARED_PTR -DASIO_HAS_STD_TYPE_TRAITS -DASIO_HAS_VARIADIC_TEMPLATES -DASIO_HAS_STD_FUNCTION -DASIO_HAS_STD_CHRONO -DASIO_HAS_STD_SYSTEM_ERROR)
endif ()

# Mac OS 10.7 and later deprecates most of the methods in OpenSSL.
# Add -Wno-deprecated-declarations to avoid the warnings.
if(APPLE)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++ -Wno-deprecated-declarations -Wno-unused-local-typedef")
endif()

if(DOXYGEN_FOUND)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/doc/Doxyfile.in ${CMAKE_CURRENT_BINARY_DIR}/doc/Doxyfile @ONLY)
add_custom_target(doc ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/doc/Doxyfile
                  WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
                  COMMENT "Generating API documentation with Doxygen" VERBATIM)
endif(DOXYGEN_FOUND)


# Copy files from the hadoop tree into the output/extern directory if
#    they've changed
function (copy_on_demand input_src_glob input_dest_dir)
  get_filename_component(src_glob ${input_src_glob} REALPATH)
  get_filename_component(dest_dir ${input_dest_dir} REALPATH)
  get_filename_component(src_dir ${src_glob} PATH)
  message(STATUS "Syncing ${src_glob} to ${dest_dir}")

  file(GLOB_RECURSE src_files ${src_glob})
  foreach(src_path ${src_files})
    file(RELATIVE_PATH relative_src ${src_dir} ${src_path})
    set(dest_path "${dest_dir}/${relative_src}")
    add_custom_command(TARGET copy_hadoop_files
     COMMAND ${CMAKE_COMMAND} -E copy_if_different "${src_path}" "${dest_path}"
    )
  endforeach()
endfunction()

# If we're building in the hadoop tree, pull the Hadoop files that
#     libhdfspp depends on.  This allows us to ensure that
#     the distribution will have a consistent set of headers and
#     .proto files
if(HADOOP_BUILD)
    set(HADOOP_IMPORT_DIR ${PROJECT_BINARY_DIR}/extern)
    get_filename_component(HADOOP_IMPORT_DIR ${HADOOP_IMPORT_DIR} REALPATH)

  add_custom_target(copy_hadoop_files ALL)

  # Gather the Hadoop files and resources that libhdfs++ needs to build
  copy_on_demand(../libhdfs/include/*.h* ${HADOOP_IMPORT_DIR}/include)
  copy_on_demand(${CMAKE_CURRENT_LIST_DIR}/../../../../../hadoop-hdfs-client/src/main/proto/*.proto ${HADOOP_IMPORT_DIR}/proto/hdfs)
  copy_on_demand(${CMAKE_CURRENT_LIST_DIR}/../../../../../../hadoop-common-project/hadoop-common/src/main/proto/*.proto  ${HADOOP_IMPORT_DIR}/proto/hadoop)
  copy_on_demand(${CMAKE_CURRENT_LIST_DIR}/../../../../../../hadoop-common-project/hadoop-common/src/test/proto/*.proto  ${HADOOP_IMPORT_DIR}/proto/hadoop_test)
else(HADOOP_BUILD)
  set(HADOOP_IMPORT_DIR ${CMAKE_CURRENT_LIST_DIR}/extern)
endif(HADOOP_BUILD)

# Paths to find the imported files
set(PROTO_HDFS_DIR         ${HADOOP_IMPORT_DIR}/proto/hdfs)
set(PROTO_HADOOP_DIR       ${HADOOP_IMPORT_DIR}/proto/hadoop)
set(PROTO_HADOOP_TEST_DIR  ${HADOOP_IMPORT_DIR}/proto/hadoop_test)

include_directories(
  include
  lib
  ${HADOOP_IMPORT_DIR}/include
)

include_directories( SYSTEM
  ${PROJECT_BINARY_DIR}/lib/proto
  ${Boost_INCLUDE_DIRS}
  third_party/rapidxml-1.13
  ${gtest_SOURCE_DIR}/include
  ${gmock_SOURCE_DIR}/include
  third_party/tr2
  third_party/protobuf
  third_party/uriparser2
  ${OPENSSL_INCLUDE_DIR}
  ${SASL_INCLUDE_DIR}
  ${PROTOBUF_INCLUDE_DIRS}
)

add_subdirectory(third_party/uriparser2)
add_subdirectory(lib)
if(NOT HDFSPP_LIBRARY_ONLY)
    add_subdirectory(tests)
    add_subdirectory(examples)
    add_subdirectory(tools)
endif()

# create an empty file; hadoop_add_dual_library wraps add_library which
# requires at least one file as an argument
set(EMPTY_FILE_CC ${CMAKE_CURRENT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/empty.cc)
file(WRITE ${EMPTY_FILE_CC} "")

# Build the output libraries
if(NEED_LINK_DL)
   set(LIB_DL dl)
endif()

set(LIBHDFSPP_VERSION "0.1.0")
set(LIBHDFSPP_ALL_OBJECTS $<TARGET_OBJECTS:x_platform_obj> $<TARGET_OBJECTS:bindings_c_obj> $<TARGET_OBJECTS:fs_obj> $<TARGET_OBJECTS:rpc_obj> $<TARGET_OBJECTS:reader_obj> $<TARGET_OBJECTS:proto_obj> $<TARGET_OBJECTS:connection_obj> $<TARGET_OBJECTS:common_obj> $<TARGET_OBJECTS:uriparser2_obj>)
# HDFS-16464: We don't support building Hadoop DLL for Windows yet.
if (HADOOP_BUILD AND NOT MSVC)
  hadoop_add_dual_library(hdfspp ${EMPTY_FILE_CC} ${LIBHDFSPP_ALL_OBJECTS})
  hadoop_target_link_dual_libraries(hdfspp
    ${LIB_DL}
    ${PROTOBUF_LIBRARY}
    ${OPENSSL_LIBRARIES}
    ${SASL_LIBRARIES}
    ${CMAKE_THREAD_LIBS_INIT}
    ${Boost_LIBRARIES})
  set_target_properties(hdfspp PROPERTIES SOVERSION ${LIBHDFSPP_VERSION})
  hadoop_dual_output_directory(hdfspp ${OUT_DIR})
else (HADOOP_BUILD AND NOT MSVC)
  add_library(hdfspp_static STATIC ${EMPTY_FILE_CC} ${LIBHDFSPP_ALL_OBJECTS})
  target_link_libraries(hdfspp_static PUBLIC
    ${LIB_DL}
    ${PROTOBUF_LIBRARY}
    ${OPENSSL_LIBRARIES}
    ${SASL_LIBRARIES}
    ${CMAKE_THREAD_LIBS_INIT}
    ${Boost_LIBRARIES})
  if(BUILD_SHARED_HDFSPP)
    add_library(hdfspp SHARED ${EMPTY_FILE_CC} ${LIBHDFSPP_ALL_OBJECTS})
    set_target_properties(hdfspp PROPERTIES SOVERSION ${LIBHDFSPP_VERSION})
  endif(BUILD_SHARED_HDFSPP)
endif (HADOOP_BUILD AND NOT MSVC)

# Set up make install targets
# Can be installed to a particular location via "make DESTDIR=... install"
file(GLOB_RECURSE LIBHDFSPP_HEADER_FILES "${CMAKE_CURRENT_LIST_DIR}/include/*.h*")
file(GLOB_RECURSE LIBHDFS_HEADER_FILES "${HADOOP_IMPORT_DIR}/include/*.h*")
install(FILES ${LIBHDFSPP_HEADER_FILES} DESTINATION include/hdfspp)
install(FILES ${LIBHDFS_HEADER_FILES} DESTINATION include/hdfs)

install(TARGETS hdfspp_static ARCHIVE DESTINATION lib)
if(BUILD_SHARED_HDFSPP)
  install(TARGETS hdfspp LIBRARY DESTINATION lib)
endif(BUILD_SHARED_HDFSPP)

add_custom_target(
    InstallToBuildDirectory
    COMMAND "${CMAKE_MAKE_PROGRAM}" install DESTDIR=${PROJECT_BINARY_DIR}/output
)
set(LIBHDFSPP_DIR ${PROJECT_BINARY_DIR}/output)
