#
# Author:: Lamont Granquist (<lamont@chef.io>)
# Copyright:: Copyright (c) 2015 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.
#

# NOTE: this was extracted from the Recipe DSL mixin, relevant specs are in spec/unit/recipe_spec.rb

class Chef
  class ResourceBuilder
    attr_reader :type
    attr_reader :name
    attr_reader :created_at
    attr_reader :params
    attr_reader :run_context
    attr_reader :cookbook_name
    attr_reader :recipe_name
    attr_reader :enclosing_provider
    attr_reader :resource

    # FIXME (ruby-2.1 syntax): most of these are mandatory
    def initialize(type:nil, name:nil, created_at: nil, params: nil, run_context: nil, cookbook_name: nil, recipe_name: nil, enclosing_provider: nil)
      @type               = type
      @name               = name
      @created_at         = created_at
      @params             = params
      @run_context        = run_context
      @cookbook_name      = cookbook_name
      @recipe_name        = recipe_name
      @enclosing_provider = enclosing_provider
    end

    def build(&block)
      raise ArgumentError, "You must supply a name when declaring a #{type} resource" if name.nil?

      @resource = resource_class.new(name, run_context)
      resource.source_line = created_at
      resource.declared_type = type

      # If we have a resource like this one, we want to steal its state
      # This behavior is very counter-intuitive and should be removed.
      # See CHEF-3694, https://tickets.opscode.com/browse/CHEF-3694
      # Moved to this location to resolve CHEF-5052, https://tickets.opscode.com/browse/CHEF-5052
      if prior_resource
        resource.load_from(prior_resource)
      end

      resource.cookbook_name = cookbook_name
      resource.recipe_name = recipe_name
      # Determine whether this resource is being created in the context of an enclosing Provider
      resource.enclosing_provider = enclosing_provider

      # XXX: this is required for definition params inside of the scope of a
      # subresource to work correctly.
      resource.params = params

      # Evaluate resource attribute DSL
      resource.instance_eval(&block) if block_given?

      # emit a cloned resource warning if it is warranted
      if prior_resource
        if is_trivial_resource?(prior_resource) && identicalish_resources?(prior_resource, resource)
          emit_harmless_cloning_debug
        else
          emit_cloned_resource_warning
        end
      end

      # Run optional resource hook
      resource.after_created

      resource
    end

    private

    def resource_class
      # Checks the new platform => short_name => resource mapping initially
      # then fall back to the older approach (Chef::Resource.const_get) for
      # backward compatibility
      @resource_class ||= Chef::Resource.resource_for_node(type, run_context.node)
    end

    def is_trivial_resource?(resource)
      identicalish_resources?(resource_class.new(name, run_context), resource)
    end

    # this is an equality test specific to checking for 3694 cloning warnings
    def identicalish_resources?(first, second)
      skipped_ivars = [ :@source_line, :@cookbook_name, :@recipe_name, :@params, :@elapsed_time, :@declared_type ]
      checked_ivars = ( first.instance_variables | second.instance_variables ) - skipped_ivars
      non_matching_ivars = checked_ivars.reject do |iv|
        if iv == :@action && ( [first.instance_variable_get(iv)].flatten == [:nothing] || [second.instance_variable_get(iv)].flatten == [:nothing] )
          # :nothing action on either side of the comparison always matches
          true
        else
          first.instance_variable_get(iv) == second.instance_variable_get(iv)
        end
      end
      Chef::Log.debug("ivars which did not match with the prior resource: #{non_matching_ivars}")
      non_matching_ivars.empty?
    end

    def emit_cloned_resource_warning
      Chef::Log.warn("Cloning resource attributes for #{resource} from prior resource (CHEF-3694)")
      Chef::Log.warn("Previous #{prior_resource}: #{prior_resource.source_line}") if prior_resource.source_line
      Chef::Log.warn("Current  #{resource}: #{resource.source_line}") if resource.source_line
    end

    def emit_harmless_cloning_debug
      Chef::Log.debug("Harmless resource cloning from #{prior_resource}:#{prior_resource.source_line} to #{resource}:#{resource.source_line}")
    end

    def prior_resource
      @prior_resource ||=
        begin
          key = "#{type}[#{name}]"
          prior_resource = run_context.resource_collection.lookup(key)
        rescue Chef::Exceptions::ResourceNotFound
          nil
        end
    end

  end
end
