module Selenium
  module WebDriver
    module Remote

      COMMANDS = {}

      #
      # Low level bridge to the remote server, through which the rest of the API works.
      #
      # @private
      #

      class Bridge
        include Find
        include BridgeHelper

        #
        # Defines a wrapper method for a command, which ultimately calls #execute.
        #
        # @param name [Symbol]
        #   name of the resulting method
        # @param url [String]
        #   a URL template, which can include some arguments, much like the definitions on the server.
        #   the :session_id parameter is implicitly handled, but the remainder will become required method arguments.
        # @param verb [Symbol]
        #   the appropriate http verb, such as :get, :post, or :delete
        #

        def self.command(name, verb, url)
          COMMANDS[name] = [verb, url.freeze]
        end

        attr_accessor :context, :http
        attr_reader :capabilities

        #
        # Initializes the bridge with the given server URL.
        #
        # @param url         [String] url for the remote server
        # @param http_client [Class] an HTTP client class that implements the same interface as DefaultHttpClient
        # @param desired_capabilities [Capabilities] an instance of Remote::Capabilities describing the capabilities you want
        #

        def initialize(opts = {})
          opts                 = default_options.merge(opts)
          http_client_class    = opts.delete(:http_client)
          desired_capabilities = opts.delete(:desired_capabilities)
          url                  = opts.delete(:url)

          unless opts.empty?
            raise ArgumentError, "unknown option#{'s' if opts.size != 1}: #{opts.inspect}"
          end

          if desired_capabilities.kind_of?(Symbol)
            unless Capabilities.respond_to?(desired_capabilities)
              raise Error::WebDriverError, "invalid desired capability: #{desired_capabilities.inspect}"
            end

            desired_capabilities = Capabilities.send(desired_capabilities)
          end

          uri = URI.parse(url)
          uri.path += "/" unless uri.path =~ /\/$/

          @http         = http_client_class.new uri
          @capabilities = create_session(desired_capabilities)
        end

        def browser
          @browser ||= @capabilities.browser_name.gsub(" ", "_").to_sym
        end

        def driver_extensions
          []
        end

        #
        # Returns the current session ID.
        #

        def session_id
          @session_id || raise(Error::WebDriverError, "no current session exists")
        end

        def create_session(desired_capabilities)
          resp = raw_execute :newSession, {}, :desiredCapabilities => desired_capabilities
          @session_id = resp['sessionId'] || raise(Error::WebDriverError, 'no sessionId in returned payload')

          Capabilities.json_create resp['value']
        end

        def get(url)
          execute :get, {}, :url => url
        end

        def goBack
          execute :goBack
        end

        def goForward
          execute :goForward
        end

        def getCurrentUrl
          execute :getCurrentUrl
        end

        def getTitle
          execute :getTitle
        end

        def getPageSource
          execute :getPageSource
        end

        def getVisible
          execute :getVisible
        end

        def setVisible(bool)
          execute :setVisible, {}, bool
        end

        def switchToWindow(name)
          execute :switchToWindow, {}, :name => name
        end

        def switchToFrame(id)
          execute :switchToFrame, {}, :id => id
        end

        def switchToDefaultContent
          execute :switchToFrame, {}, :id => nil
        end

        def quit
          execute :quit
        end

        def close
          execute :close
        end

        def refresh
          execute :refresh
        end

        def getWindowHandles
          execute :getWindowHandles
        end

        def getCurrentWindowHandle
          execute :getCurrentWindowHandle
        end

        def setSpeed(value)
          execute :setSpeed, {}, :speed => value
        end

        def getSpeed
          execute :getSpeed
        end

        def executeScript(script, *args)
          unless capabilities.javascript?
            raise Error::UnsupportedOperationError, "underlying webdriver instance does not support javascript"
          end

          result = execute :executeScript, {}, :script => script, :args => args
          unwrap_script_result result
        end

        def addCookie(cookie)
          execute :addCookie, {}, :cookie => cookie
        end

        def deleteCookie(name)
          execute :deleteCookieNamed, :name => name
        end

        def getAllCookies
          execute :getAllCookies
        end

        def deleteAllCookies
          execute :deleteAllCookies
        end

        def findElementByClassName(parent, class_name)
          find_element_by 'class name', class_name, parent
        end

        def findElementsByClassName(parent, class_name)
          find_elements_by 'class name', class_name, parent
        end

        def findElementById(parent, id)
          find_element_by 'id', id, parent
        end

        def findElementsById(parent, id)
          find_elements_by 'id', id, parent
        end

        def findElementByLinkText(parent, link_text)
          find_element_by 'link text', link_text, parent
        end

        def findElementsByLinkText(parent, link_text)
          find_elements_by 'link text', link_text, parent
        end

        def findElementByPartialLinkText(parent, link_text)
          find_element_by 'partial link text', link_text, parent
        end

        def findElementsByPartialLinkText(parent, link_text)
          find_elements_by 'partial link text', link_text, parent
        end

        def findElementByName(parent, name)
          find_element_by 'name', name, parent
        end

        def findElementsByName(parent, name)
          find_elements_by 'name', name, parent
        end

        def findElementByTagName(parent, tag_name)
          find_element_by 'tag name', tag_name, parent
        end

        def findElementsByTagName(parent, tag_name)
          find_elements_by 'tag name', tag_name, parent
        end

        def findElementByXpath(parent, xpath)
          find_element_by 'xpath', xpath, parent
        end

        def findElementsByXpath(parent, xpath)
          find_elements_by 'xpath', xpath, parent
        end


        #
        # Element functions
        #

        def clickElement(element)
          execute :clickElement, :id => element
        end

        def getElementTagName(element)
          execute :getElementTagName, :id => element
        end

        def getElementAttribute(element, name)
          execute :getElementAttribute, :id => element, :name => name
        end

        def getElementValue(element)
          execute :getElementValue, :id => element
        end

        def getElementText(element)
          execute :getElementText, :id => element
        end

        def getElementLocation(element)
          data = execute :getElementLocation, :id => element

          Point.new data['x'], data['y']
        end

        def getElementSize(element)
          data = execute :getElementSize, :id => element

          Dimension.new data['width'], data['height']
        end

        def sendKeysToElement(element, string)
          execute :sendKeysToElement, {:id => element}, {:value => string.split(//u)}
        end

        def clearElement(element)
          execute :clearElement, :id => element
        end

        def isElementEnabled(element)
          execute :isElementEnabled, :id => element
        end

        def isElementSelected(element)
          execute :isElementSelected, :id => element
        end

        def isElementDisplayed(element)
          execute :isElementDisplayed, :id => element
        end

        def submitElement(element)
          execute :submitElement, :id => element
        end

        def toggleElement(element)
          execute :toggleElement, :id => element
        end

        def setElementSelected(element)
          execute :setElementSelected, :id => element
        end

        def getElementValueOfCssProperty(element, prop)
          execute :getElementValueOfCssProperty, :id => element, :property_name => prop
        end

        def getActiveElement
          Element.new self, element_id_from(execute(:getActiveElement))
        end
        alias_method :switchToActiveElement, :getActiveElement

        def hoverOverElement(element)
          execute :hoverOverElement, :id => element
        end

        def dragElement(element, rigth_by, down_by)
          execute :dragElement, {:id => element}, :x => rigth_by, :y => down_by
        end

        def getCapabilities
          Capabilities.json_create execute(:getCapabilities)
        end

        private

        def find_element_by(how, what, parent = nil)
          if parent
            id = execute :findChildElement, {:id => parent}, {:using => how, :value => what}
          else
            id = execute :findElement, {}, {:using => how, :value => what}
          end

          Element.new self, element_id_from(id)
        end

        def find_elements_by(how, what, parent = nil)
          if parent
            # TODO: why is how sent twice in the payload?
            ids = execute :findChildElements, {:id => parent}, {:using => how, :value => what}
          else
            ids = execute :findElements, {}, {:using => how, :value => what}
          end

          ids.map { |id| Element.new self, element_id_from(id) }
        end


        #
        # executes a command on the remote server via the REST / JSON API.
        #
        #
        # Returns the 'value' of the returned payload
        #

        def execute(*args)
          raw_execute(*args)['value']
        end

        #
        # executes a command on the remote server via the REST / JSON API.
        #
        # Returns a WebDriver::Remote::Response instance
        #

        def raw_execute(command, opts = {}, command_hash = nil)
          verb, path = COMMANDS[command] || raise("unknown command #{command.inspect}")
          path       = path.dup

          path[':session_id'] = @session_id if path.include?(":session_id")

          begin
            opts.each { |key, value| path[key.inspect] = URI.escape(value.to_s)}
          rescue IndexError
            raise ArgumentError, "#{opts.inspect} invalid for #{command.inspect}"
          end

          puts "-> #{verb.to_s.upcase} #{path}" if $DEBUG
          http.call verb, path, command_hash
        end

        def default_options
          {
            :url                  => "http://localhost:4444/wd/hub",
            # TODO(jari): enable Patron when http://github.com/toland/patron/issues/issue/13 is fixed
            :http_client          => DefaultHttpClient,
            :desired_capabilities => Capabilities.firefox
          }
        end

        def http_client_class
          require "selenium/webdriver/remote/patron_http_client"
          PatronHttpClient
        rescue LoadError
          # patron not available
          DefaultHttpClient
        end

      end # Bridge
    end # Remote
  end # WebDriver
end # Selenium