module CanCan
  # Handle the load and authorization controller logic
  # so we don't clutter up all controllers with non-interface methods.
  # This class is used internally, so you do not need to call methods directly on it.
  class ControllerResource # :nodoc:
    def self.add_before_action(controller_class, method, *args)
      options = args.extract_options!
      resource_name = args.first
      before_action_method = before_callback_name(options)
      controller_class.send(before_action_method, options.slice(:only, :except, :if, :unless)) do |controller|
        controller.class.cancan_resource_class
                  .new(controller, resource_name, options.except(:only, :except, :if, :unless)).send(method)
      end
    end

    def self.before_callback_name(options)
      if ActiveSupport.respond_to?(:version) && ActiveSupport.version >= Gem::Version.new('4')
        options.delete(:prepend) ? :prepend_before_action : :before_action
      else
        options.delete(:prepend) ? :prepend_before_filter : :before_filter
      end
    end

    def initialize(controller, *args)
      @controller = controller
      @params = controller.params
      @options = args.extract_options!
      @name = args.first
      nested_err = 'The :nested option is no longer supported, instead use :through with separate load/authorize call.'
      raise CanCan::ImplementationRemoved, nested_err if @options[:nested]
      name_err = 'The :name option is no longer supported, instead pass the name as the first argument.'
      raise CanCan::ImplementationRemoved, name_err if @options[:name]
      resource_err = 'The :resource option has been renamed back to :class, use false if no class.'
      raise CanCan::ImplementationRemoved, resource_err if @options[:resource]
    end

    def load_and_authorize_resource
      load_resource
      authorize_resource
    end

    def load_resource
      return if skip?(:load)
      if load_instance?
        self.resource_instance ||= load_resource_instance
      elsif load_collection?
        self.collection_instance ||= load_collection
      end
    end

    def authorize_resource
      return if skip?(:authorize)
      @controller.authorize!(authorization_action, resource_instance || resource_class_with_parent)
    end

    def parent?
      @options.key?(:parent) ? @options[:parent] : @name && @name != name_from_controller.to_sym
    end

    def skip?(behavior)
      return false unless (options = @controller.class.cancan_skipper[behavior][@name])
      options == {} ||
        options[:except] && !action_exists_in?(options[:except]) ||
        action_exists_in?(options[:only])
    end

    protected

    def load_resource_instance
      if !parent? && new_actions.include?(@params[:action].to_sym)
        build_resource
      elsif id_param || @options[:singleton]
        find_resource
      end
    end

    def load_instance?
      parent? || member_action?
    end

    def load_collection?
      resource_base.respond_to?(:accessible_by) && !current_ability.has_block?(authorization_action, resource_class)
    end

    def load_collection
      resource_base.accessible_by(current_ability, authorization_action)
    end

    def build_resource
      resource = resource_base.new(resource_params || {})
      assign_attributes(resource)
    end

    def assign_attributes(resource)
      resource.send("#{parent_name}=", parent_resource) if @options[:singleton] && parent_resource
      initial_attributes.each do |attr_name, value|
        resource.send("#{attr_name}=", value)
      end
      resource
    end

    def initial_attributes
      current_ability.attributes_for(@params[:action].to_sym, resource_class).delete_if do |key, _value|
        resource_params && resource_params.include?(key)
      end
    end

    def find_resource
      if @options[:singleton] && parent_resource.respond_to?(name)
        parent_resource.send(name)
      elsif @options[:find_by]
        find_resource_using_find_by
      else
        adapter.find(resource_base, id_param)
      end
    end

    def find_resource_using_find_by
      if resource_base.respond_to? "find_by_#{@options[:find_by]}!"
        resource_base.send("find_by_#{@options[:find_by]}!", id_param)
      elsif resource_base.respond_to? 'find_by'
        resource_base.send('find_by', @options[:find_by].to_sym => id_param)
      else
        resource_base.send(@options[:find_by], id_param)
      end
    end

    def adapter
      ModelAdapters::AbstractAdapter.adapter_class(resource_class)
    end

    def authorization_action
      parent? ? parent_authorization_action : @params[:action].to_sym
    end

    def parent_authorization_action
      @options[:parent_action] || :show
    end

    def id_param
      @params[id_param_key].to_s if @params[id_param_key]
    end

    def id_param_key
      if @options[:id_param]
        @options[:id_param]
      else
        parent? ? :"#{name}_id" : :id
      end
    end

    def member_action?
      new_actions.include?(@params[:action].to_sym) || @options[:singleton] ||
        ((@params[:id] || @params[@options[:id_param]]) &&
          !collection_actions.include?(@params[:action].to_sym))
    end

    # Returns the class used for this resource. This can be overriden by the :class option.
    # If +false+ is passed in it will use the resource name as a symbol in which case it should
    # only be used for authorization, not loading since there's no class to load through.
    def resource_class
      case @options[:class]
      when false then
        name.to_sym
      when nil then
        namespaced_name.to_s.camelize.constantize
      when String then
        @options[:class].constantize
      else
        @options[:class]
      end
    end

    def resource_class_with_parent
      parent_resource ? { parent_resource => resource_class } : resource_class
    end

    def resource_instance=(instance)
      @controller.instance_variable_set("@#{instance_name}", instance)
    end

    def resource_instance
      @controller.instance_variable_get("@#{instance_name}") if load_instance?
    end

    def collection_instance=(instance)
      @controller.instance_variable_set("@#{instance_name.to_s.pluralize}", instance)
    end

    def collection_instance
      @controller.instance_variable_get("@#{instance_name.to_s.pluralize}")
    end

    # The object that methods (such as "find", "new" or "build") are called on.
    # If the :through option is passed it will go through an association on that instance.
    # If the :shallow option is passed it will use the resource_class if there's no parent
    # If the :singleton option is passed it won't use the association because it needs to be handled later.
    def resource_base
      if @options[:through]
        resource_base_through
      else
        resource_class
      end
    end

    def resource_base_through
      if parent_resource
        base = if @options[:singleton]
                 resource_class
               else
                 parent_resource.send(@options[:through_association] || name.to_s.pluralize)
               end
        base = base.scoped if base.respond_to?(:scoped) && active_record_3?
        base
      elsif @options[:shallow]
        resource_class
      else
        # maybe this should be a record not found error instead?
        raise AccessDenied.new(nil, authorization_action, resource_class)
      end
    end

    def active_record_3?
      defined?(ActiveRecord) && ActiveRecord::VERSION::MAJOR == 3
    end

    def parent_name
      @options[:through] && [@options[:through]].flatten.detect { |i| fetch_parent(i) }
    end

    # The object to load this resource through.
    def parent_resource
      parent_name && fetch_parent(parent_name)
    end

    def fetch_parent(name)
      if @controller.instance_variable_defined? "@#{name}"
        @controller.instance_variable_get("@#{name}")
      elsif @controller.respond_to?(name, true)
        @controller.send(name)
      end
    end

    def current_ability
      @controller.send(:current_ability)
    end

    def name
      @name || name_from_controller
    end

    def resource_params
      if parameters_require_sanitizing? && params_method.present?
        case params_method
        when Symbol then
          @controller.send(params_method)
        when String then
          @controller.instance_eval(params_method)
        when Proc then
          params_method.call(@controller)
        end
      else
        resource_params_by_namespaced_name
      end
    end

    def parameters_require_sanitizing?
      save_actions.include?(@params[:action].to_sym) || resource_params_by_namespaced_name.present?
    end

    def resource_params_by_namespaced_name
      if @options[:instance_name] && @params.key?(extract_key(@options[:instance_name]))
        @params[extract_key(@options[:instance_name])]
      elsif @options[:class] && @params.key?(extract_key(@options[:class]))
        @params[extract_key(@options[:class])]
      else
        @params[extract_key(namespaced_name)]
      end
    end

    def params_method
      params_methods.each do |method|
        return method if (method.is_a?(Symbol) && @controller.respond_to?(method, true)) ||
                         method.is_a?(String) || method.is_a?(Proc)
      end
      nil
    end

    def params_methods
      methods = ["#{@params[:action]}_params".to_sym, "#{name}_params".to_sym, :resource_params]
      methods.unshift(@options[:param_method]) if @options[:param_method].present?
      methods
    end

    def namespace
      @params[:controller].split('/')[0..-2]
    end

    def namespaced_name
      [namespace, name].join('/').singularize.camelize.safe_constantize || name
    end

    def name_from_controller
      @params[:controller].split('/').last.singularize
    end

    def instance_name
      @options[:instance_name] || name
    end

    def collection_actions
      [:index] + Array(@options[:collection])
    end

    def new_actions
      [:new, :create] + Array(@options[:new])
    end

    def save_actions
      [:create, :update]
    end

    private

    def action_exists_in?(options)
      Array(options).include?(@params[:action].to_sym)
    end

    def extract_key(value)
      value.to_s.underscore.tr('/', '_')
    end
  end
end