module Mobylette
  # Defines the behavior for responding to mobile requests with a different view.
  #
  # By including this to a controller, it will automaticaly look for a .mobile view
  # instead of a .html, when the request comes from a mobile device.
  #
  # Usage:
  #
  #   class AppicationController < ActionController::Base
  #     include Mobylette::RespondToMobileRequests
  #   end
  #
  #
  # You can define some options:
  #
  #   class...
  #     include Mobylette::RespondToMobileRequests
  #     mobylette_config do |config|
  #       config[:fall_back] = :html
  #       config[:skip_xhr_requests] = false
  #       config[:mobile_user_agents] = proc { /iphone/i }
  #     end
  #     ...
  #   end
  #
  # note: By default it will already fall back to the :html format
  #
  module RespondToMobileRequests
    extend ActiveSupport::Concern

    included do
      helper_method :is_mobile_request?
      helper_method :is_mobile_view?
      helper_method :request_device?

      before_filter :handle_mobile

      cattr_accessor :mobylette_options
      @@mobylette_options = Hash.new
      @@mobylette_options[:skip_xhr_requests]  = true
      @@mobylette_options[:fallback_chains]    = { mobile: [:mobile, :html] }
      @@mobylette_options[:mobile_user_agents] = Mobylette::MobileUserAgents.new
      @@mobylette_options[:devices]            = Hash.new
      @@mobylette_options[:skip_user_agents]   = []

      cattr_accessor :mobylette_resolver
      self.mobylette_resolver = Mobylette::Resolvers::ChainedFallbackResolver.new({}, self.view_paths)
      self.mobylette_resolver.replace_fallback_formats_chain(@@mobylette_options[:fallback_chains])
      append_view_path self.mobylette_resolver
    end

    module ClassMethods
      # This method enables the controller do handle mobile requests
      #
      # You must add this to every controller you want to respond differently to mobile devices,
      # or make it application wide calling it from the ApplicationController
      #
      # Possible options:
      # * fall_back: :html
      #     You may pass a fall_back option to the method, it will force the render
      #     to look for that other format, in case there is not a .mobile file for the view.
      #     By default, it will fall back to the html format.
      #     If you don't want fall back at all, pass fall_back: false
      # * skip_xhr_requests: true/false
      #     By default this is set to true. When a xhr request enters in, it will skip the
      #     mobile verification. This will let your ajax calls to work as intended.
      #     You may disable this (actually you will have to) if you are using JQuery Mobile, or
      #     other js framework that uses ajax. To disable, set skip_xhr_requests: false
      # * mobile_user_agents: proc { /user_agents_match/ }
      #     If you want to restrict the user agents that will be considered mobile devices,
      #     you can send a custom proc/object that returns the matching regex you wish.
      # * devices: {device_name: /device_reg/, device2_name: /device2_reg/, ...}
      #     You may register devices for custom behavior by device.
      #     Once a device is registered you may call the helper request_device?(device_symb)
      #     to see if the request comes from that device or not.
      #     By default :iphone, :ipad, :ios and :android are already registered.
      # * skip_user_agents: [:ipad, :android]
      #     Don't consider these user agents mobile devices.
      #
      # Example Usage:
      #
      #   class ApplicationController...
      #     include Mobylette::RespondToMobileRequests
      #     ...
      #     mobylette_config do |config|
      #       config[:fall_back] = :html
      #       config[:skip_xhr_requests] = false
      #       config[:mobile_user_agents] = proc { %r{iphone|android}i }
      #       config[:devices] = {cool_phone: %r{cool\s+phone} }
      #     end
      #     ...
      #   end
      #
      def mobylette_config
        yield(self.mobylette_options)
        configure_fallback_resolver(self.mobylette_options)
        Mobylette.devices.register(self.mobylette_options[:devices]) if self.mobylette_options[:devices]
      end

      private

      # Private: Configures how the resolver shall handle fallbacks.
      #
      # if options has a :fallback_chains key, it will use it 
      # as the fallback rules for the resolver, the format should
      # be a hash, where each key defines a array of formats.
      # Example:
      #   options[:fallback_chains]
      #   #=> { iphone: [:iphone, :mobile, :html], mobile: [:mobile, :html] }
      #
      # if there is no :fallback_chains, then it will look for a :fall_back
      # key, that shall define only 1 fallback format to the mobile format.
      # This keep compatibility with older versions.
      # Example:
      #   options[:fall_back]
      #   #=> :html
      #
      def configure_fallback_resolver(options)
        if options[:fall_back]
          logger.warn "DEPRECATED > Mobylette: Please don't user :fall_back to configure fall backs any more. See the README for :fallback_chains instead."
          self.mobylette_resolver.replace_fallback_formats_chain({ mobile: [:mobile, options[:fall_back]] })
        else
          if options[:fallback_chains]
            self.mobylette_resolver.replace_fallback_formats_chain((options[:fallback_chains] || {}).reverse_merge({ mobile: [:mobile, :html] }))
          end
        end
      end
    end

    private

    # Private: Tells if the request comes from a mobile user_agent or not
    #
    def is_mobile_request?
      (not user_agent_excluded?) && !(request.user_agent.to_s.downcase =~ @@mobylette_options[:mobile_user_agents].call).nil?
    end

    # Private: Returns if this request comes from the informed device
    #
    # devive - device symbol. It must be previously registered as a device.
    #
    def request_device?(device)
      (request.user_agent.to_s.downcase =~ (Mobylette.devices.device(device) || %r{not_to_be_matched_please}) ? true : false)
    end

    # Private: Helper method that tells if the currently view is mobile or not
    #
    def is_mobile_view?
      true if (params[:format] == "mobile") || (request.format.to_s == "mobile")
    end

    # Private: This is the method that tells if the request will be threated as mobile
    #          or not
    #
    def respond_as_mobile?
      impediments = stop_processing_because_xhr? || stop_processing_because_param?
      (not impediments) && (force_mobile_by_session? || is_mobile_request? || params[:format] == 'mobile')
    end

    # Private: Rertuns true if the current user agent should be skipped by configuration
    #
    def user_agent_excluded?
      request.user_agent.to_s.downcase =~ Regexp.union([self.mobylette_options[:skip_user_agents]].flatten.map(&:to_s))
    end

    # Private: Returns true if the visitor has the force_mobile set in it's session
    #
    def force_mobile_by_session?
      session[:mobylette_override] == :force_mobile
    end

    # Private: Tells when mobylette should not interfere in the rendering
    #          process because the `skip_mobile` param is set to true
    #
    # Passing :skip_mobile = true to a request will not render it as a mobile
    #
    def stop_processing_because_param?
      return true if params[:skip_mobile] == 'true'
      false
    end

    # Private: Tells when mobylette should or not interfere in the rendering
    #          process because of a xhr request.
    #
    # if the request is not xhr this will aways return false
    # this will only return true for xhr requests, when you explicit want to
    # not skip_xhr_requests.
    #
    def stop_processing_because_xhr?
      if request.xhr? && self.mobylette_options[:skip_xhr_requests]
        true
      else
        false
      end
    end

    # Private: Process the request as mobile
    #
    def handle_mobile
      return if session[:mobylette_override] == :ignore_mobile
      if respond_as_mobile?
        request.format = set_mobile_format
      end
    end

    # Private: Checks if there are fallback_chains defined.
    # if there is, it will try to match the request browser
    # agains a fallback_chain key.
    #
    def set_mobile_format
      if self.mobylette_options[:fallback_chains]
        self.mobylette_options[:fallback_chains].keys.each do |device|
          return device if request_device?(device)
        end
      end
      :mobile 
    end

  end
end
