#--
# Author:: Adam Jacob (<adam@chef.io>)
# Author:: AJ Christensen (<aj@chef.io>)
# Copyright:: Copyright 2008-2016, 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 "chef/node/immutable_collections"
require "chef/node/attribute_collections"
require "chef/mixin/deep_merge"
require "chef/log"

class Chef
  class Node

    # == Attribute
    # Attribute implements a nested key-value (Hash) and flat collection
    # (Array) data structure supporting multiple levels of precedence, such
    # that a given key may have multiple values internally, but will only
    # return the highest precedence value when reading.
    class Attribute < Mash

      include Immutablize

      include Enumerable

      # List of the component attribute hashes, in order of precedence, low to
      # high.
      COMPONENTS = [
        :@default,
        :@env_default,
        :@role_default,
        :@force_default,
        :@normal,
        :@override,
        :@role_override,
        :@env_override,
        :@force_override,
        :@automatic,
      ].freeze

      DEFAULT_COMPONENTS = [
        :@default,
        :@env_default,
        :@role_default,
        :@force_default,
      ]

      OVERRIDE_COMPONENTS = [
        :@override,
        :@role_override,
        :@env_override,
        :@force_override,
      ]

      [:all?,
       :any?,
       :assoc,
       :chunk,
       :collect,
       :collect_concat,
       :compare_by_identity,
       :compare_by_identity?,
       :count,
       :cycle,
       :detect,
       :drop,
       :drop_while,
       :each,
       :each_cons,
       :each_entry,
       :each_key,
       :each_pair,
       :each_slice,
       :each_value,
       :each_with_index,
       :each_with_object,
       :empty?,
       :entries,
       :except,
       :fetch,
       :find,
       :find_all,
       :find_index,
       :first,
       :flat_map,
       :flatten,
       :grep,
       :group_by,
       :has_value?,
       :include?,
       :index,
       :inject,
       :invert,
       :key,
       :keys,
       :length,
       :map,
       :max,
       :max_by,
       :merge,
       :min,
       :min_by,
       :minmax,
       :minmax_by,
       :none?,
       :one?,
       :partition,
       :rassoc,
       :reduce,
       :reject,
       :reverse_each,
       :select,
       :size,
       :slice_before,
       :sort,
       :sort_by,
       :store,
       :symbolize_keys,
       :take,
       :take_while,
       :to_a,
       :to_hash,
       :to_set,
       :value?,
       :values,
       :values_at,
       :zip].each do |delegated_method|
         define_method(delegated_method) do |*args, &block|
           merged_attributes.send(delegated_method, *args, &block)
         end
       end

       # return the cookbook level default attribute component
      attr_reader :default

       # return the role level default attribute component
      attr_reader :role_default

       # return the environment level default attribute component
      attr_reader :env_default

       # return the force_default level attribute component
      attr_reader :force_default

       # return the "normal" level attribute component
      attr_reader :normal

       # return the cookbook level override attribute component
      attr_reader :override

       # return the role level override attribute component
      attr_reader :role_override

       # return the enviroment level override attribute component
      attr_reader :env_override

       # return the force override level attribute component
      attr_reader :force_override

       # return the automatic level attribute component
      attr_reader :automatic

       # This is used to track the top level key as we descend through method chaining into
       # a precedence level (e.g. node.default['foo']['bar']['baz']= results in 'foo' here).  We
       # need this so that when we hit the end of a method chain which results in a mutator method
       # that we can invalidate the whole top-level deep merge cache for the top-level key.  It is
       # the responsibility of the accessor on the Chef::Node object to reset this to nil, and then
       # the first VividMash#[] call can ||= and set this to the first key we encounter.
      attr_accessor :top_level_breadcrumb

       # Cache of deep merged values by top-level key.  This is a simple hash which has keys that are the
       # top-level keys of the node object, and we save the computed deep-merge for that key here.  There is
       # no cache of subtrees.
      attr_accessor :deep_merge_cache

      def initialize(normal, default, override, automatic)
        @set_unless_present = false

        @default = VividMash.new(self, default)
        @env_default = VividMash.new(self, {})
        @role_default = VividMash.new(self, {})
        @force_default = VividMash.new(self, {})

        @normal = VividMash.new(self, normal)

        @override = VividMash.new(self, override)
        @role_override = VividMash.new(self, {})
        @env_override = VividMash.new(self, {})
        @force_override = VividMash.new(self, {})

        @automatic = VividMash.new(self, automatic)

        @merged_attributes = nil
        @combined_override = nil
        @combined_default = nil
        @top_level_breadcrumb = nil
        @deep_merge_cache = {}
      end

       # Debug what's going on with an attribute. +args+ is a path spec to the
       # attribute you're interested in. For example, to debug where the value
       # of `node[:network][:default_interface]` is coming from, use:
       #   debug_value(:network, :default_interface).
       # The return value is an Array of Arrays. The first element is
       # `["set_unless_enabled?", Boolean]`, which describes whether the
       # attribute collection is in "set_unless" mode. The rest of the Arrays
       # are pairs of `["precedence_level", value]`, where precedence level is
       # the component, such as role default, normal, etc. and value is the
       # attribute value set at that precedence level. If there is no value at
       # that precedence level, +value+ will be the symbol +:not_present+.
      def debug_value(*args)
        components = COMPONENTS.map do |component|
          ivar = instance_variable_get(component)
          value = args.inject(ivar) do |so_far, key|
            if so_far == :not_present
              :not_present
            elsif so_far.has_key?(key)
              so_far[key]
            else
              :not_present
            end
          end
          [component.to_s.sub(/^@/, ""), value]
        end
        [["set_unless_enabled?", @set_unless_present]] + components
      end

       # Enables or disables `||=`-like attribute setting. See, e.g., Node#set_unless
      def set_unless_value_present=(setting)
        @set_unless_present = setting
      end

       # Invalidate a key in the deep_merge_cache.  If called with nil, or no arg, this will invalidate
       # the entire deep_merge cache.  In the case of the user doing node.default['foo']['bar']['baz']=
       # that eventually results in a call to reset_cache('foo') here.  A node.default=hash_thing call
       # must invalidate the entire cache and re-deep-merge the entire node object.
      def reset_cache(path = nil)
        if path.nil?
          @deep_merge_cache = {}
        else
          deep_merge_cache.delete(path.to_s)
        end
      end

      alias :reset :reset_cache

       # Set the cookbook level default attribute component to +new_data+.
      def default=(new_data)
        reset
        @default = VividMash.new(self, new_data)
      end

       # Set the role level default attribute component to +new_data+
      def role_default=(new_data)
        reset
        @role_default = VividMash.new(self, new_data)
      end

       # Set the environment level default attribute component to +new_data+
      def env_default=(new_data)
        reset
        @env_default = VividMash.new(self, new_data)
      end

       # Set the force_default (+default!+) level attributes to +new_data+
      def force_default=(new_data)
        reset
        @force_default = VividMash.new(self, new_data)
      end

       # Set the normal level attribute component to +new_data+
      def normal=(new_data)
        reset
        @normal = VividMash.new(self, new_data)
      end

       # Set the cookbook level override attribute component to +new_data+
      def override=(new_data)
        reset
        @override = VividMash.new(self, new_data)
      end

       # Set the role level override attribute component to +new_data+
      def role_override=(new_data)
        reset
        @role_override = VividMash.new(self, new_data)
      end

       # Set the environment level override attribute component to +new_data+
      def env_override=(new_data)
        reset
        @env_override = VividMash.new(self, new_data)
      end

      def force_override=(new_data)
        reset
        @force_override = VividMash.new(self, new_data)
      end

      def automatic=(new_data)
        reset
        @automatic = VividMash.new(self, new_data)
      end

       #
       # Deleting attributes
       #

       # clears attributes from all precedence levels
      def rm(*args)
        reset(args[0])
        # just easier to compute our retval, rather than collect+merge sub-retvals
        ret = args.inject(merged_attributes) do |attr, arg|
          if attr.nil? || !attr.respond_to?(:[])
            nil
          else
            begin
              attr[arg]
            rescue TypeError
              raise TypeError, "Wrong type in index of attribute (did you use a Hash index on an Array?)"
            end
          end
        end
        rm_default(*args)
        rm_normal(*args)
        rm_override(*args)
        ret
      end

       # does <level>['foo']['bar'].delete('baz')
      def remove_from_precedence_level(level, *args, key)
        multimash = level.element(*args)
        multimash.nil? ? nil : multimash.delete(key)
      end

      private :remove_from_precedence_level

       # clears attributes from all default precedence levels
       #
       # equivalent to: force_default!['foo']['bar'].delete('baz')
      def rm_default(*args)
        reset(args[0])
        remove_from_precedence_level(force_default!(autovivify: false), *args)
      end

       # clears attributes from normal precedence
       #
       # equivalent to: normal!['foo']['bar'].delete('baz')
      def rm_normal(*args)
        reset(args[0])
        remove_from_precedence_level(normal!(autovivify: false), *args)
      end

       # clears attributes from all override precedence levels
       #
       # equivalent to: force_override!['foo']['bar'].delete('baz')
      def rm_override(*args)
        reset(args[0])
        remove_from_precedence_level(force_override!(autovivify: false), *args)
      end

       #
       # Replacing attributes without merging
       #

       # sets default attributes without merging
      def default!(opts = {})
        # FIXME: do not flush whole cache
        reset
        MultiMash.new(self, @default, [], opts)
      end

       # sets normal attributes without merging
      def normal!(opts = {})
        # FIXME: do not flush whole cache
        reset
        MultiMash.new(self, @normal, [], opts)
      end

       # sets override attributes without merging
      def override!(opts = {})
        # FIXME: do not flush whole cache
        reset
        MultiMash.new(self, @override, [], opts)
      end

       # clears from all default precedence levels and then sets force_default
      def force_default!(opts = {})
        # FIXME: do not flush whole cache
        reset
        MultiMash.new(self, @force_default, [@default, @env_default, @role_default], opts)
      end

       # clears from all override precedence levels and then sets force_override
      def force_override!(opts = {})
        # FIXME: do not flush whole cache
        reset
        MultiMash.new(self, @force_override, [@override, @env_override, @role_override], opts)
      end

       #
       # Accessing merged attributes.
       #
       # Note that merged_attributes('foo', 'bar', 'baz') can be called to compute only the
       # deep merge of node['foo']['bar']['baz'], but in practice we currently always compute
       # all of node['foo'] even if the user only requires node['foo']['bar']['baz'].
       #

      def merged_attributes(*path)
       # immutablize(
        merge_all(path)
       # )
      end

      def combined_override(*path)
        immutablize(merge_overrides(path))
      end

      def combined_default(*path)
        immutablize(merge_defaults(path))
      end

      def [](key)
        if deep_merge_cache.has_key?(key.to_s)
          # return the cache of the deep merged values by top-level key
          deep_merge_cache[key.to_s]
        else
          # save all the work of computing node[key]
          deep_merge_cache[key.to_s] = merged_attributes(key)
        end
      end

      def []=(key, value)
        raise Exceptions::ImmutableAttributeModification
      end

      def has_key?(key)
        COMPONENTS.any? do |component_ivar|
          instance_variable_get(component_ivar).has_key?(key)
        end
      end

      alias :attribute? :has_key?
      alias :member? :has_key?
      alias :include? :has_key?
      alias :key? :has_key?

      alias :each_attribute :each

      def method_missing(symbol, *args)
        if args.empty?
          if key?(symbol)
            self[symbol]
          else
            raise NoMethodError, "Undefined method or attribute `#{symbol}' on `node'"
          end
        elsif symbol.to_s =~ /=$/
          key_to_set = symbol.to_s[/^(.+)=$/, 1]
          self[key_to_set] = (args.length == 1 ? args[0] : args)
        else
          raise NoMethodError, "Undefined node attribute or method `#{symbol}' on `node'"
        end
      end

      def to_s
        merged_attributes.to_s
      end

      def inspect
        "#<#{self.class} " << (COMPONENTS + [:@merged_attributes, :@properties]).map {|iv|
          "#{iv}=#{instance_variable_get(iv).inspect}"
        }.join(", ") << ">"
      end

      def set_unless?
        @set_unless_present
      end

       private

       # Helper method for merge_all/merge_defaults/merge_overrides.
       #
       # apply_path(thing, [ "foo", "bar", "baz" ]) = thing["foo"]["bar"]["baz"]
       #
       # The path value can be nil in which case the whole component is returned.
       #
       # It returns nil (does not raise an exception) if it walks off the end of an Mash/Hash/Array, it does not
       # raise any TypeError if it attempts to apply a hash key to an Integer/String/TrueClass, and just returns
       # nil in that case.
       #
      def apply_path(component, path)
        path ||= []
        path.inject(component) do |val, path_arg|
          if val.respond_to?(:[])
            # Have an Array-like or Hash-like thing
            if !val.respond_to?(:has_key?)
              # Have an Array-like thing
              val[path_arg]
            elsif val.has_key?(path_arg)
              # Hash-like thing (must check has_key? first to protect against Autovivification)
              val[path_arg]
            else
              nil
            end
          else
            nil
          end
        end
      end

       # For elements like Fixnums, true, nil...
      def safe_dup(e)
        e.dup
      rescue TypeError
        e
      end

       # Deep merge all attribute levels using hash-only merging between different precidence
       # levels (so override arrays completely replace arrays set at any default level).
       #
       # The path allows for selectively deep-merging a subtree of the node object.
       #
       # @param path [Array] Array of args to method chain to descend into the node object
       # @return [attr] Deep Merged values (may be VividMash, Hash, Array, etc) from the node object
      def merge_all(path)
        components = [
          merge_defaults(path),
          apply_path(@normal, path),
          merge_overrides(path),
          apply_path(@automatic, path),
        ]

        components.map! do |component|
          safe_dup(component)
        end

        return nil if components.compact.empty?

        components.inject(ImmutableMash.new({})) do |merged, component|
          Chef::Mixin::DeepMerge.hash_only_merge!(merged, component)
        end
      end

       # Deep merge the default attribute levels with array merging.
       #
       # The path allows for selectively deep-merging a subtree of the node object.
       #
       # @param path [Array] Array of args to method chain to descend into the node object
       # @return [attr] Deep Merged values (may be VividMash, Hash, Array, etc) from the node object
      def merge_defaults(path)
        DEFAULT_COMPONENTS.inject(nil) do |merged, component_ivar|
          component_value = apply_path(instance_variable_get(component_ivar), path)
          Chef::Mixin::DeepMerge.deep_merge(component_value, merged)
        end
      end

       # Deep merge the override attribute levels with array merging.
       #
       # The path allows for selectively deep-merging a subtree of the node object.
       #
       # @param path [Array] Array of args to method chain to descend into the node object
       # @return [attr] Deep Merged values (may be VividMash, Hash, Array, etc) from the node object
      def merge_overrides(path)
        OVERRIDE_COMPONENTS.inject(nil) do |merged, component_ivar|
          component_value = apply_path(instance_variable_get(component_ivar), path)
          Chef::Mixin::DeepMerge.deep_merge(component_value, merged)
        end
      end

    end

  end
end
