# Usage:
#   lcm_wrap_types([C_HEADERS <VARIABLE_NAME> C_SOURCES <VARIABLE_NAME>
#                   [C_INCLUDE <PATH>] [C_EXPORT <NAME>]
#                   [C_NOPUBSUB] [C_TYPEINFO]]
#                  [CPP_HEADERS <VARIABLE_NAME>
#                   [CPP_INCLUDE <PATH>] [CPP11]]
#                  [JAVA_SOURCES <VARIABLE_NAME>]
#                  [CSHARP_SOURCES <VARIABLE_NAME>]
#                  [PYTHON_SOURCES <VARIABLE_NAME>]
#                  [LUA_SOURCES <VARIABLE_NAME>]
#                  [GO_SOURCES <VARIABLE_NAME>]
#                  [DESTINATION <PATH>]
#                  [GO_DESTINATION <PATH>]
#                  <FILE> [<FILE>...])
#     generate bindings for specified LCM type definition files
#
#   lcm_add_library(<NAME> C [STATIC|SHARED|MODULE] <SOURCES>)
#   lcm_add_library(<NAME> CPP <SOURCES>)
#     declare library for LCM type bindings
#
#   lcm_target_link_libraries(<TARGET> <...>)
#     link libraries, adding dependencies as needed for C++ bindings
#
#   lcm_install_headers([DESTINATION <PATH>]
#                       <FILE> [<FILE>...])
#
#   lcm_install_python([DESTINATION <PATH>]
#                      <FILE> [<FILE>...])

if(WIN32)
  # Need 'cmake -E env'
  cmake_minimum_required(VERSION 3.1.0)
else()
  cmake_minimum_required(VERSION 2.8.3)
endif()
include(CMakeParseArguments)

#------------------------------------------------------------------------------
function(_lcm_extract_token OUT INDEX)
  string(STRIP "${ARGN}" _text)
  string(REGEX REPLACE " +" ";" _tokens "${_text}")
  list(GET _tokens ${INDEX} _token)
  set(${OUT} "${_token}" PARENT_SCOPE)
endfunction()

#------------------------------------------------------------------------------
macro(_lcm_add_outputs VAR)
  foreach(_file ${ARGN})
    list(APPEND ${${VAR}} ${_DESTINATION}/${_file})
    list(APPEND _outputs ${_DESTINATION}/${_file})
  endforeach()
endmacro()

#------------------------------------------------------------------------------
macro(_lcm_add_go_outputs VAR)
  foreach(_file ${ARGN})
    list(APPEND ${${VAR}} ${_GO_DESTINATION}/${_file})
    list(APPEND _outputs ${_GO_DESTINATION}/${_file})
  endforeach()
endmacro()

#------------------------------------------------------------------------------
macro(_lcm_parent_list_append VAR)
  list(APPEND ${VAR} ${ARGN})
  set(${VAR} "${${VAR}}" PARENT_SCOPE)
endmacro()

#------------------------------------------------------------------------------
macro(_lcm_export VAR)
  if(DEFINED ${VAR})
    set(${${VAR}} "${${${VAR}}}" PARENT_SCOPE)
  endif()
endmacro()

#------------------------------------------------------------------------------
function(_lcm_create_aggregate_header NAME VAR)
  set(_header "${_DESTINATION}/${NAME}")
  list(FIND _aggregate_headers "${_header}" _index)
  if(_index EQUAL -1)
    string(REPLACE "/" "_" _guard "__lcmtypes_${NAME}__")
    file(WRITE ${_header}
      "#ifndef ${_guard}\n"
      "#define ${_guard}\n"
      "\n"
    )
    _lcm_parent_list_append(_aggregate_headers ${_header})
    _lcm_parent_list_append(${${VAR}} ${_header})
  endif()
endfunction()

#------------------------------------------------------------------------------
function(_lcm_add_aggregate_include AGGREGATE_HEADER TYPE_HEADER)
  set(_header "${_DESTINATION}/${AGGREGATE_HEADER}")
  if(ARGC GREATER 2)
    get_filename_component(_dir "${ARGV2}" NAME)
    file(APPEND ${_header} "#include \"${_dir}/${TYPE_HEADER}\"\n")
  else()
    file(APPEND ${_header} "#include \"${TYPE_HEADER}\"\n")
  endif()
endfunction()

#------------------------------------------------------------------------------
function(_lcm_add_python_package PATH)
  list(FIND _python_packages "${PATH}" _index)
  if(_index EQUAL -1)
    set(_init "${_DESTINATION}/${PATH}/__init__.py")
    if(EXISTS ${_init})
      file(REMOVE ${_init})
    endif()
    file(APPEND ${_init}
      "\"\"\"LCM package __init__.py file\n"
      "This file automatically generated by lcm_wrap_types.\n"
      "DO NOT MODIFY BY HAND!!!!\n"
      "\"\"\"\n\n")
    _lcm_parent_list_append(_python_packages ${PATH})
    _lcm_parent_list_append(${_PYTHON_SOURCES} ${_init})
  endif()
endfunction()

#------------------------------------------------------------------------------
function(_lcm_add_lua_package PATH)
  list(FIND _lua_packages "${PATH}" _index)
  if(_index EQUAL -1)
    set(_init "${_DESTINATION}/${PATH}/init.lua")
    file(WRITE ${_init} "local M = {}\n\n")
    _lcm_parent_list_append(_lua_packages ${PATH})
    _lcm_parent_list_append(${_LUA_SOURCES} ${_init})
  endif()
endfunction()

#------------------------------------------------------------------------------
function(_lcm_add_python_type PACKAGE TYPE)
  set(_init "${_DESTINATION}/${PACKAGE}/__init__.py")
  file(APPEND ${_init} "from .${TYPE} import ${TYPE}\n")
endfunction()

#------------------------------------------------------------------------------
function(_lcm_add_lua_type PACKAGE TYPE)
  set(_init "${_DESTINATION}/${PACKAGE}/init.lua")
  file(APPEND ${_init} "M.${TYPE} = require('${PACKAGE}.${TYPE}')\n")
endfunction()

#------------------------------------------------------------------------------
function(_lcm_finalize_aggregate_headers)
  foreach(_header ${_aggregate_headers})
    file(APPEND "${_header}" "\n#endif\n")
  endforeach()
endfunction()

#------------------------------------------------------------------------------
function(_lcm_finalize_lua_packages)
  foreach(_package ${_lua_packages})
    set(_init "${_DESTINATION}/${_package}/init.lua")
    file(APPEND ${_init} "\nreturn M\n")
  endforeach()
endfunction()

#------------------------------------------------------------------------------
function(_lcm_install_files DESTINATION RELATIVE_PATH)
  foreach(_file ${ARGN})
    file(RELATIVE_PATH _package_dir ${RELATIVE_PATH} ${_file})
    get_filename_component(_package_dir ${_package_dir} PATH)
    install(FILES ${_file} DESTINATION ${DESTINATION}/${_package_dir})
  endforeach()
endfunction()

#------------------------------------------------------------------------------
function(lcm_wrap_types)
  # Parse arguments
  set(_flags
    C_NOPUBSUB C_TYPEINFO
    CPP11
    CREATE_C_AGGREGATE_HEADER
    CREATE_CPP_AGGREGATE_HEADER
  )
  set(_sv_opts
    C_HEADERS C_SOURCES C_INCLUDE C_EXPORT
    CPP_HEADERS CPP_INCLUDE
    JAVA_SOURCES
    CSHARP_SOURCES
    PYTHON_SOURCES
    LUA_SOURCES
    GO_SOURCES
    DESTINATION
    GO_DESTINATION
    PACKAGE_PREFIX
  )
  set(_mv_opts "")
  cmake_parse_arguments("" "${_flags}" "${_sv_opts}" "${_mv_opts}" ${ARGN})

  # Check that either both or neither of C_SOURCES, C_HEADERS are given
  if(DEFINED _C_HEADERS AND NOT DEFINED _C_SOURCES)
    message(SEND_ERROR
      "lcm_wrap_types: C_SOURCES must be speficied if C_HEADERS is used")
    return()
  endif()
  if(NOT DEFINED _C_HEADERS AND DEFINED _C_SOURCES)
    message(SEND_ERROR
      "lcm_wrap_types: C_HEADERS must be speficied if C_SOURCES is used")
    return()
  endif()

  # Check for at least one language specified
  if(NOT DEFINED _C_HEADERS AND
     NOT DEFINED _CPP_HEADERS AND
     NOT DEFINED _JAVA_SOURCES AND
     NOT DEFINED _CSHARP_SOURCES AND
     NOT DEFINED _PYTHON_SOURCES AND
     NOT DEFINED _LUA_SOURCES AND
     NOT DEFINED _GO_SOURCES)
    message(SEND_ERROR
      "lcm_wrap_types: at least one of C_HEADERS, CPP_HEADERS, JAVA_SOURCES,"
      " CSHARP_SOURCES, PYTHON_SOURCES, LUA_SOURCES or GO_SOURCES is required")
    return()
  endif()

  # Set default destination, if none given
  if(NOT DEFINED _DESTINATION)
    set(_DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")
  endif()
  if(NOT DEFINED _GO_DESTINATION)
    set(_GO_DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/go/src")
  endif()

  # Set up arguments for invoking lcm-gen
  set(_args "")
  if(DEFINED _C_HEADERS)
    list(APPEND _args --c --c-cpath ${_DESTINATION} --c-hpath ${_DESTINATION})
    if(DEFINED _C_EXPORT)
      string(TOUPPER "${_C_EXPORT}_EXPORT" _C_EXPORT_SYMBOL)
      list(APPEND _args --c-export-symbol ${_C_EXPORT_SYMBOL})
      list(APPEND _args --c-export-include "${_C_EXPORT}_export.h")
    endif()
    if(DEFINED _C_INCLUDE)
      list(APPEND _args --cinclude ${_C_INCLUDE})
    endif()
    if(_C_NOPUBSUB)
      list(APPEND _args --c-no-pubsub)
    endif()
    if(_C_TYPEINFO)
      list(APPEND _args --c-typeinfo)
    endif()
  endif()
  if(DEFINED _CPP_HEADERS)
    list(APPEND _args --cpp --cpp-hpath ${_DESTINATION})
    if(DEFINED _CPP_INCLUDE)
      list(APPEND _args --cpp-include ${_CPP_INCLUDE})
    endif()
    if(_CPP11)
      list(APPEND _args --cpp-std=c++11)
    endif()
  endif()
  if(DEFINED _JAVA_SOURCES)
    list(APPEND _args --java --jpath ${_DESTINATION})
  endif()
  if(DEFINED _CSHARP_SOURCES)
    list(APPEND _args --csharp --csharp-path ${_DESTINATION})
  endif()
  if(DEFINED _PYTHON_SOURCES)
    list(APPEND _args --python --python-no-init --ppath ${_DESTINATION})
  endif()
  if(DEFINED _LUA_SOURCES)
    list(APPEND _args --lua --lua-no-init --lpath ${_DESTINATION})
  endif()
  if(DEFINED _GO_SOURCES)
    list(APPEND _args --go --go-path ${_GO_DESTINATION})
  endif()
  if(DEFINED _PACKAGE_PREFIX)
    list(APPEND _args --package-prefix ${_PACKAGE_PREFIX})
  endif()

  # Create build rules
  set(_aggregate_headers "")
  set(_python_packages "")
  set(_lua_packages "")
  foreach(_lcmtype ${_UNPARSED_ARGUMENTS})
    set(_package "")
    set(_outputs "")
    set(_package_dir ".")
    set(_package_pre "")
    # Read type definition
    file(READ ${_lcmtype} _text)
    # Strip comments
    string(REGEX REPLACE "//[^\n]*\n" "" _text "${_text}")
    string(REGEX REPLACE "/[*]([^*]*[*][^*/])*[^*]*[*]+/" "" _text "${_text}")
    # Tokenize
    string(REGEX REPLACE "[\t\n ]+" " " _text "${_text}")
    string(REGEX REPLACE "[{}]" "\\0;" _text "${_text}")
    # Look for package and struct specifications
    foreach(_line ${_text})
      if(_line MATCHES "^ *package +")
        # Get package name
        _lcm_extract_token(_package 1 "${_line}")
        # Append package prefix if one was specified
        if(DEFINED _PACKAGE_PREFIX)
          set(_package "${_PACKAGE_PREFIX}.${_package}")
        endif()
        string(REPLACE "." "/" _package_dir "${_package}")
        string(REPLACE "." "_" _package_pre "${_package}")
        if(DEFINED _C_HEADERS AND _CREATE_C_AGGREGATE_HEADER)
          _lcm_create_aggregate_header("${_package_dir}.h" _C_HEADERS)
        endif()
        if(DEFINED _CPP_HEADERS AND _CREATE_CPP_AGGREGATE_HEADER)
          _lcm_create_aggregate_header("${_package_dir}.hpp" _CPP_HEADERS)
        endif()
      elseif(_line MATCHES "^ *(struct|enum) +")
        # Get type name
        _lcm_extract_token(_type 1 "${_line}")

        # Determine output file name(s) and add to output variables
        if(DEFINED _C_HEADERS AND DEFINED _C_SOURCES)
          if(_package_pre STREQUAL "")
            _lcm_add_outputs(_C_HEADERS ${_type}.h)
            _lcm_add_outputs(_C_SOURCES ${_type}.c)
          else()
            _lcm_add_outputs(_C_HEADERS ${_package_pre}_${_type}.h)
            _lcm_add_outputs(_C_SOURCES ${_package_pre}_${_type}.c)
          endif()
          if(_CREATE_CPP_AGGREGATE_HEADER)
            _lcm_add_aggregate_include("${_package_dir}.h"
              "${_package_pre}_${_type}.h")
          endif()
        endif()
        if(DEFINED _CPP_HEADERS)
          _lcm_add_outputs(_CPP_HEADERS ${_package_dir}/${_type}.hpp)
          if(_CREATE_CPP_AGGREGATE_HEADER)
            _lcm_add_aggregate_include("${_package_dir}.hpp"
              "${_type}.hpp" "${_package_dir}")
          endif()
        endif()
        if(DEFINED _PYTHON_SOURCES)
          _lcm_add_python_package(${_package_dir})
          _lcm_add_python_type(${_package_dir} ${_type})
          _lcm_add_outputs(_PYTHON_SOURCES ${_package_dir}/${_type}.py)
        endif()
        if(DEFINED _LUA_SOURCES)
          _lcm_add_lua_package(${_package_dir})
          _lcm_add_lua_type(${_package_dir} ${_type})
          _lcm_add_outputs(_LUA_SOURCES ${_package_dir}/${_type}.lua)
        endif()
        if(DEFINED _GO_SOURCES)
          _lcm_add_go_outputs(_GO_SOURCES ${_package_dir}/${_type}.go)
        endif()
        if(DEFINED _JAVA_SOURCES)
          if(_package_dir STREQUAL ".")
            _lcm_add_outputs(_JAVA_SOURCES lcmtypes/${_type}.java)
          else()
            _lcm_add_outputs(_JAVA_SOURCES ${_package_dir}/${_type}.java)
          endif()
        endif()
        if(DEFINED _CSHARP_SOURCES)
          if(_package_dir STREQUAL ".")
            _lcm_add_outputs(_CSHARP_SOURCES LCMTypes/${_type}.cs)
          else()  
            _lcm_add_outputs(_CSHARP_SOURCES ${_package_dir}/${_type}.cs)
          endif()
        endif()
      endif()
    endforeach()

    # Define build command for input file
    get_filename_component(_lcmtype_full "${_lcmtype}" ABSOLUTE)
    if(WIN32)
      add_custom_command(
        OUTPUT ${_outputs}
        COMMAND ${CMAKE_COMMAND} -E env "PATH=${LCM_LCMGEN_PATH}"
          $<TARGET_FILE:${LCM_NAMESPACE}lcm-gen> ${_args} ${_lcmtype_full}
        DEPENDS ${_lcmtype}
      )
    else()
      add_custom_command(
        OUTPUT ${_outputs}
        COMMAND ${LCM_NAMESPACE}lcm-gen ${_args} ${_lcmtype_full}
        DEPENDS ${_lcmtype}
      )
    endif()
  endforeach()

  # Finalize aggregate headers and packages
  _lcm_finalize_aggregate_headers()
  _lcm_finalize_lua_packages()

  # Set output files in parent scope
  _lcm_export(_C_SOURCES)
  _lcm_export(_C_HEADERS)
  _lcm_export(_CPP_HEADERS)
  _lcm_export(_JAVA_SOURCES)
  _lcm_export(_CSHARP_SOURCES)
  _lcm_export(_PYTHON_SOURCES)
  _lcm_export(_LUA_SOURCES)
  _lcm_export(_GO_SOURCES)
endfunction()

#------------------------------------------------------------------------------
if(NOT CMAKE_VERSION VERSION_LESS 3.1)
  function(lcm_add_library NAME LANGUAGE)
    string(TOUPPER ${LANGUAGE} LANGUAGE)

    # C library
    if(LANGUAGE STREQUAL "C")

      # Check for library type
      if(${ARGV2} MATCHES "STATIC|SHARED|MODULE")
        set(_type ${ARGV2})
        list(REMOVE_AT ARGN 0)
      else()
        set(_type "")
      endif()

      # Add dependency target for generated sources
      add_custom_target(${NAME}.sources DEPENDS ${ARGN})

      # Add library
      add_library(${NAME} ${_type} ${ARGN})
      add_dependencies(${NAME} ${NAME}.sources)
      target_link_libraries(${NAME}
        PRIVATE ${LCM_NAMESPACE}lcm
        PUBLIC ${LCM_NAMESPACE}lcm-coretypes)

    # C++ library
    elseif(LANGUAGE MATCHES "CXX|CPP|C\\+\\+")

      # Add dependency target for generated sources
      add_custom_target(${NAME}.sources DEPENDS ${ARGN})

      # Add library
      add_library(${NAME} INTERFACE)
      target_link_libraries(${NAME} INTERFACE ${LCM_NAMESPACE}lcm-coretypes)

      # Add dependency on generated sources
      # NOTE: dependencies on INTERFACE targets not supported before CMake 3.3
      if(NOT CMAKE_VERSION VERSION_LESS 3.3)
        add_dependencies(${NAME} ${NAME}.sources)
      endif()

    # Unsupported library language
    else()
      message(SEND_ERROR
        "lcm_add_library: library language must be C or CPP")
    endif()
  endfunction()
endif()

#------------------------------------------------------------------------------
if(NOT CMAKE_VERSION VERSION_LESS 3.1)
  function(lcm_target_link_libraries TARGET)
    target_link_libraries(${TARGET} ${ARGN})

    # NOTE: with CMake 3.3 or later, lcm_add_library creates a dependency on
    # the C++ bindings library that its headers have been created first. This
    # relies on adding a dependency to an INTERFACE library, which was not
    # supported prior to CMake 3.3. For older versions of CMake, check if any
    # argument appears to be a C++ bindings library that was created with
    # lcm_add_library and, if so, manually add a dependency to the consumer
    # to ensure the bindings are generated before trying to build the consumer.
    if(CMAKE_VERSION VERSION_LESS 3.3)
      foreach(_target ${ARGN})
        if(TARGET ${_target} AND TARGET ${_target}.sources)
          add_dependencies(${TARGET} ${_target}.sources)
        endif()
      endforeach()
    endif()
  endfunction()
endif()

#------------------------------------------------------------------------------
function(lcm_install_headers)
  # Parse arguments
  set(_flags "")
  set(_sv_opts DESTINATION RELATIVE_PATH)
  set(_mv_opts "")
  cmake_parse_arguments("" "${_flags}" "${_sv_opts}" "${_mv_opts}" ${ARGN})

  # Set default destination and relative path, if none given
  if(NOT DEFINED _DESTINATION)
    set(_DESTINATION include)
  endif()
  if(NOT DEFINED _RELATIVE_PATH)
    set(_RELATIVE_PATH "${CMAKE_CURRENT_BINARY_DIR}")
  endif()

  _lcm_install_files(${_DESTINATION} ${_RELATIVE_PATH} ${_UNPARSED_ARGUMENTS})
endfunction()

#------------------------------------------------------------------------------
function(lcm_install_python)
  # Parse arguments
  set(_flags "")
  set(_sv_opts DESTINATION RELATIVE_PATH)
  set(_mv_opts "")
  cmake_parse_arguments("" "${_flags}" "${_sv_opts}" "${_mv_opts}" ${ARGN})

  # Set default destination and relative path, if none given
  if(NOT DEFINED _DESTINATION)
    if(NOT PYTHONINTERP_FOUND)
      message(SEND_ERROR
        "lcm_install_python: no DESTINATION given"
        " and no Python interpreter found (required to guess DESTINATION)")
      return()
    endif()
    execute_process(
      COMMAND "${PYTHON_EXECUTABLE}" -c "if True:
        from distutils import sysconfig as sc
        print(sc.get_python_lib(prefix='', plat_specific=True))"
      OUTPUT_VARIABLE _DESTINATION
      OUTPUT_STRIP_TRAILING_WHITESPACE)
  endif()
  if(NOT DEFINED _RELATIVE_PATH)
    set(_RELATIVE_PATH "${CMAKE_CURRENT_BINARY_DIR}")
  endif()

  _lcm_install_files(${_DESTINATION} ${_RELATIVE_PATH} ${_UNPARSED_ARGUMENTS})
endfunction()
