#--
# Author:: Adam Jacob (<adam@opscode.com>)
# Author:: AJ Christensen (<aj@opscode.com>)
# Copyright:: Copyright (c) 2008 Opscode, 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
