require 'watir'
require 'page-object/version'
require 'page-object/accessors'
require 'page-object/element_locators'
require 'page-object/nested_elements'
require 'page-object/page_factory'
require 'page-object/page_populator'
require 'page-object/javascript_framework_facade'
require 'page-object/indexed_properties'
require 'page-object/section_collection'
require 'page-object/widgets'

require 'page-object/platforms/watir'
require 'page-object/platforms/watir/page_object'

#
# Module that when included adds functionality to a page object.  This module
# will add numerous class and instance methods that you use to define and
# interact with web pages.
#
# If we have a login page with a username and password textfield and a login
# button we might define our page like the one below.  We can then interact with
# the object using the generated methods.
#
# @example Login page example
#   class LoginPage
#     include PageObject
#
#     text_field(:username, :id => 'user')
#     text_field(:password, :id => 'pass')
#     button(:login, :value => 'Login')
#   end
#
#   ...
#
#   browser = Watir::Browser.new :firefox
#   login_page = LoginPage.new(browser)
#   login_page.username = 'cheezy'
#   login_page.password = 'secret'
#   login_page.login
#
# @see PageObject::Accessors to see what class level methods are added to this module at runtime.
#
module PageObject
  include ElementLocators
  include PagePopulator

  def method_missing(method, *args, &block)
    if @root_element.respond_to?(method)
      @root_element.send(method, *args, &block)
    else
      super
    end
  end

  def respond_to_missing?(method, include_all = false)
    @root_element && @root_element.respond_to?(method) || super
  end

  # @return the platform browser passed to the constructor
  attr_reader :browser
  # @return [PageObject::WatirPageObject] the platform page object
  attr_reader :platform

  #
  # Construct a new page object.  Prior to browser initialization it will call
  # a method named initialize_accessors if it exists. Upon initialization of
  # the page it will call a method named initialize_page if it exists.
  #
  # @param [Watir::Browser, Watir::HTMLElement or Selenium::WebDriver::Driver, Selenium::WebDriver::Element] the platform browser/element to use
  # @param [bool] open the page if page_url is set
  #
  def initialize(root, visit=false)
    initialize_accessors if respond_to?(:initialize_accessors)
    initialize_browser(root)
    goto if visit && self.class.instance_methods(false).include?(:goto)
    initialize_page if respond_to?(:initialize_page)
  end

  def initialize_browser(root)
    @root_element = PageObject::Platforms::Watir.root_element_for root
    @browser = root 
    @platform = PageObject::Platforms::Watir.create_page_object @browser
  end

  # @private
  def self.included(cls)
    cls.extend PageObject::Accessors
  end

  #
  # Set the default timeout for page level waits
  #
  def self.default_page_wait=(timeout)
    @page_wait = timeout
  end

  #
  # Returns the default timeout for page lavel waits
  #
  def self.default_page_wait
    @page_wait ||= 30
  end

  #
  # Sets the default timeout for element level waits
  #
  def self.default_element_wait=(timeout)
    @element_wait = timeout
  end

  #
  # Returns the default timeout for element level waits
  #
  def self.default_element_wait
    @element_wait ||= 5
  end

  #
  # Set the javascript framework to use when determining number of
  # ajax requests.  Valid frameworks are :jquery, :prototype, :yui,
  # and :angularjs
  #
  def self.javascript_framework=(framework)
    PageObject::JavascriptFrameworkFacade.framework = framework
  end

  #
  # Add a new javascript framework to page-object.  The module passed
  # in must adhere to the same prototype as the JQuery and Prototype
  # modules.
  #
  # @param [Symbol] the name used to reference the framework in
  # subsequent calls
  # @param [Module] a module that has the necessary methods to perform
  # the required actions.
  #
  def self.add_framework(key, framework)
    PageObject::JavascriptFrameworkFacade.add_framework(key, framework)
  end

  #
  # get the current page url
  #
  def current_url
    platform.current_url
  end

  #
  # navigate to the provided url
  #
  # @param [String] the full url to navigate to
  #
  def navigate_to(url)
    platform.navigate_to(url)
  end

  #
  # Returns the text of the current page
  #
  def text
    platform.text
  end

  #
  # Returns the html of the current page
  #
  def html
    platform.html
  end

  #
  # Returns the title of the current page
  #
  def title
    platform.title
  end

  #
  # Wait until the block returns true or times out
  #
  # @example
  #   @page.wait_until(5, 'Success not found on page') do
  #     @page.text.include? 'Success'
  #   end
  #
  # @param [Numeric] the amount of time to wait for the block to return true.
  # @param [String] the message to include with the error if we exceed the timeout duration.
  # @param block the block to execute.  It should return true when successful.
  #
  def wait_until(timeout = PageObject.default_page_wait, message = nil, &block)
    platform.wait_until(timeout, message, &block)
  end


  #
  # Wait until there are no pending ajax requests.  This requires you
  # to set the javascript framework in advance.
  #
  # @param [Numeric] the amount of time to wait for the block to return true.
  # @param [String] the message to include with the error if we exceed
  # the timeout duration.
  #
  def wait_for_ajax(timeout = 30, message = nil)
    end_time = ::Time.now + timeout
    until ::Time.now > end_time
      return if browser.execute_script(::PageObject::JavascriptFrameworkFacade.pending_requests) == 0
      sleep 0.5
    end
    message = "Timed out waiting for ajax requests to complete" unless message
    raise message
  end

  #
  # Override the normal alert popup so it does not occur.
  #
  # @example
  #   message = @page.alert do
  #     @page.button_that_causes_alert
  #   end
  #
  # @param frame optional parameter used when alert is nested within a frame
  # @param block a block that has the call that will cause the alert to display
  # @return [String] the message that was contained in the alert
  #
  def alert(frame=nil, &block)
    platform.alert(frame, &block)
  end

  #
  # Override the normal confirm popup so it does not occur.
  #
  # @example
  #   message = @popup.confirm(true) do
  #     @page.button_that_causes_confirm
  #   end
  #
  # @param [bool] what response you want to return back from the confirm popup
  # @param frame optional parameter used when the confirm is nested within a frame
  # @param block a block that has the call that will cause the confirm to display
  # @return [String] the message that was prompted in the confirm
  #
  def confirm(response, frame=nil, &block)
    platform.confirm(response, frame, &block)
  end

  #
  # Override the normal prompt popup so it does not occur.
  #
  # @example
  #   message = @popup.prompt("Some Value") do
  #     @page.button_that_causes_prompt
  #   end
  #
  # @param [string] the value returned to the caller of the prompt
  # @param frame optional parameter used with the prompt is nested within a frame
  # @param block a block that has the call that will cause the prompt to display
  # @return [Hash] A has containing two keys - :message contains the prompt message and
  # :default_value contains the default value for the prompt if provided
  #
  def prompt(answer, frame=nil, &block)
    platform.prompt(answer, frame, &block)
  end

  #
  # Execute javascript on the browser
  #
  # @example Get inner HTML of element
  #   span = @page.span_element
  #   @page.execute_script "return arguments[0].innerHTML", span
  #   #=> "Span innerHTML"
  #
  def execute_script(script, *args)
    args.map! { |e| e.kind_of?(PageObject::Elements::Element) ? e.element : e }
    platform.execute_script(script, *args)
  end

  #
  # Identify an element as existing within a frame.  A frame parameter
  # is passed to the block and must be passed to the other calls to PageObject.
  # You can nest calls to in_frame by passing the frame to the next level.
  #
  # @example
  #   in_frame(:id => 'frame_id') do |frame|
  #     text_field_element(:id => 'fname', :frame => frame)
  #   end
  #
  # @param [Hash] identifier how we find the frame.  The valid keys are:
  #   * :id
  #   * :index
  #   * :name
  #   * :class
  # @param frame passed from a previous call to in_frame.  Used to nest calls
  # @param block that contains the calls to elements that exist inside the frame.
  #
  def in_frame(identifier, frame=nil, &block)
    platform.in_frame(identifier, frame, &block)
  end

  # Identify an element as existing within an iframe.  A frame parameter
  # is passed to the block and must be passed to the other calls to PageObject.
  # You can nest calls to in_iframe by passing the frame to the next level.
  #
  # @example
  #   in_iframe(:id => 'iframe_id') do |iframe|
  #     text_field_element(:id => 'ifname', :frame => iframe)
  #   end
  #
  # @param [Hash] identifier how we find the iframe.  The valid keys are:
  #   * :id
  #   * :index
  #   * :name
  #   * :class
  # @param frame passed from a previous call to in_iframe.  Used to nest calls
  # @param block that contains the calls to elements that exist inside the iframe.
  #
  def in_iframe(identifier, frame=nil, &block)
    platform.in_iframe(identifier, frame, &block)
  end

  #
  # Override the normal showModalDialog call is it opens a window instead
  # of a dialog.  You will need to attach to the new window in order to
  # continue.
  #
  # @example
  #   @page.modal_dialog do
  #     @page.action_that_spawns_the_modal
  #   end
  #
  # @param block a block that contains the call that will cause the modal dialog.
  #
  def modal_dialog(&block)
    script =
        %Q{
      window.showModalDialog = function(sURL, vArguments, sFeatures) {
        window.dialogArguments = vArguments;
        modalWin = window.open(sURL, 'modal', sFeatures);
        return modalWin;
      }
    }
    browser.execute_script script
    yield if block_given?
  end

  #
  # Attach to a running window.  You can locate the window using either
  # the window's title or url.  If it fails to connect to a window it will
  # pause for 1 second and try again.
  #
  # @example
  #     page.attach_to_window(:title => "other window's title")
  #
  # @param [Hash] either :title or :url of the other window.  The url does not need to
  # be the entire url - it can just be the page name like index.html
  # @param block if present the block is executed and then execution is returned to the
  # calling window
  #
  def attach_to_window(identifier, &block)
    begin
      platform.attach_to_window(identifier, &block)
    rescue
      sleep 1
      platform.attach_to_window(identifier, &block)
    end
  end

  #
  # Find the element that has focus on the page
  #
  def element_with_focus
    platform.element_with_focus
  end

  #
  # Refresh to current page
  #
  def refresh
    platform.refresh
  end

  #
  # Go back to the previous page
  #
  def back
    platform.back
  end

  #
  # Go forward to the next page
  #
  def forward
    platform.forward
  end

  #
  # Clear the cookies from the browser
  #
  def clear_cookies
    platform.clear_cookies
  end

  #
  # Save the current screenshot to the provided url.  File
  # is saved as a png file.
  #
  def save_screenshot(file_name)
    platform.save_screenshot file_name
  end

  #
  # Check if root element exists and is visible
  #
  def present?
    root.present?
  end

  def self.register_widget(widget_tag, widget_class, base_element_tag)
    Widgets.register_widget(widget_tag, widget_class, base_element_tag)
  end

  private

  def root
    @root_element || PageObject::Platforms::Watir.browser_root_for(browser)
  end

  def call_block(&block)
    block.arity == 1 ? block.call(self) : self.instance_eval(&block)
  end
end
