# frozen_string_literal: true

module Puppet::Interface::FaceCollection
  @faces = Hash.new { |hash, key| hash[key] = {} }

  @loader = Puppet::Util::Autoload.new(:application, 'puppet/face')

  def self.faces
    unless @loaded
      @loaded = true
      names = @loader.files_to_load(Puppet.lookup(:current_environment)).map do |fn|
        ::File.basename(fn, '.rb')
      end.uniq
      names.each { |name| self[name, :current] }
    end
    @faces.keys.select { |name| @faces[name].length > 0 }
  end

  def self.[](name, version)
    name = underscorize(name)
    get_face(name, version) or load_face(name, version)
  end

  def self.get_action_for_face(name, action_name, version)
    name = underscorize(name)

    # If the version they request specifically doesn't exist, don't search
    # elsewhere.  Usually this will start from :current and all...
    face = self[name, version]
    return nil unless face

    action = face.get_action(action_name)
    unless action
      # ...we need to search for it bound to an o{lder,ther} version.  Since
      # we load all actions when the face is first references, this will be in
      # memory in the known set of versions of the face.
      (@faces[name].keys - [:current]).sort.reverse_each do |vers|
        action = @faces[name][vers].get_action(action_name)
        break if action
      end
    end

    return action
  end

  # get face from memory, without loading.
  def self.get_face(name, pattern)
    return nil unless @faces.has_key? name
    return @faces[name][:current] if pattern == :current

    versions = @faces[name].keys - [:current]
    range = pattern.is_a?(SemanticPuppet::Version) ? SemanticPuppet::VersionRange.new(pattern, pattern) : SemanticPuppet::VersionRange.parse(pattern)
    found = find_matching(range, versions)
    return @faces[name][found]
  end

  def self.find_matching(range, versions)
    versions.select { |v| range === v }.sort.last
  end

  # try to load the face, and return it.
  def self.load_face(name, version)
    # We always load the current version file; the common case is that we have
    # the expected version and any compatibility versions in the same file,
    # the default.  Which means that this is almost always the case.
    #
    # We use require to avoid executing the code multiple times, like any
    # other Ruby library that we might want to use.  --daniel 2011-04-06
    if safely_require name then
      # If we wanted :current, we need to index to find that; direct version
      # requests just work as they go. --daniel 2011-04-06
      if version == :current then
        # We need to find current out of this.  This is the largest version
        # number that doesn't have a dedicated on-disk file present; those
        # represent "experimental" versions of faces, which we don't fully
        # support yet.
        #
        # We walk the versions from highest to lowest and take the first version
        # that is not defined in an explicitly versioned file on disk as the
        # current version.
        #
        # This constrains us to only ship experimental versions with *one*
        # version in the file, not multiple, but given you can't reliably load
        # them except by side-effect when you ignore that rule this seems safe
        # enough...
        #
        # Given those constraints, and that we are not going to ship a versioned
        # interface that is not :current in this release, we are going to leave
        # these thoughts in place, and just punt on the actual versioning.
        #
        # When we upgrade the core to support multiple versions we can solve the
        # problems then; as lazy as possible.
        #
        # We do support multiple versions in the same file, though, so we sort
        # versions here and return the last item in that set.
        #
        # --daniel 2011-04-06
        latest_ver = @faces[name].keys.sort.last
        @faces[name][:current] = @faces[name][latest_ver]
      end
    end

    unless version == :current or get_face(name, version)
      # Try an obsolete version of the face, if needed, to see if that helps?
      safely_require name, version
    end

    return get_face(name, version)
  end

  def self.safely_require(name, version = nil)
    path = @loader.expand(version ? ::File.join(version.to_s, name.to_s) : name)
    require path
    true
  rescue LoadError => e
    raise unless e.message =~ %r{-- #{path}$}

    # ...guess we didn't find the file; return a much better problem.
    nil
  rescue SyntaxError => e
    raise unless e.message =~ %r{#{path}\.rb:\d+: }

    Puppet.err _("Failed to load face %{name}:\n%{detail}") % { name: name, detail: e }
    # ...but we just carry on after complaining.
    nil
  end

  def self.register(face)
    @faces[underscorize(face.name)][face.version] = face
  end

  def self.underscorize(name)
    unless name.to_s =~ /^[-_a-z][-_a-z0-9]*$/i then
      # TRANSLATORS 'face' refers to a programming API in Puppet
      raise ArgumentError, _("%{name} (%{class_name}) is not a valid face name") %
                           { name: name.inspect, class_name: name.class }
    end

    name.to_s.downcase.split(/[-_]/).join('_').to_sym
  end
end
