#--
# Author:: Adam Jacob (<adam@chef.io>)
# Author:: Christopher Walters
# Copyright:: Copyright 2008-2018, 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 "../exceptions"

class Chef
  module DSL
    module DeclareResource

      # Helper for switching run_contexts.  Allows for using :parent or :root in place of
      # passing the run_context.  Executes the block in the run_context.  Returns the return
      # value of the passed block.
      #
      # @param rc  [Chef::RunContext,Symbol] Either :root, :parent or a Chef::RunContext
      #
      # @return return value of the block
      #
      # @example
      #   # creates/returns a 'service[foo]' resource in the root run_context
      #   resource = with_run_context(:root)
      #     edit_resource(:service, "foo") do
      #       action :nothing
      #     end
      #   end
      #
      def with_run_context(rc)
        raise ArgumentError, "with_run_context is useless without a block" unless block_given?

        old_run_context = @run_context
        @run_context =
          case rc
          when Chef::RunContext
            rc
          when :root
            run_context.root_run_context
          when :parent
            run_context.parent_run_context
          else
            raise ArgumentError, "bad argument to run_context helper, must be :root, :parent, or a Chef::RunContext"
          end
        yield
      ensure
        @run_context = old_run_context
      end

      # Lookup a resource in the resource collection by name and delete it.  This
      # will raise Chef::Exceptions::ResourceNotFound if the resource is not found.
      #
      # @param type [Symbol] The type of resource (e.g. `:file` or `:package`)
      # @param name [String] The name of the resource (e.g. '/x/y.txt' or 'apache2')
      # @param run_context [Chef::RunContext] the run_context of the resource collection to operate on
      #
      # @return [Chef::Resource] The resource
      #
      # @example
      #   delete_resource!(:template, '/x/y.txy')
      #
      def delete_resource!(type, name, run_context: self.run_context)
        run_context.resource_collection.delete("#{type}[#{name}]").tap do |resource|
          # Purge any pending notifications too. This will not raise an exception
          # if there are no notifications.
          if resource
            run_context.before_notification_collection.delete(resource.declared_key)
            run_context.immediate_notification_collection.delete(resource.declared_key)
            run_context.delayed_notification_collection.delete(resource.declared_key)
          end
        end
      end

      # Lookup a resource in the resource collection by name and delete it.  Returns
      # nil if the resource is not found and should not fail.
      #
      # @param type [Symbol] The type of resource (e.g. `:file` or `:package`)
      # @param name [String] The name of the resource (e.g. '/x/y.txt' or 'apache2')
      # @param run_context [Chef::RunContext] the run_context of the resource collection to operate on
      #
      # @return [Chef::Resource] The resource
      #
      # @example
      #   delete_resource(:template, '/x/y.txy')
      #
      def delete_resource(type, name, run_context: self.run_context)
        delete_resource!(type, name, run_context: run_context)
      rescue Chef::Exceptions::ResourceNotFound
        nil
      end

      # Lookup a resource in the resource collection by name and edit the resource.  If the resource is not
      # found this will raise Chef::Exceptions::ResourceNotFound.  This is the correct API to use for
      # "chef_rewind" functionality.
      #
      # @param type [Symbol] The type of resource (e.g. `:file` or `:package`)
      # @param name [String] The name of the resource (e.g. '/x/y.txt' or 'apache2')
      # @param run_context [Chef::RunContext] the run_context of the resource collection to operate on
      # @param resource_attrs_block A block that lets you set attributes of the
      #   resource (it is instance_eval'd on the resource instance).
      #
      # @return [Chef::Resource] The updated resource
      #
      # @example
      #   edit_resource!(:template, '/x/y.txy') do
      #     cookbook_name: cookbook_name
      #   end
      #
      def edit_resource!(type, name, created_at: nil, run_context: self.run_context, &resource_attrs_block)
        resource = find_resource!(type, name, run_context: run_context)
        if resource_attrs_block
          if defined?(new_resource)
            resource.instance_exec(new_resource, &resource_attrs_block)
          else
            resource.instance_exec(&resource_attrs_block)
          end
        end
        resource
      end

      # Lookup a resource in the resource collection by name.  If it exists,
      # return it.  If it does not exist, create it.  This is a useful function
      # for accumulator patterns.  In CRUD terminology this is an "upsert" operation and is
      # used to assert that the resource must exist with the specified properties.
      #
      # @param type [Symbol] The type of resource (e.g. `:file` or `:package`)
      # @param name [String] The name of the resource (e.g. '/x/y.txt' or 'apache2')
      # @param created_at [String] The caller of the resource.  Use `caller[0]`
      #   to get the caller of your function.  Defaults to the caller of this
      #   function.
      # @param run_context [Chef::RunContext] the run_context of the resource collection to operate on
      # @param resource_attrs_block A block that lets you set attributes of the
      #   resource (it is instance_eval'd on the resource instance).
      #
      # @return [Chef::Resource] The updated or created resource
      #
      # @example
      #   resource = edit_resource(:template, '/x/y.txy') do
      #     source "y.txy.erb"
      #     variables {}
      #   end
      #   resource.variables.merge!({ home: "/home/klowns"  })
      #
      def edit_resource(type, name, created_at: nil, run_context: self.run_context, &resource_attrs_block)
        edit_resource!(type, name, created_at: created_at, run_context: run_context, &resource_attrs_block)
      rescue Chef::Exceptions::ResourceNotFound
        resource = declare_resource(type, name, created_at: created_at, run_context: run_context)
        if resource_attrs_block
          if defined?(new_resource)
            resource.instance_exec(new_resource, &resource_attrs_block)
          else
            resource.instance_exec(&resource_attrs_block)
          end
        end
        resource
      end

      # Find existing resources by searching the list of existing resources.  Possible
      # forms are:
      #
      #   find(:file => "foobar")
      #   find(:file => [ "foobar", "baz" ])
      #   find("file[foobar]", "file[baz]")
      #   find("file[foobar,baz]")
      #
      # Calls `run_context.resource_collection.find(*args)`
      #
      # The is backcompat API, the use of find_resource, below, is encouraged.
      #
      # @return the matching resource, or an Array of matching resources.
      #
      # @raise ArgumentError if you feed it bad lookup information
      # @raise RuntimeError if it can't find the resources you are looking for.
      #
      def resources(*args)
        run_context.resource_collection.find(*args)
      end

      # Lookup a resource in the resource collection by name.  If the resource is not
      # found this will raise Chef::Exceptions::ResourceNotFound.  This API is identical to the
      # resources() call and while it is a synonym it is not intended to deprecate that call.
      #
      # @param type [Symbol] The type of resource (e.g. `:file` or `:package`)
      # @param name [String] The name of the resource (e.g. '/x/y.txt' or 'apache2')
      # @param run_context [Chef::RunContext] the run_context of the resource collection to operate on
      #
      # @return [Chef::Resource] The updated resource
      #
      # @example
      #   resource = find_resource!(:template, '/x/y.txy')
      #
      def find_resource!(type, name, run_context: self.run_context)
        raise ArgumentError, "find_resource! does not take a block" if block_given?

        run_context.resource_collection.find(type => name)
      end

      # Lookup a resource in the resource collection by name.  If the resource is not found
      # the will be no exception raised and the call will return nil.  If a block is given and
      # no resource is found it will create the resource using the block, if the resource is
      # found then the block will not be applied.  The block version is similar to create_if_missing
      #
      # @param type [Symbol] The type of resource (e.g. `:file` or `:package`)
      # @param name [String] The name of the resource (e.g. '/x/y.txt' or 'apache2')
      # @param run_context [Chef::RunContext] the run_context of the resource collection to operate on
      #
      # @return [Chef::Resource] The updated resource
      #
      # @example
      #   if ( find_resource(:template, '/x/y.txy') )
      #     # do something
      #   else
      #     # don't worry about the error
      #   end
      #
      # @example
      #   # this API can be used to return a resource from an outer run context, and will only create
      #   # an action :nothing service if one does not already exist.
      #   resource = with_run_context(:root) do
      #     find_resource(:service, 'whatever') do
      #       action :nothing
      #     end
      #   end
      #
      def find_resource(type, name, created_at: nil, run_context: self.run_context, &resource_attrs_block)
        find_resource!(type, name, run_context: run_context)
      rescue Chef::Exceptions::ResourceNotFound
        if resource_attrs_block
          declare_resource(type, name, created_at: created_at, run_context: run_context, &resource_attrs_block)
        end # returns nil otherwise
      end

      # Instantiates a resource (via #build_resource), then adds it to the
      # resource collection. Note that resource classes are looked up directly,
      # so this will create the resource you intended even if the method name
      # corresponding to that resource has been overridden.
      #
      # @param type [Symbol] The type of resource (e.g. `:file` or `:package`)
      # @param name [String] The name of the resource (e.g. '/x/y.txt' or 'apache2')
      # @param created_at [String] The caller of the resource.  Use `caller[0]`
      #   to get the caller of your function.  Defaults to the caller of this
      #   function.
      # @param run_context [Chef::RunContext] the run_context of the resource collection to operate on
      # @param resource_attrs_block A block that lets you set attributes of the
      #   resource (it is instance_eval'd on the resource instance).
      #
      # @return [Chef::Resource] The new resource.
      #
      # @example
      #   declare_resource(:file, '/x/y.txy', caller[0]) do
      #     action :delete
      #   end
      #   # Equivalent to
      #   file '/x/y.txt' do
      #     action :delete
      #   end
      #
      def declare_resource(type, name, created_at: nil, run_context: self.run_context, &resource_attrs_block)
        created_at ||= caller[0]

        resource = build_resource(type, name, created_at: created_at, &resource_attrs_block)

        run_context.resource_collection.insert(resource, resource_type: resource.declared_type, instance_name: resource.name)
        resource
      end

      # Instantiate a resource of the given +type+ with the given +name+ and
      # attributes as given in the +resource_attrs_block+.
      #
      # The resource is NOT added to the resource collection.
      #
      # @param type [Symbol] The type of resource (e.g. `:file` or `:package`)
      # @param name [String] The name of the resource (e.g. '/x/y.txt' or 'apache2')
      # @param created_at [String] The caller of the resource.  Use `caller[0]`
      #   to get the caller of your function.  Defaults to the caller of this
      #   function.
      # @param run_context [Chef::RunContext] the run_context of the resource collection to operate on
      # @param resource_attrs_block A block that lets you set attributes of the
      #   resource (it is instance_eval'd on the resource instance).
      #
      # @return [Chef::Resource] The new resource.
      #
      # @example
      #   build_resource(:file, '/x/y.txy', caller[0]) do
      #     action :delete
      #   end
      #
      def build_resource(type, name, created_at: nil, run_context: self.run_context, &resource_attrs_block)
        created_at ||= caller[0]

        # this needs to be lazy in order to avoid circular dependencies since ResourceBuilder
        # will requires the entire provider+resolver universe
        require_relative "../resource_builder" unless defined?(Chef::ResourceBuilder)

        Chef::ResourceBuilder.new(
          type:                type,
          name:                name,
          created_at:          created_at,
          params:              @params,
          run_context:         run_context,
          cookbook_name:       cookbook_name,
          recipe_name:         recipe_name,
          enclosing_provider:  is_a?(Chef::Provider) ? self : nil
        ).build(&resource_attrs_block)
      end

    end
  end
end
