#
# Author:: Adam Jacob (<adam@chef.io>)
# Author:: Daniel DeLeo (<dan@chef.io>)
# Copyright:: Copyright (c) Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require_relative "mash"
require_relative "exception"
require_relative "mixin/os"
require_relative "dsl"

module Ohai
  class ProvidesMap

    attr_reader :map

    def initialize
      @map = Mash.new
    end

    # @param [Ohai::DSL::Plugin] plugin
    # @param [Array] provided_attributes
    #
    # @return void
    #
    def set_providers_for(plugin, provided_attributes)
      unless plugin.is_a?(Ohai::DSL::Plugin)
        raise ArgumentError, "set_providers_for only accepts Ohai Plugin classes (got: #{plugin})"
      end

      provided_attributes.each do |attribute|
        attrs = @map
        parts = normalize_and_validate(attribute)
        parts.each do |part|
          attrs[part] ||= Mash.new
          attrs = attrs[part]
        end
        attrs[:_plugins] ||= []
        attrs[:_plugins] << plugin
      end
    end

    #
    # gather plugins providing exactly the attributes listed
    #
    # @param [Array] attributes
    #
    # @return [Array] plugin names
    #
    def find_providers_for(attributes)
      plugins = []
      attributes.each do |attribute|
        attrs = select_subtree(@map, attribute)
        raise Ohai::Exceptions::AttributeNotFound, "No such attribute: \'#{attribute}\'" unless attrs
        raise Ohai::Exceptions::ProviderNotFound, "Cannot find plugin providing attribute: \'#{attribute}\'" unless attrs[:_plugins]

        plugins += attrs[:_plugins]
      end
      plugins.uniq
    end

    # This function is used to fetch the plugins for the attributes specified
    # in the CLI options to Ohai.
    # It first attempts to find the plugins for the attributes
    # or the sub attributes given.
    # If it can't find any, it looks for plugins that might
    # provide the parents of a given attribute and returns the
    # first parent found.
    #
    # @param [Array] attributes
    #
    # @return [Array] plugin names
    def deep_find_providers_for(attributes)
      plugins = []
      attributes.each do |attribute|
        attrs = select_subtree(@map, attribute)

        unless attrs
          attrs = select_closest_subtree(@map, attribute)

          unless attrs
            raise Ohai::Exceptions::AttributeNotFound, "No such attribute: \'#{attribute}\'"
          end
        end

        collect_plugins_in(attrs, plugins)
      end

      plugins.uniq
    end

    # This function is used to fetch the plugins from
    # 'depends "languages"' statements in plugins.
    # It gathers plugins providing each of the attributes listed, or the
    # plugins providing the closest parent attribute
    #
    # @param [Array] attributes
    #
    # @return [Array] plugin names
    def find_closest_providers_for(attributes)
      plugins = []
      attributes.each do |attribute|
        parts = normalize_and_validate(attribute)
        raise Ohai::Exceptions::AttributeNotFound, "No such attribute: \'#{attribute}\'" unless @map[parts[0]]

        attrs = select_closest_subtree(@map, attribute)
        raise Ohai::Exceptions::ProviderNotFound, "Cannot find plugin providing attribute: \'#{attribute}\'" unless attrs

        plugins += attrs[:_plugins]
      end
      plugins.uniq
    end

    def all_plugins(attribute_filter = nil)
      if attribute_filter.nil?
        collected = []
        collect_plugins_in(map, collected).uniq
      else
        deep_find_providers_for(Array(attribute_filter))
      end
    end

    private

    def normalize_and_validate(attribute)
      raise Ohai::Exceptions::AttributeSyntaxError, "Attribute contains duplicate '/' characters: #{attribute}" if %r{//+}.match?(attribute)
      raise Ohai::Exceptions::AttributeSyntaxError, "Attribute contains a trailing '/': #{attribute}" if %r{/$}.match?(attribute)

      parts = attribute.split("/")
      parts.shift if parts.length != 0 && parts[0].length == 0 # attribute begins with a '/'
      parts
    end

    def select_subtree(provides_map, attribute)
      subtree = provides_map
      parts = normalize_and_validate(attribute)
      parts.each do |part|
        return nil unless subtree[part]

        subtree = subtree[part]
      end
      subtree
    end

    def select_closest_subtree(provides_map, attribute)
      attr, *rest = normalize_and_validate(attribute)

      # return nil if the top-level part of the attribute is not a
      # top-level key in the provides_map (can't search any lower, and
      # no information to return from this level of the search)
      return nil unless provides_map[attr]

      # attr is a key in the provides_map, search for the sub
      # attribute under attr (if attribute = attr/sub1/sub2 then we
      # search provides_map[attr] for sub1/sub2)
      unless rest.empty?
        subtree = select_closest_subtree(provides_map[attr], rest.join("/"))
      end

      if subtree.nil?
        # no subtree of provides_map[attr] either 1) has a
        # subattribute, 2) has a plugin providing a subattribute.
        unless provides_map[attr][:_plugins]
          # no providers for this attribute, this subtree won't do.
          return nil # no providers for this attribute
        else
          # there are providers for this attribute, return its subtree
          # to indicate this is the closest subtree
          return provides_map[attr]
        end
      end

      # we've already found a closest subtree or we've search all
      # parent attributes of the original attribute and found no
      # providers (subtree is nil in this case)
      subtree
    end

    # Takes a section of the map, recursively searches for a `_plugins` key
    # to find all the plugins in that section of the map. If given the whole
    # map, it will find all of the plugins that have at least one provided
    # attribute.
    def collect_plugins_in(provides_map, collected)
      provides_map.each_key do |plugin|
        if plugin.eql?("_plugins")
          collected.concat(provides_map[plugin])
        else
          collect_plugins_in(provides_map[plugin], collected)
        end
      end
      collected
    end
  end
end