# Provides an extension for Rails respond_to by expading MimeResponds::Responder
# and adding respond_to class method and respond_with instance method.
#
module ActionController #:nodoc:
  class Base #:nodoc:

    protected
      # Defines respond_to method to store formats that are rendered by default.
      #
      # Examples:
      #
      #   respond_to :html, :xml, :json
      #
      # All actions on your controller will respond to :html, :xml and :json.
      # But if you want to specify it based on your actions, you can use only and
      # except:
      #
      #   respond_to :html
      #   respond_to :xml, :json, :except => [ :edit ]
      #
      # The definition above explicits that all actions respond to :html. And all
      # actions except :edit respond to :xml and :json.
      #
      # You can specify also only parameters:
      #
      #   respond_to :rjs, :only => :create
      #
      # Which would be the same as:
      #
      #   respond_to :rjs => :create
      #
      def self.respond_to(*formats)
        options = formats.extract_options!
        formats_hash = {}

        only_actions   = Array(options.delete(:only))
        except_actions = Array(options.delete(:except))

        only_actions.map!{ |a| a.to_sym }
        except_actions.map!{ |a| a.to_sym }

        formats.each do |format|
          formats_hash[format.to_sym]          = {}
          formats_hash[format.to_sym][:only]   = only_actions   unless only_actions.empty?
          formats_hash[format.to_sym][:except] = except_actions unless except_actions.empty?
        end

        options.each do |format, actions|
          formats_hash[format.to_sym] = {}
          next if actions == :all || actions == 'all'

          actions = Array(actions)
          actions.map!{ |a| a.to_sym }

          formats_hash[format.to_sym][:only] = actions unless actions.empty?
        end

        write_inheritable_hash(:formats_for_respond_to, formats_hash)
      end
      class_inheritable_reader :formats_for_respond_to

      # Define defaults respond_to
      respond_to :html
      respond_to :xml, :except => [ :edit ]

      # Method to clear all respond_to declared until the current controller.
      # This is like freeing the controller from the inheritance chain. :)
      #
      def self.clear_respond_to!
        formats = formats_for_respond_to
        formats.each { |k,v| formats[k] = { :only => [] } }
        write_inheritable_hash(:formats_for_respond_to, formats)
      end

      # respond_with accepts an object and tries to render a view based in the
      # controller and actions that called respond_with. If the view cannot be
      # found, it will try to call :to_format in the object.
      #
      #   class ProjectsController < ApplicationController
      #     respond_to :html, :xml
      #
      #     def show
      #       @project = Project.find(:id)
      #       respond_with(@project)
      #     end
      #   end
      #
      # When the client request a xml, we will check first for projects/show.xml
      # if it can't be found, we will call :to_xml in the object @project. If the
      # object eventually doesn't respond to :to_xml it will render 404.
      #
      # If you want to overwrite the formats specified in the class, you can
      # send your new formats using the options :to.
      #
      #     def show
      #       @project = Project.find(:id)
      #       respond_with(@project, :to => :json)
      #     end
      #
      # That means that this action will ONLY reply to json requests.
      #
      # All other options sent will be forwarded to the render method. So you can
      # do:
      #
      #    def create
      #       # ... 
      #       if @project.save
      #         respond_with(@project, :status => :ok, :location => @project)
      #       else
      #         respond_with(@project.errors, :status => :unprocessable_entity)
      #      end
      #    end
      #
      # respond_with does not accept blocks, if you want advanced configurations
      # check respond_to method sending :with => @object as option.
      #
      # Returns true if anything is rendered. Returns false otherwise.
      #
      def respond_with(object, options = {})
        attempt_to_respond = false

        # You can also send a responder object as parameter.
        #
        responder = options.delete(:responder) || Responder.new(self)

        # Check for given mime types
        #
        mime_types = Array(options.delete(:to))
        mime_types.map!{ |mime| mime.to_sym }

        # If :skip_not_acceptable is sent, it will not render :not_acceptable
        # if the mime type sent by the client cannot be found.
        #
        skip_not_acceptable = options.delete(:skip_not_acceptable)

        for priority in responder.mime_type_priority
          if priority == Mime::ALL && template_exists?
            render options.merge(:action => action_name)
            return true

          elsif responder.action_respond_to_format?(priority.to_sym, mime_types)
            attempt_to_respond = true
            response.template.template_format = priority.to_sym
            response.content_type = priority.to_s

            if template_exists?
              render options.merge(:action => action_name)
              return true
            elsif object.respond_to?(:"to_#{priority.to_sym}")
              render options.merge(:text => object.send(:"to_#{priority.to_sym}"))
              return true
            end
          end
        end

        # If we got here we could not render the object. But if attempted to
        # render (this means, the format sent by the client was valid) we should
        # render a 404.
        #
        # If we even didn't attempt to respond, we respond :not_acceptable
        # unless is told otherwise.
        #
        if attempt_to_respond
          render :text => '404 Not Found', :status => 404
          return true
        elsif !skip_not_acceptable
          head :not_acceptable
          return false
        end

        return false
      end

      # Extends respond_to behaviour.
      #
      # You can now pass objects using the options :with.
      #
      #   respond_to(:html, :xml, :rjs, :with => @project)
      #
      # If you pass an object and send any block, it's exactly the same as:
      #
      #   respond_with(@project, :to => [:html, :xml, :rjs])
      #
      # But the main difference of respond_to and respond_with is that the first
      # allows further customizations:
      #
      #   respond_to(:html, :with => @project) do |format|
      #     format.xml { render :xml => @project.errors  }
      #   end
      #
      # It's the same as:
      #
      #   1. When responding to html, execute respond_with(@object).
      #   2. When accessing a xml, execute the block given.
      #
      # Formats defined in blocks have precedence to formats sent as arguments.
      # In other words, if you pass a format as argument and as block, the block
      # will always be executed.
      #
      # And as in respond_with, all extra options sent will be forwarded to 
      # the render method:
      #
      #   respond_to(:with => @projects.errors, :status => :unprocessable_entity) do |format|
      #     format.html { render :template => 'new' }
      #   end
      #
      def respond_to(*types, &block)
        options = types.extract_options!
        object = options.delete(:with)
        responder = Responder.new(self)
        
        # This is the default respond_to behaviour, when no object is given.
        if object.nil?
          block ||= lambda { |responder| types.each { |type| responder.send(type) } }
          block.call(responder)
          responder.respond
          return true # we are done here

        else
          # If a block is given, it checks if we can perform the requested format.
          #
          # Even if Mime::ALL is sent by the client, we do not respond_to it now.
          # This is done using calling :respond_to_block instead of :respond.
          #
          # It's worth to remember that responder_to_block does not respond
          # :not_acceptable also.
          #
          if block_given?
            block.call(responder)
            responder.respond_to_block
            return true if responder.responded? || performed?
          end

          # Let's see if we get lucky rendering with :respond_with.
          # At the end, respond_with checks for Mime::ALL if any template exist.
          #
          # Notice that we are sending the responder (for performance gain) and
          # sending :skip_not_acceptable because we don't want to respond
          # :not_acceptable yet.
          #
          if respond_with(object, options.merge(:to => types, :responder => responder, :skip_not_acceptable => true))
            return true

          # Since respond_with couldn't help us, our last chance is to reply to
          # any block given if the user send all as mime type.
          #
          elsif block_given?
            return true if responder.respond_to_all
          end
        end

        # If we get here it means that we could not satisfy our request.
        # Now we finally return :not_acceptable.
        #
        head :not_acceptable
        return false
      end

    private

      # Define template_exists? for Rails 2.3
      unless ActionController::Base.private_instance_methods.include? 'template_exists?'
        def template_exists?
          self.view_paths.find_template("#{controller_name}/#{action_name}", response.template.template_format)
        rescue ActionView::MissingTemplate
          false
        end
      end

    # If ApplicationController is already defined around here, we should call
    # inherited_with_inheritable_attributes to insert formats_for_respond_to.
    # This usually happens only on Rails 2.3.
    #
    if defined?(ApplicationController)
      self.send(:inherited_with_inheritable_attributes, ApplicationController)
    end

  end

  module MimeResponds #:nodoc:
    class Responder #:nodoc:

      # Create an attr_reader for @mime_type_priority
      attr_reader :mime_type_priority

      # Stores if this Responder instance called any block.
      def responded?; @responded; end

      # Similar as respond but if we can't find a valid mime type,
      # we do not send :not_acceptable message as head.
      #
      # It does not respond to Mime::ALL in priority as well.
      #
      def respond_to_block
        for priority in @mime_type_priority
          next if priority == Mime::ALL

          if @responses[priority]
            @responses[priority].call
            return (@responded = true) # mime type match found, be happy and return
          end
        end

        if @order.include?(Mime::ALL)
          @responses[Mime::ALL].call
          return (@responded = true)
        else
          return (@responded = false)
        end
      end

      # Respond to the first format given if Mime::ALL is included in the
      # mime type priorites. This is the behaviour expected when the client
      # sends "*/*" as mime type.
      #
      def respond_to_all
        if @mime_type_priority.include?(Mime::ALL) && first = @responses[@order.first]
          first.call
          return (@responded = true)
        end
      end

      # Receives an format and checks if the current action responds to
      # the given format. If additional mimes are sent, only them are checked.
      #
      def action_respond_to_format?(format, additional_mimes = [])
        if !additional_mimes.blank?
          additional_mimes.include?(format.to_sym)
        elsif formats = @controller.formats_for_respond_to[format.to_sym]
          if formats[:only]
            formats[:only].include?(@controller.action_name.to_sym)
          elsif formats[:except]
            !formats[:except].include?(@controller.action_name.to_sym)
          else
            true
          end
        else
          false
        end
      end

    end
  end
end