#--
# Author:: Adam Jacob (<adam@chef.io>)
# Author:: AJ Christensen (<aj@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 "mixin/deep_merge_cache"
require_relative "mixin/immutablize_hash"
require_relative "mixin/state_tracking"
require_relative "immutable_collections"
require_relative "attribute_collections"
require_relative "../decorator/unchain"
require_relative "../mixin/deep_merge"
require_relative "../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
      # FIXME:  what is include Enumerable doing up here, when down below we delegate
      # most of the Enumerable/Hash things to the underlying merged ImmutableHash.  That
      # is, in fact, the correct, thing to do, while including Enumerable to try to create
      # a hash-like API gets lots of things wrong because of the difference between the
      # Hash `each do |key, value|` vs the Array-like `each do |value|` API that Enumerable
      # expects.  This include should probably be deleted?
      include Enumerable

      include Chef::Node::Mixin::DeepMergeCache
      include Chef::Node::Mixin::StateTracking
      include Chef::Node::Mixin::ImmutablizeHash

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

      DEFAULT_COMPONENTS = %i{
        @default
        @env_default
        @role_default
        @force_default
      }.freeze

      OVERRIDE_COMPONENTS = %i{
        @override
        @role_override
        @env_override
        @force_override
      }.freeze

      ENUM_METHODS = %i{
        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_h
        to_hash
        to_json
        to_set
        to_yaml
        value?
        values
        values_at
        zip
      }.freeze

      ENUM_METHODS.each do |delegated_method|
        if Hash.public_method_defined?(delegated_method)
          define_method(delegated_method) do |*args, &block|
            merged_attributes.send(delegated_method, *args, &block)
          end
        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 environment 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

      def initialize(normal, default, override, automatic, node = nil)
        @default        = VividMash.new(default, self, node, :default)
        @env_default    = VividMash.new({}, self, node, :env_default)
        @role_default   = VividMash.new({}, self, node, :role_default)
        @force_default  = VividMash.new({}, self, node, :force_default)

        @normal = VividMash.new(normal, self, node, :normal)

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

        @automatic = VividMash.new(automatic, self, node, :automatic)

        super(nil, self, node, :merged)
      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 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.map do |component|
          value =
            begin
              instance_variable_get(component).read!(*args)
            rescue
              :not_present
            end
          [component.to_s.sub(/^@/, ""), value]
        end
      end

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

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

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

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

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

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

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

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

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

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

      #
      # Deleting attributes
      #

      # clears attributes from all precedence levels
      def rm(*args)
        with_deep_merged_return_value(self, *args) do
          rm_default(*args)
          rm_normal(*args)
          rm_override(*args)
        end
      end

      # clears attributes from all default precedence levels
      #
      # similar to: force_default!['foo']['bar'].delete('baz')
      # - does not autovivify
      # - does not trainwreck if interior keys do not exist
      def rm_default(*args)
        with_deep_merged_return_value(combined_default, *args) do
          default.unlink(*args)
          role_default.unlink(*args)
          env_default.unlink(*args)
          force_default.unlink(*args)
        end
      end

      # clears attributes from normal precedence
      #
      # equivalent to: normal!['foo']['bar'].delete('baz')
      # - does not autovivify
      # - does not trainwreck if interior keys do not exist
      def rm_normal(*args)
        normal.unlink(*args)
      end

      # clears attributes from all override precedence levels
      #
      # equivalent to: force_override!['foo']['bar'].delete('baz')
      # - does not autovivify
      # - does not trainwreck if interior keys do not exist
      def rm_override(*args)
        with_deep_merged_return_value(combined_override, *args) do
          override.unlink(*args)
          role_override.unlink(*args)
          env_override.unlink(*args)
          force_override.unlink(*args)
        end
      end

      def with_deep_merged_return_value(obj, *path, last)
        hash = obj.read(*path)
        return nil unless hash.is_a?(Hash)

        ret = hash[last]
        yield
        ret
      end

      private :with_deep_merged_return_value

      #
      # Replacing attributes without merging
      #

      # sets default attributes without merging
      #
      # - this API autovivifies (and cannot trainwreck)
      def default!(*args)
        return Decorator::Unchain.new(self, :default!) unless args.length > 0

        write(:default, *args)
      end

      # sets normal attributes without merging
      #
      # - this API autovivifies (and cannot trainwreck)
      def normal!(*args)
        return Decorator::Unchain.new(self, :normal!) unless args.length > 0

        write(:normal, *args)
      end

      # sets override attributes without merging
      #
      # - this API autovivifies (and cannot trainwreck)
      def override!(*args)
        return Decorator::Unchain.new(self, :override!) unless args.length > 0

        write(:override, *args)
      end

      # clears from all default precedence levels and then sets force_default
      #
      # - this API autovivifies (and cannot trainwreck)
      def force_default!(*args)
        return Decorator::Unchain.new(self, :force_default!) unless args.length > 0

        value = args.pop
        rm_default(*args)
        write(:force_default, *args, value)
      end

      # clears from all override precedence levels and then sets force_override
      def force_override!(*args)
        return Decorator::Unchain.new(self, :force_override!) unless args.length > 0

        value = args.pop
        rm_override(*args)
        write(:force_override, *args, value)
      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)
        merge_all(path)
      end

      def combined_override(*path)
        ret = merge_overrides(path)
        ret == NIL ? nil : ret
      end

      def combined_default(*path)
        ret = merge_defaults(path)
        ret == NIL ? nil : ret
      end

      def normal_unless(*args)
        return Decorator::Unchain.new(self, :normal_unless) unless args.length > 0

        write(:normal, *args) if normal.read(*args[0...-1]).nil?
      end

      def default_unless(*args)
        return Decorator::Unchain.new(self, :default_unless) unless args.length > 0

        write(:default, *args) if default.read(*args[0...-1]).nil?
      end

      def override_unless(*args)
        return Decorator::Unchain.new(self, :override_unless) unless args.length > 0

        write(:override, *args) if override.read(*args[0...-1]).nil?
      end

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

      # method-style access to attributes (has to come after the prepended ImmutablizeHash)

      def read(*path)
        if path[0].nil?
          Chef::Log.warn "Calling node.read() without any path argument is very slow, probably a bug, and should be avoided"
          merged_attributes.read(*path) # re-merges everything, slow edge case
        else
          self[path[0]] unless path[0].nil? # force deep_merge_cache key construction if necessary
          deep_merge_cache.read(*path)
        end
      end

      alias :dig :read

      def read!(*path)
        if path[0].nil?
          Chef::Log.warn "Calling node.read!() without any path argument is very slow, probably a bug, and should be avoided"
          merged_attributes.read!(*path) # re-merges everything, slow edge case
        else
          self[path[0]] unless path[0].nil? # force deep_merge_cache key construction if necessary
          deep_merge_cache.read!(*path)
        end
      end

      def exist?(*path)
        if path[0].nil?
          true
        else
          self[path[0]] unless path[0].nil? # force deep_merge_cache key construction if necessary
          deep_merge_cache.exist?(*path)
        end
      end

      def write(level, *args, &block)
        send(level).write(*args, &block)
      end

      def write!(level, *args, &block)
        send(level).write!(*args, &block)
      end

      def unlink(level, *path)
        send(level).unlink(*path)
      end

      def unlink!(level, *path)
        send(level).unlink!(*path)
      end

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

      alias :each_attribute :each

      def to_s
        merged_attributes.to_s
      end

      def inspect
        "#<#{self.class} " << (COMPONENTS + %i{@merged_attributes @properties}).map do |iv|
          "#{iv}=#{instance_variable_get(iv).inspect}"
        end.join(", ") << ">"
      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.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 precedence
      # 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),
        ]

        ret = components.inject(NIL) do |merged, component|
          component == NIL ? merged : hash_only_merge!(merged, component)
        end
        ret == NIL ? nil : ret
      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)
          component_value == NIL ? merged : deep_merge!(merged, component_value)
        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)
          component_value == NIL ? merged : deep_merge!(merged, component_value)
        end
      end

      # needed for __path__
      def convert_key(key)
        key.is_a?(Symbol) ? key.to_s : key
      end

      NIL = Object.new

      # @api private
      def deep_merge!(merge_onto, merge_with)
        # If there are two Hashes, recursively merge.
        if merge_onto.is_a?(Hash) && merge_with.is_a?(Hash)
          merge_with.each do |key, merge_with_value|
            value =
              if merge_onto.key?(key)
                deep_merge!(safe_dup(merge_onto.internal_get(key)), merge_with_value)
              else
                merge_with_value
              end

            # internal_set bypasses converting keys, does convert values and allows writing to immutable mashes
            merge_onto.internal_set(key, value)
          end
          merge_onto

        elsif merge_onto.is_a?(Array) && merge_with.is_a?(Array)
          merge_onto |= merge_with

        # In all other cases, replace merge_onto with merge_with
        else
          if merge_with.is_a?(Hash)
            Chef::Node::ImmutableMash.new(merge_with)
          elsif merge_with.is_a?(Array)
            Chef::Node::ImmutableArray.new(merge_with)
          else
            merge_with
          end
        end
      end

      # @api private
      def hash_only_merge!(merge_onto, merge_with)
        # If there are two Hashes, recursively merge.
        if merge_onto.is_a?(Hash) && merge_with.is_a?(Hash)
          merge_with.each do |key, merge_with_value|
            value =
              if merge_onto.key?(key)
                hash_only_merge!(safe_dup(merge_onto.internal_get(key)), merge_with_value)
              else
                merge_with_value
              end

            # internal_set bypasses converting keys, does convert values and allows writing to immutable mashes
            merge_onto.internal_set(key, value)
          end
          merge_onto

        # In all other cases, replace merge_onto with merge_with
        else
          if merge_with.is_a?(Hash)
            Chef::Node::ImmutableMash.new(merge_with)
          elsif merge_with.is_a?(Array)
            Chef::Node::ImmutableArray.new(merge_with)
          else
            merge_with
          end
        end
      end
    end
  end
end
