require "set"
require "yaml"
require "pathname"

require "browser/middleware"
require "browser/middleware/context"
require "browser/rails" if defined?(::Rails)

require "browser/methods/ie"
require "browser/methods/platform"
require "browser/methods/mobile"
require "browser/methods/devices"
require "browser/methods/consoles"
require "browser/methods/language"
require "browser/methods/bots"
require "browser/methods/tv"

require "browser/meta/base"
require "browser/meta/generic_browser"
require "browser/meta/id"
require "browser/meta/ie"
require "browser/meta/ios"
require "browser/meta/mobile"
require "browser/meta/modern"
require "browser/meta/platform"
require "browser/meta/safari"
require "browser/meta/webkit"

class Browser
  include IE
  include Platform
  include Mobile
  include Devices
  include Consoles
  include Language
  include Bots
  include Tv

  # Set browser's UA string.
  attr_accessor :user_agent
  alias_method :ua, :user_agent
  alias_method :ua=, :user_agent=

  NAMES = {
    edge: "Microsoft Edge",   # Must come before everything
    ie: "Internet Explorer",  # Must come before android
    chrome: "Chrome",         # Must come before android
    firefox: "Firefox",       # Must come before android
    android: "Android",
    blackberry: "BlackBerry",
    core_media: "Apple CoreMedia",
    ipad: "iPad",
    iphone: "iPhone",
    ipod: "iPod Touch",
    nintendo: "Nintendo",
    opera: "Opera",
    phantom_js: "PhantomJS",
    psp: "PlayStation Portable",
    playstation: "PlayStation",
    quicktime: "QuickTime",
    safari: "Safari",
    xbox: "Xbox",

    # This must be last item, since Ruby 1.9+ has ordered keys.
    other: "Other",
  }

  VERSIONS = {
    edge: %r[Edge/([\d.]+)],
    chrome: %r[(?:Chrome|CriOS)/([\d.]+)],
    default: %r[(?:Version|MSIE|Firefox|QuickTime|BlackBerry[^/]+|CoreMedia v|PhantomJS|AdobeAIR)[/ ]?([a-z0-9.]+)]i,
    opera: %r[(?:Opera/.*? Version/([\d.]+)|Chrome/.*?OPR/([\d.]+))],
    ie: %r[(?:MSIE |Trident/.*?; rv:)([\d.]+)]
  }

  # Define the rules which define a modern browser.
  # A rule must be a proc/lambda or any object that implements the method
  # === and accepts the browser object.
  #
  # To redefine all rules, clear the existing rules before adding your own.
  #
  #   # Only Chrome Canary is considered modern.
  #   Browser.modern_rules.clear
  #   Browser.modern_rules << -> b { b.chrome? && b.version >= '37' }
  #
  def self.modern_rules
    @modern_rules ||= []
  end

  self.modern_rules.tap do |rules|
    rules << -> b { b.webkit? }
    rules << -> b { b.firefox? && b.version.to_i >= 17 }
    rules << -> b { b.ie? && b.version.to_i >= 9 && !b.compatibility_view? }
    rules << -> b { b.edge? && !b.compatibility_view? }
    rules << -> b { b.opera? && b.version.to_i >= 12 }
    rules << -> b { b.firefox? && b.tablet? && b.android? && b.version.to_i >= 14 }
  end

  # Create a new browser instance and set
  # the UA and Accept-Language headers.
  #
  #   browser = Browser.new({
  #     :ua => "Safari",
  #     :accept_language => "pt-br"
  #   })
  #
  def initialize(options = {}, &block)
    self.user_agent = (options[:user_agent] || options[:ua]).to_s
    self.accept_language = options[:accept_language].to_s

    yield self if block
  end

  # Get readable browser name.
  def name
    NAMES[id]
  end

  # Get the browser identifier.
  def id
    NAMES.keys
      .find {|id| respond_to?("#{id}?") ? public_send("#{id}?") : id }
  end

  # Return major version.
  def version
    if ie?
      ie_version
    else
      full_version.to_s.split(".").first
    end
  end

  # Return the full version.
  def full_version
    if ie?
      ie_full_version
    else
      _, *v = *ua.match(VERSIONS.fetch(id, VERSIONS[:default]))
      v.compact.first || "0.0"
    end
  end

  # Return true if browser is modern (Webkit, Firefox 17+, IE9+, Opera 12+).
  def modern?
    self.class.modern_rules.any? {|rule| rule === self }
  end

  # Detect if browser is WebKit-based.
  def webkit?
    ua =~ /AppleWebKit/i && !edge?
  end

  # Detect if browser is QuickTime
  def quicktime?
    !!(ua =~ /QuickTime/i)
  end

  # Detect if browser is Apple CoreMedia.
  def core_media?
    !!(ua =~ /CoreMedia/)
  end

  # Detect if browser is PhantomJS
  def phantom_js?
    !!(ua =~ /PhantomJS/)
  end

  # Detect if browser is Safari.
  def safari?
    (ua =~ /Safari/ || safari_webapp_mode?) && ua !~ /Android|Chrome|CriOS|PhantomJS/
  end

  def safari_webapp_mode?
    (ipad? || iphone?) && ua =~ /AppleWebKit/
  end

  # Detect if browser is Firefox.
  def firefox?
    !!(ua =~ /Firefox/)
  end

  # Detect if browser is Chrome.
  def chrome?
    ua =~ /Chrome|CriOS/ && !opera? && !edge?
  end

  # Detect if browser is Opera.
  def opera?
    !!(ua =~ /(Opera|OPR)/)
  end

  # Detect if browser is Silk.
  def silk?
    !!(ua =~ /Silk/)
  end

  def known?
    id != :other
  end

  # Return a meta info about this browser.
  def meta
    Meta.constants.each_with_object(Set.new) do |meta_name, meta|
      meta_class = Meta.const_get(meta_name)
      meta.merge(meta_class.new(self).to_a)
    end.to_a
  end

  alias_method :to_a, :meta

  # Return meta representation as string.
  def to_s
    meta.to_a.join(" ")
  end
end
