# SPDX-License-Identifier: Apache-2.0
#
# Copyright (c) 2021, Nordic Semiconductor ASA

# Validate board and setup boards target.
#
# This CMake module will validate the BOARD argument as well as splitting the
# BOARD argument into <BOARD> and <BOARD_REVISION>. When BOARD_EXTENSIONS option
# is enabled (default) this module will also take care of finding board
# extension directories.
#
# If a board implementation is not found for the specified board an error will
# be raised and list of valid boards will be printed.
#
# If user provided board is a board alias, the board will be adjusted to real
# board name.
#
# If board name is deprecated, then board will be adjusted to new board name and
# a deprecation warning will be printed to the user.
#
# Outcome:
# The following variables will be defined when this CMake module completes:
#
# - BOARD:                       Board, without revision field.
# - BOARD_REVISION:              Board revision
# - BOARD_QUALIFIERS:            Board qualifiers
# - NORMALIZED_BOARD_QUALIFIERS: Board qualifiers in lower-case format where slashes have been
#                                replaced with underscores
# - NORMALIZED_BOARD_TARGET:     Board target in lower-case format where slashes have been
#                                replaced with underscores
# - BOARD_DIR:                   Board directory with the implementation for selected board
# - ARCH_DIR:                    Arch dir for extracted from selected board
# - BOARD_ROOT:                  BOARD_ROOT with ZEPHYR_BASE appended
# - BOARD_EXTENSION_DIRS:        List of board extension directories (If
#                                BOARD_EXTENSIONS is not explicitly disabled)
#
# The following targets will be defined when this CMake module completes:
# - board: when invoked, a list of valid boards will be printed
#
# Required variables:
# - BOARD: Board name, including any optional revision field, for example: `foo` or `foo@1.0.0`
#
# Optional variables:
# - BOARD_ROOT: CMake list of board roots containing board implementations
# - ARCH_ROOT:  CMake list of arch roots containing arch implementations
#
# Optional environment variables:
# - ZEPHYR_BOARD_ALIASES: Environment setting pointing to a CMake file
#                         containing board aliases.
#
# Variables set by this module and not mentioned above are for internal
# use only, and may be removed, renamed, or re-purposed without prior notice.

include_guard(GLOBAL)

include(python)
include(extensions)

# Check that BOARD has been provided, and that it has not changed.
# If user tries to change the BOARD, the BOARD value is reset to the BOARD_CACHED value.
zephyr_check_cache(BOARD REQUIRED)

# 'BOARD_ROOT' is a prioritized list of directories where boards may
# be found. It always includes ${ZEPHYR_BASE} at the lowest priority (except for unittesting).
if(NOT unittest IN_LIST Zephyr_FIND_COMPONENTS)
  list(APPEND BOARD_ROOT ${ZEPHYR_BASE})
endif()

# Helper function for parsing a board's name, revision, and qualifiers,
# from one input variable to three separate output variables.
function(parse_board_components board_in name_out revision_out qualifiers_out)
  if(NOT "${${board_in}}" MATCHES "^([^@/]+)(@[^@/]+)?(/[^@]+)?$")
    message(FATAL_ERROR
      "Invalid revision / qualifiers format for ${board_in} (${${board_in}}). "
      "Valid format is: <board>@<revision>/<qualifiers>"
    )
  endif()
  string(REPLACE "@" "" board_revision "${CMAKE_MATCH_2}")

  set(${name_out}       ${CMAKE_MATCH_1}  PARENT_SCOPE)
  set(${revision_out}   ${board_revision} PARENT_SCOPE)
  set(${qualifiers_out} ${CMAKE_MATCH_3}  PARENT_SCOPE)
endfunction()

parse_board_components(
  BOARD
  BOARD BOARD_REVISION BOARD_QUALIFIERS
)

zephyr_get(ZEPHYR_BOARD_ALIASES)
if(DEFINED ZEPHYR_BOARD_ALIASES)
  include(${ZEPHYR_BOARD_ALIASES})
  if(${BOARD}_BOARD_ALIAS)
    set(BOARD_ALIAS ${BOARD} CACHE STRING "Board alias, provided by user")
    parse_board_components(
      ${BOARD}_BOARD_ALIAS
      BOARD BOARD_ALIAS_REVISION BOARD_ALIAS_QUALIFIERS
    )
    message(STATUS "Aliased BOARD=${BOARD_ALIAS} changed to ${BOARD}")
    if(NOT DEFINED BOARD_REVISION)
      set(BOARD_REVISION ${BOARD_ALIAS_REVISION})
    endif()
    set(BOARD_QUALIFIERS ${BOARD_ALIAS_QUALIFIERS}${BOARD_QUALIFIERS})
  endif()
endif()

include(${ZEPHYR_BASE}/boards/deprecated.cmake)
if(${BOARD}${BOARD_QUALIFIERS}_DEPRECATED)
  set(BOARD_DEPRECATED ${BOARD}${BOARD_QUALIFIERS} CACHE STRING "Deprecated BOARD, provided by user")
  message(WARNING
    "Deprecated BOARD=${BOARD_DEPRECATED} specified, "
    "board automatically changed to: ${${BOARD}${BOARD_QUALIFIERS}_DEPRECATED}."
  )
  parse_board_components(
    ${BOARD}${BOARD_QUALIFIERS}_DEPRECATED
    BOARD BOARD_DEPRECATED_REVISION BOARD_QUALIFIERS
  )
  if(DEFINED BOARD_DEPRECATED_REVISION)
    if(DEFINED BOARD_REVISION)
      message(FATAL_ERROR
        "Invalid board revision: ${BOARD_REVISION}\n"
        "Deprecated board '${BOARD_DEPRECATED}' is now implemented as a revision of another board "
        "(${BOARD}@${BOARD_DEPRECATED_REVISION}), so the specified revision does not apply. "
        "Please consult the documentation for '${BOARD}' to see how to build for the new board."
      )
    endif()
    set(BOARD_REVISION ${BOARD_DEPRECATED_REVISION})
  endif()
endif()

zephyr_boilerplate_watch(BOARD)

foreach(root ${BOARD_ROOT})
  # Check that the board root looks reasonable.
  if(NOT IS_DIRECTORY "${root}/boards")
    message(WARNING "BOARD_ROOT element without a 'boards' subdirectory:
${root}
Hints:
  - if your board directory is '/foo/bar/boards/<ARCH>/my_board' then add '/foo/bar' to BOARD_ROOT, not the entire board directory
  - if in doubt, use absolute paths")
  endif()
endforeach()

if((HWMv1 AND NOT EXISTS ${BOARD_DIR}/${BOARD}_defconfig)
   OR (HWMv2 AND NOT EXISTS ${BOARD_DIR}/board.yml))
  message(WARNING "BOARD_DIR: ${BOARD_DIR} has been moved or deleted. "
                  "Trying to find new location."
  )
  set(BOARD_DIR BOARD_DIR-NOTFOUND CACHE PATH "Path to a file." FORCE)
endif()

# Prepare list boards command.
# This command is used for locating the board dir as well as printing all boards
# in the system in the following cases:
# - User specifies an invalid BOARD
# - User invokes '<build-command> boards' target
list(TRANSFORM ARCH_ROOT PREPEND "--arch-root=" OUTPUT_VARIABLE arch_root_args)
list(TRANSFORM BOARD_ROOT PREPEND "--board-root=" OUTPUT_VARIABLE board_root_args)
list(TRANSFORM SOC_ROOT PREPEND "--soc-root=" OUTPUT_VARIABLE soc_root_args)

set(list_boards_commands
    COMMAND ${PYTHON_EXECUTABLE} ${ZEPHYR_BASE}/scripts/list_boards.py
            ${arch_root_args} ${board_root_args} --arch-root=${ZEPHYR_BASE}
            ${soc_root_args} --soc-root=${ZEPHYR_BASE}
)

if(NOT BOARD_DIR)
  if(BOARD_ALIAS)
    execute_process(${list_boards_commands} --board=${BOARD_ALIAS} --cmakeformat={DIR}
                    OUTPUT_VARIABLE ret_board
                    ERROR_VARIABLE err_board
                    RESULT_VARIABLE ret_val
    )
    string(STRIP "${ret_board}" ret_board)
    cmake_parse_arguments(BOARD_HIDDEN "" "DIR" "" ${ret_board})
    set(BOARD_HIDDEN_DIR ${BOARD_HIDDEN_DIR} CACHE PATH "Path to a folder." FORCE)

    if(BOARD_HIDDEN_DIR)
      message("Board alias ${BOARD_ALIAS} is hiding the real board of same name")
    endif()
  endif()
endif()

set(format_str "{NAME}\;{DIR}\;{HWM}\;")
set(format_str "${format_str}{REVISION_FORMAT}\;{REVISION_DEFAULT}\;{REVISION_EXACT}\;")
set(format_str "${format_str}{REVISIONS}\;{SOCS}\;{QUALIFIERS}")

if(BOARD_DIR)
  set(board_dir_arg "--board-dir=${BOARD_DIR}")
endif()
execute_process(${list_boards_commands} --board=${BOARD} ${board_dir_arg}
  --cmakeformat=${format_str}
                OUTPUT_VARIABLE ret_board
                ERROR_VARIABLE err_board
                RESULT_VARIABLE ret_val
)
if(ret_val)
  message(FATAL_ERROR "Error finding board: ${BOARD}\nError message: ${err_board}")
endif()

if(NOT "${ret_board}" STREQUAL "")
  string(STRIP "${ret_board}" ret_board)
  string(FIND "${ret_board}" "\n" idx REVERSE)
  if(idx GREATER -1)
    while(TRUE)
      math(EXPR start "${idx} + 1")
      string(SUBSTRING "${ret_board}" ${start} -1 line)
      string(SUBSTRING "${ret_board}" 0 ${idx} ret_board)

      cmake_parse_arguments(LIST_BOARD "" "DIR" "" ${line})
      set(board_dirs "${board_dirs}\n${LIST_BOARD_DIR}")

      if(idx EQUAL -1)
        break()
      endif()
      string(FIND "${ret_board}" "\n" idx REVERSE)
    endwhile()
    message(FATAL_ERROR "Multiple boards named '${BOARD}' found in:${board_dirs}")
  endif()

  set(single_val "NAME;DIR;HWM;REVISION_FORMAT;REVISION_DEFAULT;REVISION_EXACT")
  set(multi_val  "REVISIONS;SOCS;QUALIFIERS")
  cmake_parse_arguments(LIST_BOARD "" "${single_val}" "${multi_val}" ${ret_board})
  set(BOARD_DIR ${LIST_BOARD_DIR} CACHE PATH "Board directory for board (${BOARD})" FORCE)
  set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${BOARD_DIR}/board.yml)

  # Create two CMake variables identifying the hw model.
  # CMake variable: HWM=[v1,v2]
  # CMake variable: HWMv1=True, when HWMv1 is in use.
  # CMake variable: HWMv2=True, when HWMv2 is in use.
  set(HWM       ${LIST_BOARD_HWM} CACHE INTERNAL "Zephyr hardware model version")
  set(HWM${HWM} True   CACHE INTERNAL "Zephyr hardware model")
elseif(BOARD_DIR)
  message(FATAL_ERROR "Error finding board: ${BOARD} in ${BOARD_DIR}.\n"
          "This indicates the board has been removed, renamed, or placed at a new location.\n"
	  "Please run a pristine build."
  )
else()
  message("No board named '${BOARD}' found.\n\n"
          "Please choose one of the following boards:\n"
  )
  execute_process(${list_boards_commands})
  unset(CACHED_BOARD CACHE)
  message(FATAL_ERROR "Invalid BOARD; see above.")
endif()

if(HWMv1 AND DEFINED BOARD_QUALIFIERS)
  message(FATAL_ERROR
          "Board '${BOARD}' does not support board qualifiers, ${BOARD}${BOARD_QUALIFIERS}.\n"
          "Please specify board without qualifiers.\n"
  )
endif()

cmake_path(IS_PREFIX ZEPHYR_BASE "${BOARD_DIR}" NORMALIZE in_zephyr_tree)
if(NOT in_zephyr_tree)
  set(USING_OUT_OF_TREE_BOARD 1)
endif()

if(HWMv1)
  if(EXISTS ${BOARD_DIR}/revision.cmake)
    # Board provides revision handling.
    include(${BOARD_DIR}/revision.cmake)
  elseif(BOARD_REVISION)
    message(WARNING "Board revision ${BOARD_REVISION} specified for ${BOARD}, \
                     but board has no revision so revision will be ignored.")
  endif()
elseif(HWMv2)
  if(LIST_BOARD_REVISION_FORMAT)
    if(LIST_BOARD_REVISION_FORMAT STREQUAL "custom")
      include(${BOARD_DIR}/revision.cmake)
    else()
      if(EXISTS ${BOARD_DIR}/revision.cmake)
        message(WARNING
          "revision.cmake ignored, revision.cmake is only used for revision format: 'custom'"
        )
      endif()

      string(TOUPPER "${LIST_BOARD_REVISION_FORMAT}" rev_format)
      if(LIST_BOARD_REVISION_EXACT)
        set(rev_exact EXACT)
      endif()

      board_check_revision(
        FORMAT ${rev_format}
        DEFAULT_REVISION ${LIST_BOARD_REVISION_DEFAULT}
        VALID_REVISIONS ${LIST_BOARD_REVISIONS}
        ${rev_exact}
      )
    endif()
  elseif(DEFINED BOARD_REVISION)
    if(EXISTS ${BOARD_DIR}/revision.cmake)
      message(WARNING
        "revision.cmake is not used, revisions must be defined in '${BOARD_DIR}/board.yml'"
      )
    endif()

    message(FATAL_ERROR "Invalid board revision: ${BOARD_REVISION}\n"
                        "Board '${BOARD}' does not define any revisions."
    )
  endif()

  if(LIST_BOARD_QUALIFIERS)
    # Allow users to omit the SoC when building for a board with a single SoC.
    list(LENGTH LIST_BOARD_SOCS socs_length)
    if(socs_length EQUAL 1)
      set(BOARD_SINGLE_SOC TRUE)
      set(BOARD_${BOARD}_SINGLE_SOC TRUE)
      if(NOT DEFINED BOARD_QUALIFIERS)
        set(BOARD_QUALIFIERS "/${LIST_BOARD_SOCS}")
      elseif("${BOARD_QUALIFIERS}" MATCHES "^//.*")
        string(REGEX REPLACE "^//" "/${LIST_BOARD_SOCS}/" BOARD_QUALIFIERS "${BOARD_QUALIFIERS}")
      endif()
    endif()

    set(board_targets ${LIST_BOARD_QUALIFIERS})
    list(TRANSFORM board_targets PREPEND "${BOARD}/")
    if(NOT ("${BOARD}${BOARD_QUALIFIERS}" IN_LIST board_targets))
      string(REPLACE ";" "\n" board_targets "${board_targets}")
      unset(CACHED_BOARD CACHE)
      message(FATAL_ERROR "Board qualifiers `${BOARD_QUALIFIERS}` for board \
            `${BOARD}` not found. Please specify a valid board target.\n"
            "Valid board targets for ${BOARD_NAME} are:\n${board_targets}\n")
    endif()
  endif()
else()
  message(FATAL_ERROR "Unknown hw model (${HWM}) for board: ${BOARD}.")
endif()

set(board_message "Board: ${BOARD}")

if(DEFINED BOARD_REVISION)
  set(board_message "${board_message}, Revision: ${BOARD_REVISION}")
  if(DEFINED ACTIVE_BOARD_REVISION)
    set(board_message "${board_message} (Active: ${ACTIVE_BOARD_REVISION})")
    set(BOARD_REVISION ${ACTIVE_BOARD_REVISION})
  endif()

  string(REPLACE "." "_" BOARD_REVISION_STRING ${BOARD_REVISION})
endif()

if(DEFINED BOARD_QUALIFIERS)
  string(REGEX REPLACE "^/" "qualifiers: " board_message_qualifiers "${BOARD_QUALIFIERS}")
  set(board_message "${board_message}, ${board_message_qualifiers}")

  string(REPLACE "/" "_" NORMALIZED_BOARD_QUALIFIERS "${BOARD_QUALIFIERS}")
endif()

set(NORMALIZED_BOARD_TARGET "${BOARD}${BOARD_QUALIFIERS}")
string(REPLACE "/" "_" NORMALIZED_BOARD_TARGET "${NORMALIZED_BOARD_TARGET}")

message(STATUS "${board_message}")

add_custom_target(boards ${list_boards_commands} USES_TERMINAL)

# Board extensions are enabled by default
set(BOARD_EXTENSIONS ON CACHE BOOL "Support board extensions")
zephyr_get(BOARD_EXTENSIONS)

# Process board extensions
if(BOARD_EXTENSIONS)
  get_filename_component(board_dir_name ${BOARD_DIR} NAME)

  foreach(root ${BOARD_ROOT})
    set(board_extension_dir ${root}/boards/extensions/${board_dir_name})
    if(NOT EXISTS ${board_extension_dir})
      continue()
    endif()

    list(APPEND BOARD_EXTENSION_DIRS ${board_extension_dir})
  endforeach()
endif()
