!> Manages global settings which are defined in the global config file.
module fpm_settings
  use fpm_filesystem, only: exists, join_path, get_local_prefix, is_absolute_path, mkdir
  use fpm_environment, only: os_is_unix
  use fpm_error, only: error_t, fatal_error
  use tomlf, only: toml_table, toml_error, toml_stat, toml_load
  use fpm_toml, only: get_value, check_keys
  use fpm_os, only: get_current_directory, change_directory, get_absolute_path, convert_to_absolute_path

  implicit none
  private
  public :: fpm_global_settings, get_global_settings, get_registry_settings, official_registry_base_url

  character(*), parameter :: official_registry_base_url = 'https://fpm-registry.vercel.app'
  character(*), parameter :: default_config_file_name = 'config.toml'

  type :: fpm_global_settings
    !> Path to the global config file excluding the file name.
    character(len=:), allocatable :: path_to_config_folder
    !> Name of the global config file. The default is `config.toml`.
    character(len=:), allocatable :: config_file_name
    !> Registry configs.
    type(fpm_registry_settings), allocatable :: registry_settings
  contains
    procedure :: has_custom_location, full_path, path_to_config_folder_or_empty
  end type

  type :: fpm_registry_settings
    !> The path to the local registry. If allocated, the local registry
    !> will be used instead of the remote registry and replaces the
    !> local cache.
    character(len=:), allocatable :: path
    !> The URL to the remote registry. Can be used to get packages
    !> from the official or a custom registry.
    character(len=:), allocatable :: url
    !> The path to the cache folder. If not specified, the default cache
    !> folders are `~/.local/share/fpm/dependencies` on Unix and
    !> `%APPDATA%\local\fpm\dependencies` on Windows.
    !> Cannot be used together with `path`.
    character(len=:), allocatable :: cache_path
  end type

contains
  !> Obtain global settings from the global config file.
  subroutine get_global_settings(global_settings, error)
    !> Global settings to be obtained.
    type(fpm_global_settings), intent(inout) :: global_settings
    !> Error reading config file.
    type(error_t), allocatable, intent(out) :: error
    !> TOML table to be filled with global config settings.
    type(toml_table), allocatable :: table
    !> Error parsing to TOML table.
    type(toml_error), allocatable :: parse_error

    type(toml_table), pointer :: registry_table
    integer :: stat

    ! Use custom path to the config file if it was specified.
    if (global_settings%has_custom_location()) then
      ! Throw error if folder doesn't exist.
      if (.not. exists(global_settings%path_to_config_folder)) then
        call fatal_error(error, "Folder not found: '"//global_settings%path_to_config_folder//"'."); return
      end if

      ! Throw error if the file doesn't exist.
      if (.not. exists(global_settings%full_path())) then
        call fatal_error(error, "File not found: '"//global_settings%full_path()//"'."); return
      end if

      ! Make sure that the path to the global config file is absolute.
      call convert_to_absolute_path(global_settings%path_to_config_folder, error)
      if (allocated(error)) return
    else
      ! Use default path if it wasn't specified.
      if (os_is_unix()) then
        global_settings%path_to_config_folder = join_path(get_local_prefix(), 'share', 'fpm')
      else
        global_settings%path_to_config_folder = join_path(get_local_prefix(), 'fpm')
      end if

      ! Use default file name.
      global_settings%config_file_name = default_config_file_name

      ! Apply default registry settings and return if config file doesn't exist.
      if (.not. exists(global_settings%full_path())) then
        call use_default_registry_settings(global_settings); return
      end if
    end if

    ! Load into TOML table.
    call toml_load(table, global_settings%full_path(), error=parse_error)

    if (allocated(parse_error)) then
      allocate (error); call move_alloc(parse_error%message, error%message); return
    end if

    call get_value(table, 'registry', registry_table, requested=.false., stat=stat)

    if (stat /= toml_stat%success) then
      call fatal_error(error, "Error reading registry from config file '"// &
      & global_settings%full_path()//"'."); return
    end if

    ! A registry table was found.
    if (associated(registry_table)) then
      call get_registry_settings(registry_table, global_settings, error)
    else
      call use_default_registry_settings(global_settings)
    end if
  end

  !> Default registry settings are typically applied if the config file doesn't exist or no registry table was found in
  !> the global config file.
  subroutine use_default_registry_settings(global_settings)
    type(fpm_global_settings), intent(inout) :: global_settings

    if (.not. allocated(global_settings%registry_settings)) allocate (global_settings%registry_settings)
    global_settings%registry_settings%url = official_registry_base_url
    global_settings%registry_settings%cache_path = join_path(global_settings%path_to_config_folder_or_empty(), &
    & 'dependencies')
  end

  !> Read registry settings from the global config file.
  subroutine get_registry_settings(table, global_settings, error)
    !> The [registry] subtable from the global config file.
    type(toml_table), target, intent(inout) :: table
    !> The global settings which can be filled with the registry settings.
    type(fpm_global_settings), intent(inout) :: global_settings
    !> Error handling.
    type(error_t), allocatable, intent(out) :: error

    character(:), allocatable :: path, url, cache_path
    integer :: stat

    !> List of valid keys for the dependency table.
    character(*), dimension(*), parameter :: valid_keys = [character(10) :: &
        & 'path', &
        & 'url', &
        & 'cache_path' &
        & ]

    call check_keys(table, valid_keys, error)
    if (allocated(error)) return

    allocate (global_settings%registry_settings)

    if (table%has_key('path')) then
      call get_value(table, 'path', path, stat=stat)
      if (stat /= toml_stat%success) then
        call fatal_error(error, "Error reading registry path: '"//path//"'."); return
      end if
    end if

    if (allocated(path)) then
      if (is_absolute_path(path)) then
        global_settings%registry_settings%path = path
      else
        ! Get canonical, absolute path on both Unix and Windows.
        call get_absolute_path(join_path(global_settings%path_to_config_folder_or_empty(), path), &
        & global_settings%registry_settings%path, error)
        if (allocated(error)) return

        ! Check if the path to the registry exists.
        if (.not. exists(global_settings%registry_settings%path)) then
          call fatal_error(error, "Directory '"//global_settings%registry_settings%path// &
          & "' doesn't exist."); return
        end if
      end if
    end if

    if (table%has_key('url')) then
      call get_value(table, 'url', url, stat=stat)
      if (stat /= toml_stat%success) then
        call fatal_error(error, "Error reading registry url: '"//url//"'."); return
      end if
    end if

    if (allocated(url)) then
      ! Throw error when both path and url were provided.
      if (allocated(path)) then
        call fatal_error(error, 'Do not provide both path and url to the registry.'); return
      end if
      global_settings%registry_settings%url = url
    else if (.not. allocated(path)) then
      global_settings%registry_settings%url = official_registry_base_url
    end if

    if (table%has_key('cache_path')) then
      call get_value(table, 'cache_path', cache_path, stat=stat)
      if (stat /= toml_stat%success) then
        call fatal_error(error, "Error reading path to registry cache: '"//cache_path//"'."); return
      end if
    end if

    if (allocated(cache_path)) then
      ! Throw error when both path and cache_path were provided.
      if (allocated(path)) then
        call fatal_error(error, "Do not provide both 'path' and 'cache_path'."); return
      end if

      if (is_absolute_path(cache_path)) then
        if (.not. exists(cache_path)) call mkdir(cache_path)
        global_settings%registry_settings%cache_path = cache_path
      else
        cache_path = join_path(global_settings%path_to_config_folder_or_empty(), cache_path)
        if (.not. exists(cache_path)) call mkdir(cache_path)
        ! Get canonical, absolute path on both Unix and Windows.
        call get_absolute_path(cache_path, global_settings%registry_settings%cache_path, error)
        if (allocated(error)) return
      end if
    else if (.not. allocated(path)) then
      global_settings%registry_settings%cache_path = &
        join_path(global_settings%path_to_config_folder_or_empty(), 'dependencies')
    end if
  end

  !> True if the global config file is not at the default location.
  elemental logical function has_custom_location(self)
    class(fpm_global_settings), intent(in) :: self

    has_custom_location = allocated(self%path_to_config_folder) .and. allocated(self%config_file_name)
    if (.not. has_custom_location) return
    has_custom_location = len_trim(self%path_to_config_folder) > 0 .and. len_trim(self%config_file_name) > 0
  end

  !> The full path to the global config file.
  function full_path(self) result(result)
    class(fpm_global_settings), intent(in) :: self
    character(len=:), allocatable :: result

    result = join_path(self%path_to_config_folder_or_empty(), self%config_file_name)
  end

  !> The path to the global config directory.
  pure function path_to_config_folder_or_empty(self)
    class(fpm_global_settings), intent(in) :: self
    character(len=:), allocatable :: path_to_config_folder_or_empty

    if (allocated(self%path_to_config_folder)) then
      path_to_config_folder_or_empty = self%path_to_config_folder
    else
      path_to_config_folder_or_empty = ""
    end if
  end
end
