# ==== Why do we use Underscores?
# In Merb, views are actually methods on controllers. This provides
# not-insignificant speed benefits, as well as preventing us from
# needing to copy over instance variables, which we think is proof
# that everything belongs in one class to begin with.
#
# Unfortunately, this means that view helpers need to be included
# into the <strong>Controller</strong> class. To avoid causing confusion
# when your helpers potentially conflict with our instance methods,
# we use an _ to disambiguate. As long as you don't begin your helper
# methods with _, you only need to worry about conflicts with Merb
# methods that are part of the public API.
#
#
#
# ==== Filters
# #before is a class method that allows you to specify before filters in
# your controllers. Filters can either be a symbol or string that
# corresponds to a method name to call, or a proc object. if it is a method
# name that method will be called and if it is a proc it will be called
# with an argument of self where self is the current controller object.
# When you use a proc as a filter it needs to take one parameter.
# 
# #after is identical, but the filters are run after the action is invoked.
#
# ===== Examples
#   before :some_filter
#   before :authenticate, :exclude => [:login, :signup]
#   before :has_role, :with => ["Admin"], :exclude => [:index, :show]
#   before Proc.new { some_method }, :only => :foo
#   before :authorize, :unless => :logged_in?  
#
# You can use either <code>:only => :actionname</code> or 
# <code>:exclude => [:this, :that]</code> but not both at once. 
# <code>:only</code> will only run before the listed actions and 
# <code>:exclude</code> will run for every action that is not listed.
#
# Merb's before filter chain is very flexible. To halt the filter chain you
# use <code>throw :halt</code>. If <code>throw</code> is called with only one 
# argument of <code>:halt</code> the return value of the method 
# <code>filters_halted</code> will be what is rendered to the view. You can 
# override <code>filters_halted</code> in your own controllers to control what 
# it outputs. But the <code>throw</code> construct is much more powerful than 
# just that.
#
# <code>throw :halt</code> can also take a second argument. Here is what that 
# second argument can be and the behavior each type can have:
#
# * +String+:
#   when the second argument is a string then that string will be what
#   is rendered to the browser. Since merb's <code>#render</code> method returns
#   a string you can render a template or just use a plain string:
#
#     throw :halt, "You don't have permissions to do that!"
#     throw :halt, render(:action => :access_denied)
#
# * +Symbol+:
#   If the second arg is a symbol, then the method named after that
#   symbol will be called
#
#     throw :halt, :must_click_disclaimer
#
# * +Proc+:
#   If the second arg is a Proc, it will be called and its return
#   value will be what is rendered to the browser:
#
#     throw :halt, proc { access_denied }
#     throw :halt, proc { Tidy.new(c.index) }
#
# ===== Filter Options (.before, .after, .add_filter, .if, .unless)
# :only<Symbol, Array[Symbol]>::
#   A list of actions that this filter should apply to
#
# :exclude<Symbol, Array[Symbol]::
#   A list of actions that this filter should *not* apply to
# 
# :if<Symbol, Proc>::
#   Only apply the filter if the method named after the symbol or calling the proc evaluates to true
# 
# :unless<Symbol, Proc>::
#   Only apply the filter if the method named after the symbol or calling the proc evaluates to false
#
# :with<Array[Object]>::
#   Arguments to be passed to the filter. Since we are talking method/proc calls,
#   filter method or Proc should to have the same arity
#   as number of elements in Array you pass to this option.
#
# ===== Types (shortcuts for use in this file)
# Filter:: <Array[Symbol, (Symbol, String, Proc)]>
#
# ==== params[:action] and params[:controller] deprecated
# <code>params[:action]</code> and <code>params[:controller]</code> have been deprecated as of
# the 0.9.0 release. They are no longer set during dispatch, and
# have been replaced by <code>action_name</code> and <code>controller_name</code> respectively.
class Merb::AbstractController
  include Merb::RenderMixin
  include Merb::InlineTemplates
  
  class_inheritable_accessor :_layout, :_template_root, :template_roots
  class_inheritable_accessor :_before_filters, :_after_filters
  class_inheritable_accessor :_before_dispatch_callbacks, :_after_dispatch_callbacks

  cattr_accessor :_abstract_subclasses

  #---
  # @api semipublic
  attr_accessor :body
  attr_accessor :action_name
  attr_accessor :_benchmarks, :_thrown_content  

  # Stub so content-type support in RenderMixin doesn't throw errors
  attr_accessor :content_type

  FILTER_OPTIONS = [:only, :exclude, :if, :unless, :with]

  self._before_filters, self._after_filters = [], []
  self._before_dispatch_callbacks, self._after_dispatch_callbacks = [], []

  #---
  # We're using abstract_subclasses so that Merb::Controller can have its
  # own subclasses. We're using a Set so we don't have to worry about
  # uniqueness.
  self._abstract_subclasses = Set.new

  # ==== Returns
  # String:: The controller name in path form, e.g. "admin/items".
  # @api public
  def self.controller_name() @controller_name ||= self.name.to_const_path end

  # ==== Returns
  # String:: The controller name in path form, e.g. "admin/items".
  #
  # @api public
  def controller_name()      self.class.controller_name                   end
  
  # This is called after the controller is instantiated to figure out where to
  # look for templates under the _template_root. Override this to define a new
  # structure for your app.
  #
  # ==== Parameters
  # context<~to_s>:: The controller context (the action or template name).
  # type<~to_s>:: The content type. Could be nil. 
  # controller<~to_s>::
  #   The name of the controller. Defaults to being called with the controller_name.  Set t
  #
  #
  # ==== Returns
  # String:: 
  #   Indicating where to look for the template for the current controller,
  #   context, and content-type.
  #
  # ==== Notes
  # The type is irrelevant for controller-types that don't support
  # content-type negotiation, so we default to not include it in the
  # superclass.
  #
  # ==== Examples
  #   def _template_location
  #     "#{params[:controller]}.#{params[:action]}.#{content_type}"
  #   end
  #
  # This would look for templates at controller.action.mime.type instead
  # of controller/action.mime.type
  #
  # @api public
  # @overridable
  def _template_location(context, type, controller)
    controller ? "#{controller}/#{context}" : context
  end

  # The location to look for a template - override this method for particular behaviour. 
  #
  # ==== Parameters
  # template<String>:: The absolute path to a template - without template extension.
  # type<~to_s>::
  #    The mime-type of the template that will be rendered. Defaults to being called with nil.
  #
  # @api public
  # @overridable
  def _absolute_template_location(template, type)
    template
  end

  # Resets the template roots to the template root passed in.
  #
  # ==== Parameters
  # root<~to_s>:: 
  #   The new path to set the template root to.  
  #
  # @api public
  def self._template_root=(root)
    @_template_root = root
    _reset_template_roots
  end

  # Reset the template root based on the @_template_root ivar.
  #
  # @api private
  def self._reset_template_roots
    self.template_roots = [[self._template_root, :_template_location]]
  end

  # ==== Returns
  # roots<Array[Array]>::
  #   Template roots as pairs of template root path and template location
  #   method.
  #
  # @api unknown
  def self._template_roots
    self.template_roots || _reset_template_roots
  end

  # ==== Parameters
  # roots<Array[Array]>::
  #   Template roots as pairs of template root path and template location
  #   method.
  #
  # @api unknown
  def self._template_roots=(roots)
    self.template_roots = roots
  end
  
  # Returns the list of classes that have specifically subclassed AbstractController.  
  # Does not include all decendents.  
  #
  # ==== Returns
  # Set:: The subclasses.
  #
  # @api private
  def self.subclasses_list() _abstract_subclasses end
  
  # ==== Parameters
  # klass<Merb::AbstractController>::
  #   The controller that is being inherited from Merb::AbstractController
  #
  # @api private
  def self.inherited(klass)
    _abstract_subclasses << klass.to_s
    helper_module_name = klass.to_s =~ /^(#|Merb::)/ ? "#{klass}Helper" : "Merb::#{klass}Helper"
    Object.make_module helper_module_name
    klass.class_eval <<-HERE
      include Object.full_const_get("#{helper_module_name}") rescue nil
    HERE
    super
  end    
  
  # This will initialize the controller, it is designed to be overridden in subclasses (like MerbController)
  # ==== Parameters
  # *args:: The args are ignored in this class, but we need this so that subclassed initializes can have parameters
  #
  # @overridable
  def initialize(*args)
    @_benchmarks = {}
    @_caught_content = {}
  end
  
  # This will dispatch the request, calling internal before/after dispatch callbacks.  
  # If the return value of _call_filters is not :filter_chain_completed the action is not called, and the return from the filters is used instead. 
  # 
  # ==== Parameters
  # action<~to_s>::
  #   The action to dispatch to. This will be #send'ed in _call_action.
  #   Defaults to :to_s.
  #
  # ==== Returns
  # <~to_s>::
  #   Returns the string that was returned from the action. 
  #
  # ==== Raises
  # ArgumentError:: Invalid result caught from before filters.
  #
  # @api plugin
  def _dispatch(action)
    self.action_name = action
    self._before_dispatch_callbacks.each { |cb| cb.call(self) }

    caught = catch(:halt) do
      start = Time.now
      result = _call_filters(_before_filters)
      @_benchmarks[:before_filters_time] = Time.now - start if _before_filters
      result
    end
  
    @body = case caught
    when :filter_chain_completed  then _call_action(action_name)
    when String                   then caught
    # return *something* if you throw halt with nothing
    when nil                      then "<html><body><h1>Filter Chain Halted!</h1></body></html>"
    when Symbol                   then __send__(caught)
    when Proc                     then self.instance_eval(&caught)
    else
      raise ArgumentError, "Threw :halt, #{caught}. Expected String, nil, Symbol, Proc."
    end
    start = Time.now
    _call_filters(_after_filters)
    @_benchmarks[:after_filters_time] = Time.now - start if _after_filters
    
    self._after_dispatch_callbacks.each { |cb| cb.call(self) }
    
    @body
  end
  
  # This method exists to provide an overridable hook for ActionArgs.  It uses #send to call the action method.
  #
  # ==== Parameters
  # action<~to_s>:: the action method to dispatch to
  #
  # @api plugin
  # @overridable
  def _call_action(action)
    send(action)
  end
  
  # Calls a filter chain. 
  #
  # ==== Parameters
  # filter_set<Array[Filter]>::
  #   A set of filters in the form [[:filter, rule], [:filter, rule]]
  #
  # ==== Returns
  # Symbol:: :filter_chain_completed.
  #
  # ==== Notes
  # Filter rules can be Symbols, Strings, or Procs.
  #
  # Symbols or Strings::
  #   Call the method represented by the +Symbol+ or +String+.
  # Procs::
  #   Execute the +Proc+, in the context of the controller (self will be the
  #   controller)
  #
  # @api private
  def _call_filters(filter_set)
    (filter_set || []).each do |filter, rule|
      if _call_filter_for_action?(rule, action_name) && _filter_condition_met?(rule)
        case filter
        when Symbol, String
          if rule.key?(:with)
            args = rule[:with]
            send(filter, *args)
          else
            send(filter)
          end
        when Proc then self.instance_eval(&filter)
        end
      end
    end
    return :filter_chain_completed
  end

  # Determine whether the filter should be called for the current action using :only and :exclude.
  #
  # ==== Parameters
  # rule<Hash>:: Rules for the filter (see below).
  # action_name<~to_s>:: The name of the action to be called.
  #
  # ==== Options (rule)
  # :only<Array>::
  #   Optional list of actions to fire. If given, action_name must be a part of
  #   it for this function to return true.
  # :exclude<Array>::
  #   Optional list of actions not to fire. If given, action_name must not be a
  #   part of it for this function to return true.
  #
  # ==== Returns
  # Boolean:: True if the action should be called.
  #
  # @api private
  def _call_filter_for_action?(rule, action_name)
    # Both:
    # * no :only or the current action is in the :only list
    # * no :exclude or the current action is not in the :exclude list
    (!rule.key?(:only) || rule[:only].include?(action_name)) &&
    (!rule.key?(:exclude) || !rule[:exclude].include?(action_name))
  end

  # Determines whether the filter should be run based on the conditions passed (:if and :unless)
  #
  # ==== Parameters
  # rule<Hash>:: Rules for the filter (see below).
  #
  # ==== Options (rule)
  # :if<Array>:: Optional conditions that must be met for the filter to fire.
  # :unless<Array>::
  #   Optional conditions that must not be met for the filter to fire.
  #
  # ==== Returns
  # Boolean:: True if the conditions are met.
  #
  # @api private
  def _filter_condition_met?(rule)
    # Both:
    # * no :if or the if condition evaluates to true
    # * no :unless or the unless condition evaluates to false
    (!rule.key?(:if) || _evaluate_condition(rule[:if])) &&
    (!rule.key?(:unless) || ! _evaluate_condition(rule[:unless]))
  end

  # Evaluates a filter condition (:if or :unless)
  #
  # ==== Parameters
  # condition<Symbol, Proc>:: The condition to evaluate.
  #
  # ==== Raises
  # ArgumentError:: condition not a Symbol or Proc.
  #
  # ==== Returns
  # Boolean:: True if the condition is met.
  #
  # ==== Alternatives
  # If condition is a symbol, it will be send'ed. If it is a Proc it will be
  # called directly with self as an argument.
  #
  # @api private
  def _evaluate_condition(condition)
    case condition
    when Symbol : self.send(condition)
    when Proc : self.instance_eval(&condition)
    else
      raise ArgumentError,
            'Filter condtions need to be either a Symbol or a Proc'
    end
  end

  # Adds a filter to the after filter chain
  # ==== Parameters
  # filter<Symbol, Proc>:: The filter to add. Defaults to nil.
  # opts<Hash>::
  #   Filter options (see class documentation under <tt>Filter Options</tt>).
  # &block:: A block to use as a filter if filter is nil.
  #
  # ==== Notes
  # If the filter already exists, its options will be replaced with opts.;
  #
  # @api public
  def self.after(filter = nil, opts = {}, &block)
    add_filter(self._after_filters, filter || block, opts)
  end

  # Adds a filter to the before filter chain.  
  #
  # ==== Parameters
  # filter<Symbol, Proc>:: The filter to add. Defaults to nil.
  # opts<Hash>::
  #   Filter options (see class documentation under <tt>Filter Options</tt>).
  # &block:: A block to use as a filter if filter is nil.
  #
  # ==== Notes
  # If the filter already exists, its options will be replaced with opts.
  #
  # @api public
  def self.before(filter = nil, opts = {}, &block)
    add_filter(self._before_filters, filter || block, opts)
  end
     
  # Removes a filter from the after filter chain.  This removes the 
  # filter from the filter chain for the whole controller and does not 
  # take any options. 
  #
  # ==== Parameters
  # filter<Symbol, String>:: A filter name to skip.
  #
  # @api public
  def self.skip_after(filter)
    skip_filter(self._after_filters, filter)
  end
  
  # Removes a filter from the before filter chain.  This removes the 
  # filter from the filter chain for the whole controller and does not 
  # take any options.
  #
  # ==== Parameters
  # filter<Symbol, String>:: A filter name to skip.
  #
  # @api public
  def self.skip_before(filter)
    skip_filter(self._before_filters , filter)
  end  

  # There are three possible ways to use this method.  First, if you have a named route, 
  # you can specify the route as the first parameter as a symbol and any paramters in a 
  # hash.  Second, you can generate the default route by just passing the params hash, 
  # just passing the params hash.  Finally, you can use the anonymous parameters.  This 
  # allows you to specify the parameters to a named route in the order they appear in the 
  # router.  
  #
  # ==== Parameters(Named Route)
  # name<Symbol>:: 
  #   The name of the route. 
  # args<Hash>:: 
  #   Parameters for the route generation.
  #
  # ==== Parameters(Default Route)
  # args<Hash>:: 
  #   Parameters for the route generation.  This route will use the default route. 
  #
  # ==== Parameters(Anonymous Parameters)
  # name<Symbol>::
  #   The name of the route.  
  # args<Array>:: 
  #   An array of anonymous parameters to generate the route
  #   with. These parameters are assigned to the route parameters
  #   in the order that they are passed.
  #
  # ==== Returns
  # String:: The generated URL.
  #
  # ==== Examples
  # Named Route
  #
  # Merb::Router.prepare do
  #   match("/articles/:title").to(:controller => :articles, :action => :show).name("articles")
  # end
  #
  # url(:articles, :title => "new_article")
  #
  # Default Route
  #
  # Merb::Router.prepare do
  #   default_routes
  # end
  #
  # url(:controller => "articles", :action => "new")
  #
  # Anonymous Paramters
  #
  # Merb::Router.prepare do
  #   match("/articles/:year/:month/:title").to(:controller => :articles, :action => :show).name("articles")
  # end
  #
  # url(:articles, 2008, 10, "test_article")
  #
  # @api public
  def url(name, *args)
    args << {}
    Merb::Router.url(name, *args)
  end
  
  alias_method :relative_url, :url

  # Returns the absolute url including the passed protocol and host.  
  # 
  # This uses the same arguments as the url method, with added requirements 
  # of protocol and host options. 
  #
  # @api public
  def absolute_url(*args)
    # FIXME: arrgh, why request.protocol returns http://?
    # :// is not part of protocol name
    options  = extract_options_from_args!(args) || {}
    protocol = options.delete(:protocol)
    host     = options.delete(:host)
    
    raise ArgumentError, "The :protocol option must be specified" unless protocol
    raise ArgumentError, "The :host option must be specified"     unless host
    
    args << options
    
    protocol + "://" + host + url(*args)
  end
  
  # Generates a URL for a single or nested resource.
  #
  # ==== Parameters
  # resources<Symbol,Object>:: The resources for which the URL
  #   should be generated. These resources should be specified
  #   in the router.rb file using #resources and #resource.
  #
  # options<Hash>:: Any extra parameters that are needed to
  #   generate the URL.
  #
  # ==== Returns
  # String:: The generated URL.
  #
  # ==== Examples
  #
  # Merb::Router.prepare do
  #   resources :users do
  #     resources :comments
  #   end
  # end
  #
  # resource(:users)            # => /users
  # resource(@user)             # => /users/10
  # resource(@user, :comments)  # => /users/10/comments
  # resource(@user, @comment)   # => /users/10/comments/15
  # resource(:users, :new)      # => /users/new
  # resource(:@user, :edit)     # => /users/10/edit
  #
  # @api public
  def resource(*args)
    args << {}
    Merb::Router.resource(*args)
  end

  # Calls the capture method for the selected template engine.
  #
  # ==== Parameters
  # *args:: Arguments to pass to the block.
  # &block:: The block to call.
  #
  # ==== Returns
  # String:: The output of a template block or the return value of a non-template block converted to a string.
  #
  # @api public
  def capture(*args, &block)
    ret = nil

    captured = send("capture_#{@_engine}", *args) do |*args|
      ret = yield *args
    end

    # return captured value only if it is not empty
    captured.empty? ? ret.to_s : captured
  end

  # Calls the concatenate method for the selected template engine.
  #
  # ==== Parameters
  # str<String>:: The string to concatenate to the buffer.
  # binding<Binding>:: The binding to use for the buffer.
  #
  # @api public
  def concat(str, binding)
    send("concat_#{@_engine}", str, binding)
  end

  private
  # adds a filter to the specified filter chain
  # ==== Parameters
  # filters<Array[Filter]>:: The filter chain that this should be added to.
  # filter<Filter>:: A filter that should be added.
  # opts<Hash>::
  #   Filter options (see class documentation under <tt>Filter Options</tt>).
  #
  # ==== Raises
  # ArgumentError::
  #   Both :only and :exclude, or :if and :unless given, if filter is not a
  #   Symbol, String or Proc, or if an unknown option is passed.
  #
  # @api private
  def self.add_filter(filters, filter, opts={})
    raise(ArgumentError,
      "You can specify either :only or :exclude but 
       not both at the same time for the same filter.") if opts.key?(:only) && opts.key?(:exclude)
       
     raise(ArgumentError,
       "You can specify either :if or :unless but 
        not both at the same time for the same filter.") if opts.key?(:if) && opts.key?(:unless)
        
    opts.each_key do |key| raise(ArgumentError,
      "You can only specify known filter options, #{key} is invalid.") unless FILTER_OPTIONS.include?(key)
    end

    opts = normalize_filters!(opts)
    
    case filter
    when Proc
      # filters with procs created via class methods have identical signature
      # regardless if they handle content differently or not. So procs just
      # get appended
      filters << [filter, opts]
    when Symbol, String
      if existing_filter = filters.find {|f| f.first.to_s == filter.to_s}
        filters[ filters.index(existing_filter) ] = [filter, opts]
      else
        filters << [filter, opts]
      end
    else
      raise(ArgumentError, 
        'Filters need to be either a Symbol, String or a Proc'
      )        
    end
  end  

  # Skip a filter that was previously added to the filter chain. Useful in
  # inheritence hierarchies.
  #
  # ==== Parameters
  # filters<Array[Filter]>:: The filter chain that this should be removed from.
  # filter<Filter>:: A filter that should be removed.
  #
  # ==== Raises
  # ArgumentError:: filter not Symbol or String.
  #
  # @api private
  def self.skip_filter(filters, filter)
    raise(ArgumentError, 'You can only skip filters that have a String or Symbol name.') unless
      [Symbol, String].include? filter.class

    Merb.logger.warn("Filter #{filter} was not found in your filter chain.") unless
      filters.reject! {|f| f.first.to_s[filter.to_s] }
  end

  # Ensures that the passed in hash values are always arrays.
  #
  # ==== Parameters
  # opts<Hash>:: Options for the filters (see below).
  #
  # ==== Options (opts)
  # :only<Symbol, Array[Symbol]>:: A list of actions.
  # :exclude<Symbol, Array[Symbol]>:: A list of actions.
  #
  # ==== Examples
  #   normalize_filters!(:only => :new) #=> {:only => [:new]}
  #
  # @api public
  def self.normalize_filters!(opts={})
    opts[:only]     = Array(opts[:only]).map {|x| x.to_s} if opts[:only]
    opts[:exclude]  = Array(opts[:exclude]).map {|x| x.to_s} if opts[:exclude]
    return opts
  end
end
