#
# 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/mixin/deep_merge'
require 'chef/log'

class Chef
  class Node
    class Attribute
      HIDDEN_ATTRIBUES = [:@override, :@attribute, :@default, :@normal, :@automatic]

      attr_accessor :normal,
                    :default,
                    :override,
                    :automatic,
                    :current_normal,
                    :current_default,
                    :current_override,
                    :current_automatic,
                    :auto_vivifiy_on_read,
                    :set_unless_value_present,
                    :set_type

      include Enumerable

      def initialize(normal, default, override, automatic, state=[])
        @normal = normal
        @current_normal = normal
        @default = default
        @current_default = default
        @override = override
        @current_override = override
        @automatic = automatic
        @current_automatic = automatic
        @current_nesting_level = state
        @auto_vivifiy_on_read = false
        @set_unless_value_present = false
        @set_type = nil
        @has_been_read = false
      end

      def attribute
        normal
      end

      def attribute=(value)
        normal = value
      end

      def set_type_hash
        case @set_type
        when :normal
          @normal
        when :override
          @override
        when :default
          @default
        when :automatic
          @automatic
        end
      end

      # Reset our internal current_nesting_level to the top of every tree
      def reset
        @current_normal = @normal
        @current_default = @default
        @current_override = @override
        @current_automatic = @automatic
        @has_been_read = false
        @current_nesting_level = []
      end

      def [](key)
        @current_nesting_level << key

        # We set this to so that we can cope with ||= as a setting.
        # See the comments in []= for more details.
        @has_been_read = true

        # If we have a set type, our destiny is to write
        if @set_type
          a_value = @set_type == :automatic ? value_or_descend(current_automatic, key, auto_vivifiy_on_read) : nil
          o_value = @set_type == :override ? value_or_descend(current_override, key, auto_vivifiy_on_read) : nil
          n_value = @set_type == :normal ? value_or_descend(current_normal, key, auto_vivifiy_on_read) : nil
          d_value = @set_type == :default ? value_or_descend(current_default, key, auto_vivifiy_on_read) : nil

          determine_value(a_value, o_value, n_value, d_value)
        # Our destiny is only to read, so we get the full list.
        else
          a_value = value_or_descend(current_automatic, key)
          o_value = value_or_descend(current_override, key)
          n_value = value_or_descend(current_normal, key)
          d_value = value_or_descend(current_default, key)

          determine_value(a_value, o_value, n_value, d_value)
        end
      end

      def has_key?(key)
        return true if component_has_key?(@default,key)
        return true if component_has_key?(@automatic,key)
        return true if component_has_key?(@normal,key)
        return true if component_has_key?(@override,key)
        false
      end

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

      def each(&block)
        get_keys.each do |key|
          value = determine_value(
            get_value(automatic, key),
            get_value(override, key),
            get_value(normal, key),
            get_value(default, key)
          )
          block.call([key, value])
        end
      end

      def each_pair(&block)
        get_keys.each do |key|
          value = determine_value(
            get_value(automatic, key),
            get_value(override, key),
            get_value(normal, key),
            get_value(default, key)
          )
          block.call(key, value)
        end
      end

      def each_attribute(&block)
        get_keys.each do |key|
          value = determine_value(
            get_value(automatic, key),
            get_value(override, key),
            get_value(normal, key),
            get_value(default, key)
          )
          block.call(key, value)
        end
      end

      def each_key(&block)
        get_keys.each do |key|
          block.call(key)
        end
      end

      def each_value(&block)
        get_keys.each do |key|
          value = determine_value(
            get_value(automatic, key),
            get_value(override, key),
            get_value(normal, key),
            get_value(default, key)
          )
          block.call(value)
        end
      end

      def empty?
        get_keys.empty?
      end

      def fetch(key, default_value=nil, &block)
        if get_keys.include? key
          determine_value(
            get_value(automatic, key),
            get_value(override, key),
            get_value(normal, key),
            get_value(default, key)
          )
        elsif default_value
          default_value
        elsif block_given?
          block.call(key)
        else
          raise IndexError, "Key #{key} does not exist"
        end
      end

      # Writing this method hurts me a little bit.
      #
      # TODO: Refactor all this stuff so this kind of horror is no longer needed
      #
      # We have invented a new kind of duck-typing, we call it Madoff typing.
      # We just lie and hope we die before you recognize our scheme. :)
      def kind_of?(klass)
        if klass == Hash || klass == Mash || klass == Chef::Node::Attribute
          true
        else
          false
        end
      end

      def has_value?(value)
        self.any? do |k,v|
          value == v
        end
      end

      alias :value? :has_value?

      def index(value)
        index = self.find do |h|
          value == h[1]
        end
        index.first if index.is_a? Array || nil
      end

      def values
        self.collect { |h| h[1] }
      end

      def size
        self.collect{}.length
      end

      alias :length :size

      def get_keys
        keys
      end

      def keys
        tkeys = current_automatic ? current_automatic.keys : []
        [ current_override, current_normal, current_default ].each do |attr_hash|
          if attr_hash
            attr_hash.keys.each do |key|
              tkeys << key unless tkeys.include?(key)
            end
          end
        end
        tkeys
      end

      def get_value(data_hash, key)
        last = nil

        if @current_nesting_level.length == 0
          if data_hash.has_key?(key) && ! data_hash[key].nil?
            return data_hash[key]
          else
            return nil
          end
        end

        0.upto(@current_nesting_level.length) do |i|
          if i == 0
            last = auto_vivifiy(data_hash, @current_nesting_level[i])
          elsif i == @current_nesting_level.length
            fk = last[@current_nesting_level[i - 1]]
            if fk.has_key?(key) && ! fk[key].nil?
              return fk[key]
            else
              return nil
            end
          else
            last = auto_vivifiy(last[@current_nesting_level[i - 1]], @current_nesting_level[i])
          end
        end
      end

      def hash_and_not_cna?(to_check)
        (! to_check.kind_of?(Chef::Node::Attribute)) && to_check.respond_to?(:has_key?)
      end

      def determine_value(a_value, o_value, n_value, d_value)
        if hash_and_not_cna?(a_value)
          value = {}
          value = Chef::Mixin::DeepMerge.merge(value, d_value) if hash_and_not_cna?(d_value)
          value = Chef::Mixin::DeepMerge.merge(value, n_value) if hash_and_not_cna?(n_value)
          value = Chef::Mixin::DeepMerge.merge(value, o_value) if hash_and_not_cna?(o_value)
          value = Chef::Mixin::DeepMerge.merge(value, a_value)
          value
        elsif hash_and_not_cna?(o_value)
          value = {}
          value = Chef::Mixin::DeepMerge.merge(value, d_value) if hash_and_not_cna?(d_value)
          value = Chef::Mixin::DeepMerge.merge(value, n_value) if hash_and_not_cna?(n_value)
          value = Chef::Mixin::DeepMerge.merge(value, o_value)
          value
        elsif hash_and_not_cna?(n_value)
          value = {}
          value = Chef::Mixin::DeepMerge.merge(value, d_value) if hash_and_not_cna?(d_value)
          value = Chef::Mixin::DeepMerge.merge(value, n_value)
          value
        elsif hash_and_not_cna?(d_value)
          d_value
        else
          return a_value if ! a_value.nil?
          return o_value if ! o_value.nil?
          return n_value if ! n_value.nil?
          return d_value if ! d_value.nil?
          return nil
        end
      end

      def []=(key, value)
        # If we don't have one, then we'll pretend we're normal
        if @set_type.nil?
          @set_type = :normal
          warning=<<-W
Setting attributes without specifying a precedence is deprecated and will be
removed in Chef 11.0. To set attributes at normal precedence, change code like:
`node["key"] = "value"` # Not this
to:
`node.set["key"] = "value"` # This

Called from:
W

          warning_with_line_nrs = caller[0...3].inject(warning) do |msg, source_line|
            msg << "  #{source_line}\n"
          end

          Chef::Log.warn(warning_with_line_nrs) if Chef::Config[:chef11_deprecation_warnings]
        end

        if set_unless_value_present
          if get_value(set_type_hash, key) != nil
            Chef::Log.debug("Not setting #{@current_nesting_level.join("/")}/#{key} to #{value.inspect} because it has a #{@set_type} value already")
            return false
          end
        end

        # If we have been read, and the key we are writing is the same
        # as our parent, we have most like been ||='ed.  So we need to
        # just rewind a bit.
        #
        # In practice, these objects are single use - this is just
        # supporting one more single-use style.
        @current_nesting_level.pop if @has_been_read && @current_nesting_level.last == key

        set_value(set_type_hash, key, value)
        value
      end

      def set_value(data_hash, key, value)
        last = nil

        # If there is no current_nesting_level, just set the value
        if @current_nesting_level.length == 0
          data_hash[key] = value
          return data_hash
        end

        # Walk all the previous places we have been
        0.upto(@current_nesting_level.length) do |i|
          # If we are the first, we are top level, and should vivifiy the data_hash
          if i == 0
            last = auto_vivifiy(data_hash, @current_nesting_level[i])
          # If we are one past the last current_nesting_level, we are adding a key to that hash with a value
          elsif i == @current_nesting_level.length
            last[@current_nesting_level[i - 1]][key] = value
          # Otherwise, we're auto-vivifiy-ing an interim mash
          else
            last = auto_vivifiy(last[@current_nesting_level[i - 1]], @current_nesting_level[i])
          end
        end
        data_hash
      end

      def auto_vivifiy_on_read?
        auto_vivifiy_on_read
      end

      def auto_vivifiy(data_hash, key)
        if data_hash.has_key?(key)
          unless data_hash[key].respond_to?(:has_key?)
            raise ArgumentError, "You tried to set a nested key, where the parent is not a hash-like object: #{@current_nesting_level.join("/")}/#{key} " unless auto_vivifiy_on_read
          end
        else
          data_hash[key] = Mash.new
        end
        data_hash
      end

      def value_or_descend(data_hash, key, auto_vivifiy=false)
        if auto_vivifiy
          hash_to_vivifiy = auto_vivifiy(data_hash, key)
          data_hash[key] = hash_to_vivifiy[key]
        else
          return nil if data_hash == nil
          return nil unless data_hash.has_key?(key)
        end

        if data_hash[key].respond_to?(:has_key?)
          cna = Chef::Node::Attribute.new(@normal, @default, @override, @automatic, @current_nesting_level)
          cna.current_normal = current_normal.nil? ? Mash.new : current_normal[key]
          cna.current_default   = current_default.nil? ? Mash.new : current_default[key]
          cna.current_override  = current_override.nil? ? Mash.new : current_override[key]
          cna.current_automatic  = current_automatic.nil? ? Mash.new : current_automatic[key]
          cna.auto_vivifiy_on_read = auto_vivifiy_on_read
          cna.set_unless_value_present = set_unless_value_present
          cna.set_type = set_type
          cna
        else
          data_hash[key]
        end
      end

      # Fetches or sets the value, depending on if any arguments are given.
      # ==== Fetching
      # If no arguments are given, fetches the value:
      #   node.network
      #   => {network data}
      # Getters will find either a string or symbol key.
      # ==== Setting
      # If arguments are given, a value will be set. Both normal setter and DSL
      # style setters are allowed:
      #   node.foo = "bar"
      #   node.foo("bar")
      # Both set node[:foo] = "bar"
      def method_missing(symbol, *args)
        if args.empty?
          if key?(symbol)
            self[symbol]
          elsif key?(symbol.to_s)
            self[symbol.to_s]
          elsif auto_vivifiy_on_read?
            self[symbol] = Mash.new
            self[symbol]
          else
            raise ArgumentError, "Attribute #{symbol} is not defined!" unless auto_vivifiy_on_read
          end
        else
          key_to_set = symbol.to_s[/^(.+)=$/, 1] || symbol
          self[key_to_set] = (args.length == 1 ? args[0] : args)
        end
      end

      def inspect
        determine_value(current_automatic, current_override, current_normal, current_default)

        "#<#{self.class} " << instance_variables.map{|iv|
          iv.to_s + '=' + (HIDDEN_ATTRIBUES.include?(iv.to_sym) ? "{...}" : instance_variable_get(iv).inspect)
        }.join(', ') << ">"
      end

      def to_hash
        result = determine_value(current_automatic, current_override, current_normal, current_default)
        if result.class == Hash
          result
        else
          result.to_hash
        end
      end

      def delete(key)
        [@automatic, @override, @normal, @default].inject(nil) do |return_value, attrs|
          deleted_value = delete_from_component(attrs, key)
          return_value || deleted_value
        end
      end

      def delete_from_component(component_attrs, key)
        # get the Hash-like object at the current nesting level:
        nested_attrs = value_at_current_nesting(component_attrs, key)

        if nested_attrs.respond_to?(:delete)
          nested_attrs.delete(key)
        else
          nil
        end
      end

      def component_has_key?(component_attrs,key)
        # get the Hash-like object at the current nesting level:
        nested_attrs = value_at_current_nesting(component_attrs, key)
        nested_attrs.respond_to?(:key?) && nested_attrs.key?(key)
      end

      def value_at_current_nesting(component_attrs, key)
        @current_nesting_level.inject(component_attrs) do |subtree, intermediate_key|
          # if the intermediate value isn't a hash or doesn't have the intermediate key,
          # it can't have the bottom-level key we're looking for.
          (subtree.respond_to?(:key?) && subtree[intermediate_key]) or (return false)
        end
      end

    end
  end
end
