#
# Author:: Adam Jacob (<adam@opscode.com>)
# Author:: Christopher Walters (<cw@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/params_validate'
require 'chef/mixin/check_helper'
require 'chef/mixin/language'
require 'chef/mixin/convert_to_class_name'
require 'chef/resource/conditional'
require 'chef/resource_collection'
require 'chef/resource_platform_map'
require 'chef/node'

require 'chef/mixin/deprecation'

class Chef
  class Resource
    class Notification < Struct.new(:resource, :action, :notifying_resource)

      def duplicates?(other_notification)
        unless other_notification.respond_to?(:resource) && other_notification.respond_to?(:action)
          msg = "only duck-types of Chef::Resource::Notification can be checked for duplication "\
                "you gave #{other_notification.inspect}"
          raise ArgumentError, msg
        end
        other_notification.resource == resource && other_notification.action == action
      end

      # If resource and/or notifying_resource is not a resource object, this will look them up in the resource collection
      # and fix the references from strings to actual Resource objects.
      def resolve_resource_reference(resource_collection)
        return resource if resource.kind_of?(Chef::Resource) && notifying_resource.kind_of?(Chef::Resource)

        if not(resource.kind_of?(Chef::Resource))
          fix_resource_reference(resource_collection)
        end

        if not(notifying_resource.kind_of?(Chef::Resource))
          fix_notifier_reference(resource_collection)
        end
      end

      # This will look up the resource if it is not a Resource Object.  It will complain if it finds multiple
      # resources, can't find a resource, or gets invalid syntax.
      def fix_resource_reference(resource_collection)
        matching_resource = resource_collection.find(resource)
        if Array(matching_resource).size > 1
          msg = "Notification #{self} from #{notifying_resource} was created with a reference to multiple resources, "\
          "but can only notify one resource. Notifying resource was defined on #{notifying_resource.source_line}"
          raise Chef::Exceptions::InvalidResourceReference, msg
        end
        self.resource = matching_resource

      rescue Chef::Exceptions::ResourceNotFound => e
        err = Chef::Exceptions::ResourceNotFound.new(<<-FAIL)
resource #{notifying_resource} is configured to notify resource #{resource} with action #{action}, \
but #{resource} cannot be found in the resource collection. #{notifying_resource} is defined in \
#{notifying_resource.source_line}
FAIL
        err.set_backtrace(e.backtrace)
        raise err
      rescue Chef::Exceptions::InvalidResourceSpecification => e
          err = Chef::Exceptions::InvalidResourceSpecification.new(<<-F)
Resource #{notifying_resource} is configured to notify resource #{resource} with action #{action}, \
but #{resource.inspect} is not valid syntax to look up a resource in the resource collection. Notification \
is defined near #{notifying_resource.source_line}
F
          err.set_backtrace(e.backtrace)
        raise err
      end

      # This will look up the notifying_resource if it is not a Resource Object.  It will complain if it finds multiple
      # resources, can't find a resource, or gets invalid syntax.
      def fix_notifier_reference(resource_collection)
        matching_notifier = resource_collection.find(notifying_resource)
        if Array(matching_notifier).size > 1
          msg = "Notification #{self} from #{notifying_resource} was created with a reference to multiple notifying "\
          "resources, but can only originate from one resource.  Destination resource was defined "\
          "on #{resource.source_line}"
          raise Chef::Exceptions::InvalidResourceReference, msg
        end
        self.notifying_resource = matching_notifier

      rescue Chef::Exceptions::ResourceNotFound => e
        err = Chef::Exceptions::ResourceNotFound.new(<<-FAIL)
Resource #{resource} is configured to receive notifications from #{notifying_resource} with action #{action}, \
but #{notifying_resource} cannot be found in the resource collection. #{resource} is defined in \
#{resource.source_line}
FAIL
        err.set_backtrace(e.backtrace)
        raise err
      rescue Chef::Exceptions::InvalidResourceSpecification => e
          err = Chef::Exceptions::InvalidResourceSpecification.new(<<-F)
Resource #{resource} is configured to receive notifications from  #{notifying_resource} with action #{action}, \
but #{notifying_resource.inspect} is not valid syntax to look up a resource in the resource collection. Notification \
is defined near #{resource.source_line}
F
          err.set_backtrace(e.backtrace)
        raise err
      end

    end

    FORBIDDEN_IVARS = [:@run_context, :@node, :@not_if, :@only_if, :@enclosing_provider]
    HIDDEN_IVARS = [:@allowed_actions, :@resource_name, :@source_line, :@run_context, :@name, :@node, :@not_if, :@only_if, :@elapsed_time, :@enclosing_provider]

    # Track all subclasses of Resource. This is used so names can be looked up
    # when attempting to deserialize from JSON. (See: json_compat)
    def self.resource_classes
      # Using a class variable here ensures we have one variable to track
      # subclasses shared by the entire class hierarchy; without this, each
      # subclass would have its own list of subclasses.
      @@resource_classes ||= []
    end

    # Callback when subclass is defined. Adds subclass to list of subclasses.
    def self.inherited(subclass)
      resource_classes << subclass
    end

    # Look up a subclass by +class_name+ which should be a string that matches
    # `Subclass.name`
    def self.find_subclass_by_name(class_name)
      resource_classes.first {|c| c.name == class_name }
    end


    include Chef::Mixin::CheckHelper
    include Chef::Mixin::ParamsValidate
    include Chef::Mixin::Language
    include Chef::Mixin::ConvertToClassName
    include Chef::Mixin::Deprecation

    if Module.method(:const_defined?).arity == 1
      def self.strict_const_defined?(const)
        const_defined?(const)
      end
    else
      def self.strict_const_defined?(const)
        const_defined?(const, false)
      end
    end

    # Set or return the list of "state attributes" implemented by the Resource
    # subclass. State attributes are attributes that describe the desired state
    # of the system, such as file permissions or ownership. In general, state
    # attributes are attributes that could be populated by examining the state
    # of the system (e.g., File.stat can tell you the permissions on an
    # existing file). Contrarily, attributes that are not "state attributes"
    # usually modify the way Chef itself behaves, for example by providing
    # additional options for a package manager to use when installing a
    # package.
    #
    # This list is used by the Chef client auditing system to extract
    # information from resources to describe changes made to the system.
    def self.state_attrs(*attr_names)
      @state_attrs ||= []
      @state_attrs = attr_names unless attr_names.empty?

      # Return *all* state_attrs that this class has, including inherited ones
      if superclass.respond_to?(:state_attrs)
        superclass.state_attrs + @state_attrs
      else
        @state_attrs
      end
    end

    # Set or return the "identity attribute" for this resource class. This is
    # generally going to be the "name attribute" for this resource. In other
    # words, the resource type plus this attribute uniquely identify a given
    # bit of state that chef manages. For a File resource, this would be the
    # path, for a package resource, it will be the package name. This will show
    # up in chef-client's audit records as a searchable field.
    def self.identity_attr(attr_name=nil)
      @identity_attr ||= nil
      @identity_attr = attr_name if attr_name

      # If this class doesn't have an identity attr, we'll defer to the superclass:
      if @identity_attr || !superclass.respond_to?(:identity_attr)
        @identity_attr
      else
        superclass.identity_attr
      end
    end

    def self.dsl_name
      convert_to_snake_case(name, 'Chef::Resource')
    end

    attr_accessor :params
    attr_accessor :provider
    attr_accessor :allowed_actions
    attr_accessor :run_context
    attr_accessor :cookbook_name
    attr_accessor :recipe_name
    attr_accessor :enclosing_provider
    attr_accessor :source_line
    attr_accessor :retries
    attr_accessor :retry_delay

    attr_reader :updated

    attr_reader :resource_name
    attr_reader :not_if_args
    attr_reader :only_if_args

    attr_reader :elapsed_time

    # Each notify entry is a resource/action pair, modeled as an
    # Struct with a #resource and #action member

    def initialize(name, run_context=nil)
      @name = name
      @run_context = run_context
      @noop = nil
      @before = nil
      @params = Hash.new
      @provider = nil
      @allowed_actions = [ :nothing ]
      @action = :nothing
      @updated = false
      @updated_by_last_action = false
      @supports = {}
      @ignore_failure = false
      @retries = 0
      @retry_delay = 2
      @not_if = []
      @only_if = []
      @source_line = nil
      @elapsed_time = 0

      @node = run_context ? deprecated_ivar(run_context.node, :node, :warn) : nil
    end

    # Returns a Hash of attribute => value for the state attributes declared in
    # the resource's class definition.
    def state
      self.class.state_attrs.inject({}) do |state_attrs, attr_name|
        state_attrs[attr_name] = send(attr_name)
        state_attrs
      end
    end

    # Returns the value of the identity attribute, if declared. Falls back to
    # #name if no identity attribute is declared.
    def identity
      if identity_attr = self.class.identity_attr
        send(identity_attr)
      else
        name
      end
    end


    def updated=(true_or_false)
      Chef::Log.warn("Chef::Resource#updated=(true|false) is deprecated. Please call #updated_by_last_action(true|false) instead.")
      Chef::Log.warn("Called from:")
      caller[0..3].each {|line| Chef::Log.warn(line)}
      updated_by_last_action(true_or_false)
      @updated = true_or_false
    end

    def node
      run_context && run_context.node
    end

    # If an unknown method is invoked, determine whether the enclosing Provider's
    # lexical scope can fulfill the request. E.g. This happens when the Resource's
    # block invokes new_resource.
    def method_missing(method_symbol, *args, &block)
      if enclosing_provider && enclosing_provider.respond_to?(method_symbol)
        enclosing_provider.send(method_symbol, *args, &block)
      else
        raise NoMethodError, "undefined method `#{method_symbol.to_s}' for #{self.class.to_s}"
      end
    end

    def load_prior_resource
      begin
        prior_resource = run_context.resource_collection.lookup(self.to_s)
        # if we get here, there is a prior resource (otherwise we'd have jumped
        # to the rescue clause).
        Chef::Log.warn("Cloning resource attributes for #{self.to_s} from prior resource (CHEF-3694)")
        Chef::Log.warn("Previous #{prior_resource}: #{prior_resource.source_line}") if prior_resource.source_line
        Chef::Log.warn("Current  #{self}: #{self.source_line}") if self.source_line
        prior_resource.instance_variables.each do |iv|
          unless iv.to_sym == :@source_line || iv.to_sym == :@action || iv.to_sym == :@not_if || iv.to_sym == :@only_if
            self.instance_variable_set(iv, prior_resource.instance_variable_get(iv))
          end
        end
        true
      rescue Chef::Exceptions::ResourceNotFound => e
        true
      end
    end

    def supports(args={})
      if args.any?
        @supports = args
      else
        @supports
      end
    end

    def provider(arg=nil)
      klass = if arg.kind_of?(String) || arg.kind_of?(Symbol)
                lookup_provider_constant(arg)
              else
                arg
              end
      set_or_return(
        :provider,
        klass,
        :kind_of => [ Class ]
      )
    end

    def action(arg=nil)
      if arg
        action_list = arg.kind_of?(Array) ? arg : [ arg ]
        action_list = action_list.collect { |a| a.to_sym }
        action_list.each do |action|
          validate(
            {
              :action => action,
            },
            {
              :action => { :kind_of => Symbol, :equal_to => @allowed_actions },
            }
          )
        end
        @action = action_list
      else
        @action
      end
    end

    def name(name=nil)
      set_if_args(@name, name) do
        raise ArgumentError, "name must be a string!" unless name.kind_of?(String)
        @name = name
      end
    end

    def noop(tf=nil)
      set_if_args(@noop, tf) do
        raise ArgumentError, "noop must be true or false!" unless tf == true || tf == false
        @noop = tf
      end
    end

    def ignore_failure(arg=nil)
      set_or_return(
        :ignore_failure,
        arg,
        :kind_of => [ TrueClass, FalseClass ]
      )
    end

    def retries(arg=nil)
      set_or_return(
        :retries,
        arg,
        :kind_of => Integer
      )
    end

    def retry_delay(arg=nil)
      set_or_return(
        :retry_delay,
        arg,
        :kind_of => Integer
      )
    end

    def epic_fail(arg=nil)
      ignore_failure(arg)
    end

    def notifies(*args)
      unless ( args.size > 0 && args.size < 4)
        raise ArgumentError, "Wrong number of arguments for notifies: should be 1-3 arguments, you gave #{args.inspect}"
      end

      if args.size > 1 # notifies(:action, resource) OR notifies(:action, resource, :immediately)
        add_notification(*args)
      else
        # This syntax is so weird. surely people will just give us one hash?
        notifications = args.flatten
        notifications.each do |resources_notifications|
          resources_notifications.each do |resource, notification|
            action, timing = notification[0], notification[1]
            Chef::Log.debug "Adding notification from resource #{self} to `#{resource.inspect}' => `#{notification.inspect}'"
            add_notification(action, resource, timing)
          end
        end
      end
    rescue NoMethodError
      Chef::Log.fatal("Error processing notifies(#{args.inspect}) on #{self}")
      raise
    end

    def add_notification(action, resources, timing=:delayed)
      resources = [resources].flatten
      resources.each do |resource|
        case timing.to_s
        when 'delayed'
          notifies_delayed(action, resource)
        when 'immediate', 'immediately'
          notifies_immediately(action, resource)
        else
          raise ArgumentError,  "invalid timing: #{timing} for notifies(#{action}, #{resources.inspect}, #{timing}) resource #{self} "\
                                "Valid timings are: :delayed, :immediate, :immediately"
        end
      end

      true
    end

    # Iterates over all immediate and delayed notifications, calling
    # resolve_resource_reference on each in turn, causing them to
    # resolve lazy/forward references.
    def resolve_notification_references
      run_context.immediate_notifications(self).each { |n| n.resolve_resource_reference(run_context.resource_collection) }
      run_context.delayed_notifications(self).each {|n| n.resolve_resource_reference(run_context.resource_collection) }
    end

    def notifies_immediately(action, resource_spec)
      run_context.notifies_immediately(Notification.new(resource_spec, action, self))
    end

    def notifies_delayed(action, resource_spec)
      run_context.notifies_delayed(Notification.new(resource_spec, action, self))
    end

    def immediate_notifications
      run_context.immediate_notifications(self)
    end

    def delayed_notifications
      run_context.delayed_notifications(self)
    end

    def resources(*args)
      run_context.resource_collection.find(*args)
    end

    def subscribes(action, resources, timing=:delayed)
      resources = [resources].flatten
      resources.each do |resource|
        if resource.is_a?(String)
          resource = Chef::Resource.new(resource, run_context)
        end
        if resource.run_context.nil?
          resource.run_context = run_context
        end
        resource.add_notification(action, self, timing)
      end
      true
    end

    def is(*args)
      if args.size == 1
        args.first
      else
        return *args
      end
    end

    def to_s
      "#{@resource_name}[#{@name}]"
    end

    def to_text
      ivars = instance_variables.map { |ivar| ivar.to_sym } - HIDDEN_IVARS
      text = "# Declared in #{@source_line}\n\n"
      text << self.class.dsl_name + "(\"#{name}\") do\n"
      ivars.each do |ivar|
        if (value = instance_variable_get(ivar)) && !(value.respond_to?(:empty?) && value.empty?)
          value_string = value.respond_to?(:to_text) ? value.to_text : value.inspect
          text << "  #{ivar.to_s.sub(/^@/,'')} #{value_string}\n"
        end
      end
      [@not_if, @only_if].flatten.each do |conditional|
        text << "  #{conditional.to_text}\n"
      end
      text << "end\n"
    end

    def inspect
      ivars = instance_variables.map { |ivar| ivar.to_sym } - FORBIDDEN_IVARS
      ivars.inject("<#{to_s}") do |str, ivar|
        str << " #{ivar}: #{instance_variable_get(ivar).inspect}"
      end << ">"
    end

    # as_json does most of the to_json heavy lifted. It exists here in case activesupport
    # is loaded. activesupport will call as_json and skip over to_json. This ensure
    # json is encoded as expected
    def as_json(*a)
      safe_ivars = instance_variables.map { |ivar| ivar.to_sym } - FORBIDDEN_IVARS
      instance_vars = Hash.new
      safe_ivars.each do |iv|
        instance_vars[iv.to_s.sub(/^@/, '')] = instance_variable_get(iv)
      end
      results = {
        'json_class' => self.class.name,
        'instance_vars' => instance_vars
      }
    end

    # Serialize this object as a hash
    def to_json(*a)
      results = as_json
      results.to_json(*a)
    end

    def to_hash
      safe_ivars = instance_variables.map { |ivar| ivar.to_sym } - FORBIDDEN_IVARS
      instance_vars = Hash.new
      safe_ivars.each do |iv|
        key = iv.to_s.sub(/^@/,'').to_sym
        instance_vars[key] = instance_variable_get(iv)
      end
      instance_vars
    end

    # If command is a block, returns true if the block returns true, false if it returns false.
    # ("Only run this resource if the block is true")
    #
    # If the command is not a block, executes the command.  If it returns any status other than
    # 0, it returns false (clearly, a 0 status code is true)
    #
    # === Parameters
    # command<String>:: A a string to execute.
    # opts<Hash>:: Options control the execution of the command
    # block<Proc>:: A ruby block to run. Ignored if a command is given.
    #
    # === Evaluation
    # * evaluates to true if the block is true, or if the command returns 0
    # * evaluates to false if the block is false, or if the command returns a non-zero exit code.
    def only_if(command=nil, opts={}, &block)
      if command || block_given?
        @only_if << Conditional.only_if(command, opts, &block)
      end
      @only_if
    end

    # If command is a block, returns false if the block returns true, true if it returns false.
    # ("Do not run this resource if the block is true")
    #
    # If the command is not a block, executes the command.  If it returns a 0 exitstatus, returns false.
    # ("Do not run this resource if the command returns 0")
    #
    # === Parameters
    # command<String>:: A a string to execute.
    # opts<Hash>:: Options control the execution of the command
    # block<Proc>:: A ruby block to run. Ignored if a command is given.
    #
    # === Evaluation
    # * evaluates to true if the block is false, or if the command returns a non-zero exit status.
    # * evaluates to false if the block is true, or if the command returns a 0 exit status.
    def not_if(command=nil, opts={}, &block)
      if command || block_given?
        @not_if << Conditional.not_if(command, opts, &block)
      end
      @not_if
    end

    def defined_at
      if cookbook_name && recipe_name && source_line
        "#{cookbook_name}::#{recipe_name} line #{source_line.split(':')[1]}"
      elsif source_line
        file, line_no = source_line.split(':')
        "#{file} line #{line_no}"
      else
        "dynamically defined"
      end
    end

    def cookbook_version
      if cookbook_name
        run_context.cookbook_collection[cookbook_name]
      end
    end

    def events
      run_context.events
    end

    def run_action(action, notification_type=nil, notifying_resource=nil)
      # reset state in case of multiple actions on the same resource.
      @elapsed_time = 0
      start_time = Time.now
      events.resource_action_start(self, action, notification_type, notifying_resource)
      # Try to resolve lazy/forward references in notifications again to handle
      # the case where the resource was defined lazily (ie. in a ruby_block)
      resolve_notification_references
      validate_action(action)

      if Chef::Config[:verbose_logging] || Chef::Log.level == :debug
        # This can be noisy
        Chef::Log.info("Processing #{self} action #{action} (#{defined_at})")
      end

      # ensure that we don't leave @updated_by_last_action set to true
      # on accident
      updated_by_last_action(false)

      begin
        return if should_skip?(action)
        provider_for_action(action).run_action
      rescue Exception => e
        if ignore_failure
          Chef::Log.error("#{self} (#{defined_at}) had an error: #{e.message}; ignore_failure is set, continuing")
          events.resource_failed(self, action, e)
        elsif retries > 0
          events.resource_failed_retriable(self, action, retries, e)
          @retries -= 1
          Chef::Log.info("Retrying execution of #{self}, #{retries} attempt(s) left")
          sleep retry_delay
          retry
        else
          events.resource_failed(self, action, e)
          raise customize_exception(e)
        end
      ensure
        @elapsed_time = Time.now - start_time
        events.resource_completed(self)
      end
    end

    def validate_action(action)
      raise ArgumentError, "nil is not a valid action for resource #{self}" if action.nil?
    end

    def provider_for_action(action)
      # leverage new platform => short_name => resource
      # which requires explicitly setting provider in
      # resource class
      if self.provider
        provider = self.provider.new(self, self.run_context)
        provider.action = action
        provider
      else # fall back to old provider resolution
        Chef::Platform.provider_for_resource(self, action)
      end
    end

    def customize_exception(e)
      new_exception = e.exception("#{self} (#{defined_at}) had an error: #{e.class.name}: #{e.message}")
      new_exception.set_backtrace(e.backtrace)
      new_exception
    end
    # Evaluates not_if and only_if conditionals. Returns a falsey value if any
    # of the conditionals indicate that this resource should be skipped, i.e.,
    # if an only_if evaluates to false or a not_if evaluates to true.
    #
    # If this resource should be skipped, returns the first conditional that
    # "fails" its check. Subsequent conditionals are not evaluated, so in
    # general it's not a good idea to rely on side effects from not_if or
    # only_if commands/blocks being evaluated.
    def should_skip?(action)
      conditionals = only_if + not_if
      return false if conditionals.empty?

      conditionals.find do |conditional|
        if conditional.continue?
          false
        else
          events.resource_skipped(self, action, conditional)
          Chef::Log.debug("Skipping #{self} due to #{conditional.description}")
          true
        end
      end
    end

    def updated_by_last_action(true_or_false)
      @updated ||= true_or_false
      @updated_by_last_action = true_or_false
    end

    def updated_by_last_action?
      @updated_by_last_action
    end

    def updated?
      updated
    end

    def self.json_create(o)
      resource = self.new(o["instance_vars"]["@name"])
      o["instance_vars"].each do |k,v|
        resource.instance_variable_set("@#{k}".to_sym, v)
      end
      resource
    end

    # Hook to allow a resource to run specific code after creation
    def after_created
      nil
    end

    extend Chef::Mixin::ConvertToClassName

    def self.attribute(attr_name, validation_opts={})
      # This atrocity is the only way to support 1.8 and 1.9 at the same time
      # When you're ready to drop 1.8 support, do this:
      # define_method attr_name.to_sym do |arg=nil|
      # etc.
      shim_method=<<-SHIM
      def #{attr_name}(arg=nil)
        _set_or_return_#{attr_name}(arg)
      end
      SHIM
      class_eval(shim_method)

      define_method("_set_or_return_#{attr_name.to_s}".to_sym) do |arg|
        set_or_return(attr_name.to_sym, arg, validation_opts)
      end
    end

    def self.build_from_file(cookbook_name, filename, run_context)
      rname = filename_to_qualified_string(cookbook_name, filename)

      # Add log entry if we override an existing light-weight resource.
      class_name = convert_to_class_name(rname)
      if Chef::Resource.strict_const_defined?(class_name)
        Chef::Log.info("#{class_name} light-weight resource already initialized -- overriding!")
        old_class = Chef::Resource.send(:remove_const, class_name)
        Chef::Resource.resource_classes.delete(old_class)
      end

      new_resource_class = Class.new self do |cls|

        # default initialize method that ensures that when initialize is finally
        # wrapped (see below), super is called in the event that the resource
        # definer does not implement initialize
        def initialize(name, run_context)
          super(name, run_context)
        end

        @actions_to_create = []

        class << cls
          include Chef::Mixin::FromFile

          attr_accessor :run_context
          attr_reader :action_to_set_default

          def node
            self.run_context.node
          end

          def actions_to_create
            @actions_to_create
          end

          define_method(:default_action) do |action_name|
            actions_to_create.push(action_name)
            @action_to_set_default = action_name
          end

          define_method(:actions) do |*action_names|
            actions_to_create.push(*action_names)
          end
        end

        # set the run context in the class instance variable
        cls.run_context = run_context

        # load resource definition from file
        cls.class_from_file(filename)

        # create a new constructor that wraps the old one and adds the actions
        # specified in the DSL
        alias_method :old_init, :initialize

        new_init =<<INIT
        def initialize(name, run_context=nil)
          @resource_name = "#{rname}".to_sym
          old_init(name, run_context)
          @action = self.class.action_to_set_default || @action
          allowed_actions.push(self.class.actions_to_create).flatten!
       end
INIT
      class_eval(new_init)
    end

      # register new class as a Chef::Resource
      class_name = convert_to_class_name(rname)
      Chef::Resource.const_set(class_name, new_resource_class)
      Chef::Log.debug("Loaded contents of #{filename} into a resource named #{rname} defined in Chef::Resource::#{class_name}")

      new_resource_class
    end

    # Resources that want providers namespaced somewhere other than
    # Chef::Provider can set the namespace with +provider_base+
    # Ex:
    #   class MyResource < Chef::Resource
    #     provider_base Chef::Provider::Deploy
    #     # ...other stuff
    #   end
    def self.provider_base(arg=nil)
      @provider_base ||= arg
      @provider_base ||= Chef::Provider
    end

    def self.platform_map
      @@platform_map ||= PlatformMap.new
    end

    # Maps a short_name (and optionally a platform  and version) to a
    # Chef::Resource.  This allows finer grained per platform resource
    # attributes and the end of overloaded resource definitions
    # (I'm looking at you Chef::Resource::Package)
    # Ex:
    #   class WindowsFile < Chef::Resource
    #     provides :file, :on_platforms => ["windows"]
    #     # ...other stuff
    #   end
    #
    # TODO: 2011-11-02 schisamo - platform_version support
    def self.provides(short_name, opts={})
      short_name_sym = short_name
      if short_name.kind_of?(String)
        short_name.downcase!
        short_name.gsub!(/\s/, "_")
        short_name_sym = short_name.to_sym
      end
      if opts.has_key?(:on_platforms)
        platforms = [opts[:on_platforms]].flatten
        platforms.each do |p|
          p = :default if :all == p.to_sym
          platform_map.set(
            :platform => p.to_sym,
            :short_name => short_name_sym,
            :resource => self
          )
        end
      else
        platform_map.set(
          :short_name => short_name_sym,
          :resource => self
        )
      end
    end

    # Returns a resource based on a short_name anda platform and version.
    #
    #
    # ==== Parameters
    # short_name<Symbol>:: short_name of the resource (ie :directory)
    # platform<Symbol,String>:: platform name
    # version<String>:: platform version
    #
    # === Returns
    # <Chef::Resource>:: returns the proper Chef::Resource class
    def self.resource_for_platform(short_name, platform=nil, version=nil)
      platform_map.get(short_name, platform, version)
    end

    # Returns a resource based on a short_name and a node's
    # platform and version.
    #
    # ==== Parameters
    # short_name<Symbol>:: short_name of the resource (ie :directory)
    # node<Chef::Node>:: Node object to look up platform and version in
    #
    # === Returns
    # <Chef::Resource>:: returns the proper Chef::Resource class
    def self.resource_for_node(short_name, node)
      begin
        platform, version = Chef::Platform.find_platform_and_version(node)
      rescue ArgumentError
      end
      resource = resource_for_platform(short_name, platform, version)
      resource
    end

    private

    def lookup_provider_constant(name)
      begin
        self.class.provider_base.const_get(convert_to_class_name(name.to_s))
      rescue NameError => e
        if e.to_s =~ /#{Regexp.escape(self.class.provider_base.to_s)}/
          raise ArgumentError, "No provider found to match '#{name}'"
        else
          raise e
        end
      end
    end

  end
end