require 'puppet/util/platform'
require 'puppet/file_system'

module Puppet::GettextConfig
  LOCAL_PATH = File.absolute_path('../../../locales', File.dirname(__FILE__))
  POSIX_PATH = File.absolute_path('../../../../../share/locale', File.dirname(__FILE__))
  WINDOWS_PATH = File.absolute_path('../../../../../../../puppet/share/locale', File.dirname(__FILE__))

  # This is the only domain name that won't be a symbol, making it unique from environments.
  DEFAULT_TEXT_DOMAIN = 'default-text-domain'

  # Load gettext helpers and track whether they're available.
  # Used instead of features because we initialize gettext before features is available.
  begin
    require 'fast_gettext'
    require 'locale'

    # Make translation methods (e.g. `_()` and `n_()`) available everywhere.
    class ::Object
      include FastGettext::Translation
    end

    @gettext_loaded = true
  rescue LoadError
    # Stub out gettext's `_` and `n_()` methods, which attempt to load translations,
    # with versions that do nothing
    require 'puppet/gettext/stubs'
    @gettext_loaded = false
  end

  # @api private
  # Whether we were able to require fast_gettext and locale
  # @return [Boolean] true if translation gems were successfully loaded
  def self.gettext_loaded?
    @gettext_loaded
  end

  # @api private
  # Returns the currently selected locale from FastGettext,
  # or 'en' of gettext has not been loaded
  # @return [String] the active locale
  def self.current_locale
    if gettext_loaded?
      return FastGettext.default_locale
    else
      return 'en'
    end
  end

  # @api private
  # Returns a list of the names of the loaded text domains
  # @return [[String]] the names of the loaded text domains
  def self.loaded_text_domains
    return [] if @gettext_disabled || !gettext_loaded?

    return FastGettext.translation_repositories.keys
  end

  # @api private
  # Clears the translation repository for the given text domain,
  # creating it if it doesn't exist, then adds default translations
  # and switches to using this domain.
  # @param [String, Symbol] domain_name the name of the domain to create
  def self.reset_text_domain(domain_name)
    return if @gettext_disabled || !gettext_loaded?
    domain_name = domain_name.to_sym

    Puppet.debug "Reset text domain to #{domain_name.inspect}"
    FastGettext.add_text_domain(domain_name,
                                type: :chain,
                                chain: [],
                                report_warning: false)
    copy_default_translations(domain_name)
    FastGettext.text_domain = domain_name
  end

  # @api private
  # Resets the thread's configured text_domain to the default text domain.
  # In Puppet Server, thread A may process a compile request that configures
  # a domain, while thread B may invalidate that environment and delete the
  # domain. That leaves thread A with an invalid text_domain selected.
  # To avoid that, clear_text_domain after any processing that needs the
  # non-default text domain.
  def self.clear_text_domain
    return if @gettext_disabled || !gettext_loaded?
    FastGettext.text_domain = nil
  end

  # @api private
  # Creates a default text domain containing the translations for
  # Puppet as the start of chain. When semantic_puppet gets initialized,
  # its translations are added to this chain. This is used as a cache
  # so that all non-module translations only need to be loaded once as
  # we create and reset environment-specific text domains.
  #
  # @return true if Puppet translations were successfully loaded, false
  # otherwise
  def self.create_default_text_domain
    return if @gettext_disabled || !gettext_loaded?

    FastGettext.add_text_domain(DEFAULT_TEXT_DOMAIN,
                                type: :chain,
                                chain: [],
                                report_warning: false)
    FastGettext.default_text_domain = DEFAULT_TEXT_DOMAIN

    load_translations('puppet', puppet_locale_path, translation_mode(puppet_locale_path), DEFAULT_TEXT_DOMAIN)
  end

  # @api private
  # Switches the active text domain, if the requested domain exists.
  # @param [String, Symbol] domain_name the name of the domain to switch to
  def self.use_text_domain(domain_name)
    return if @gettext_disabled || !gettext_loaded?
    domain_name = domain_name.to_sym

    if FastGettext.translation_repositories.include?(domain_name)
      Puppet.debug "Use text domain #{domain_name.inspect}"
      FastGettext.text_domain = domain_name
    else
      Puppet.debug "Requested unknown text domain #{domain_name.inspect}"
    end
  end

  # @api private
  # Delete all text domains.
  def self.delete_all_text_domains
    FastGettext.translation_repositories.clear
    FastGettext.default_text_domain = nil
    FastGettext.text_domain = nil
  end

  # @api private
  # Deletes the text domain with the given name
  # @param [String, Symbol] domain_name the name of the domain to delete
  def self.delete_text_domain(domain_name)
    return if @gettext_disabled || !gettext_loaded?
    domain_name = domain_name.to_sym

    deleted = FastGettext.translation_repositories.delete(domain_name)
    if FastGettext.text_domain == domain_name
      Puppet.debug "Deleted current text domain #{domain_name.inspect}: #{!deleted.nil?}"
      FastGettext.text_domain = nil
    else
      Puppet.debug "Deleted text domain #{domain_name.inspect}: #{!deleted.nil?}"
    end
  end

  # @api private
  # Deletes all text domains except the default one
  def self.delete_environment_text_domains
    return if @gettext_disabled || !gettext_loaded?

    FastGettext.translation_repositories.keys.each do |key|
      # do not clear default translations
      next if key == DEFAULT_TEXT_DOMAIN

      FastGettext.translation_repositories.delete(key)
    end
    FastGettext.text_domain = nil
  end

  # @api private
  # Adds translations from the default text domain to the specified
  # text domain. Creates the default text domain if one does not exist
  # (this will load Puppet's translations).
  #
  # Since we are currently (Nov 2017) vendoring semantic_puppet, in normal
  # flows these translations will be copied along with Puppet's.
  #
  # @param [Symbol] domain_name the name of the domain to add translations to
  def self.copy_default_translations(domain_name)
    return if @gettext_disabled || !gettext_loaded?

    if FastGettext.default_text_domain.nil?
      create_default_text_domain
    end

    puppet_translations = FastGettext.translation_repositories[FastGettext.default_text_domain].chain
    FastGettext.translation_repositories[domain_name].chain.push(*puppet_translations)
  end

  # @api private
  # Search for puppet gettext config files
  # @return [String] path to the config, or nil if not found
  def self.puppet_locale_path
    if Puppet::FileSystem.exist?(LOCAL_PATH)
      return LOCAL_PATH
    elsif Puppet::Util::Platform.windows? && Puppet::FileSystem.exist?(WINDOWS_PATH)
      return WINDOWS_PATH
    elsif !Puppet::Util::Platform.windows? && Puppet::FileSystem.exist?(POSIX_PATH)
      return POSIX_PATH
    else
      nil
    end
  end

  # @api private
  # Determine which translation file format to use
  # @param [String] conf_path the path to the gettext config file
  # @return [Symbol] :mo if in a package structure, :po otherwise
  def self.translation_mode(conf_path)
    if WINDOWS_PATH == conf_path || POSIX_PATH == conf_path
      return :mo
    else
      return :po
    end
  end

  # @api private
  # Prevent future gettext initializations
  def self.disable_gettext
    @gettext_disabled = true
  end

  # @api private
  # Attempt to load translations for the given project.
  # @param [String] project_name the project whose translations we want to load
  # @param [String] locale_dir the path to the directory containing translations
  # @param [Symbol] file_format translation file format to use, either :po or :mo
  # @return true if initialization succeeded, false otherwise
  def self.load_translations(project_name, locale_dir, file_format, text_domain = FastGettext.text_domain)
    if project_name.nil? || project_name.empty?
      raise Puppet::Error, "A project name must be specified in order to initialize translations."
    end

    return false if @gettext_disabled || !@gettext_loaded

    return false unless locale_dir && Puppet::FileSystem.exist?(locale_dir)

    unless file_format == :po || file_format == :mo
      raise Puppet::Error, "Unsupported translation file format #{file_format}; please use :po or :mo"
    end

    add_repository_to_domain(project_name, locale_dir, file_format, text_domain)
    return true
  end

  # @api private
  # Add the translations for this project to the domain's repository chain
  # chain for the currently selected text domain, if needed.
  # @param [String] project_name the name of the project for which to load translations
  # @param [String] locale_dir the path to the directory containing translations
  # @param [Symbol] file_format the format of the translations files, :po or :mo
  def self.add_repository_to_domain(project_name, locale_dir, file_format, text_domain = FastGettext.text_domain)
    return if @gettext_disabled || !gettext_loaded?

    current_chain = FastGettext.translation_repositories[text_domain].chain

    repository = FastGettext::TranslationRepository.build(project_name,
                                                          path: locale_dir,
                                                          type: file_format,
                                                          report_warning: false)
    current_chain << repository
  end

  # @api private
  # Sets FastGettext's locale to the current system locale
  def self.setup_locale
    return if @gettext_disabled || !gettext_loaded?

    set_locale(Locale.current.language)
  end

  # @api private
  # Sets the language in which to display strings.
  # @param [String] locale the language portion of a locale string (e.g. "ja")
  def self.set_locale(locale)
    return if @gettext_disabled || !gettext_loaded?
    # make sure we're not using the `available_locales` machinery
    FastGettext.default_available_locales = nil

    FastGettext.default_locale = locale
  end
end
