<?php
// Copyright 2004-present Facebook. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

class RemoteWebDriver implements WebDriver, JavaScriptExecutor {
  /**
   * @var HttpCommandExecutor
   */
  protected $executor;
  /**
   * @var string
   */
  protected $sessionID;
  /**
   * @var RemoteMouse
   */
  protected $mouse;
  /**
   * @var RemoteKeyboard
   */
  protected $keyboard;
  /**
   * @var RemoteTouchScreen
   */
  protected $touch;
  /**
   * @var RemoteExecuteMethod
   */
  protected $executeMethod;

  protected function __construct() {}

  /**
   * Construct the RemoteWebDriver by a desired capabilities.
   *
   * @param string $url The url of the remote server
   * @param DesiredCapabilities $desired_capabilities The desired capabilities
   * @param int $timeout_in_ms
   * @return RemoteWebDriver
   */
  public static function create(
    $url = 'http://localhost:4444/wd/hub',
    $desired_capabilities = null,
    $timeout_in_ms = 300000
  ) {
    $url = preg_replace('#/+$#', '', $url);

    // Passing DesiredCapabilities as $desired_capabilities is encourged but
    // array is also accepted for legacy reason.
    if ($desired_capabilities instanceof DesiredCapabilities) {
      $desired_capabilities = $desired_capabilities->toArray();
    }

    $executor = new HttpCommandExecutor($url);

    $command = new WebDriverCommand(
      null,
      DriverCommand::NEW_SESSION,
      array('desiredCapabilities' => $desired_capabilities)
    );

    $response = $executor->execute(
      $command,
      array(
        CURLOPT_CONNECTTIMEOUT_MS => $timeout_in_ms,
      )
    );

    $driver = new static();
    $driver->setSessionID($response->getSessionID())
           ->setCommandExecutor($executor);
    return $driver;
  }

  /**
   * [Experimental] Construct the RemoteWebDriver by an existing session.
   *
   * This constructor can boost the performance a lot by reusing the same
   * browser for the whole test suite. You do not have to pass the desired
   * capabilities because the session was created before.
   *
   * @param string $url The url of the remote server
   * @param string $session_id The existing session id
   * @return RemoteWebDriver
   */
  public static function createBySessionID(
    $session_id,
    $url = 'http://localhost:4444/wd/hub'
  ) {
    $driver = new static();
    $driver->setSessionID($session_id)
           ->setCommandExecutor(new HttpCommandExecutor($url));
    return $driver;
  }

  /**
   * Close the current window.
   *
   * @return RemoteWebDriver The current instance.
   */
  public function close() {
    $this->execute(DriverCommand::CLOSE, array());

    return $this;
  }

  /**
   * Find the first WebDriverElement using the given mechanism.
   *
   * @param WebDriverBy $by
   * @return RemoteWebElement NoSuchElementException is thrown in
   *    HttpCommandExecutor if no element is found.
   * @see WebDriverBy
   */
  public function findElement(WebDriverBy $by) {
    $params = array('using' => $by->getMechanism(), 'value' => $by->getValue());
    $raw_element = $this->execute(
      DriverCommand::FIND_ELEMENT,
      $params
    );

    return $this->newElement($raw_element['ELEMENT']);
  }

  /**
   * Find all WebDriverElements within the current page using the given
   * mechanism.
   *
   * @param WebDriverBy $by
   * @return RemoteWebElement[] A list of all WebDriverElements, or an empty
   *    array if nothing matches
   * @see WebDriverBy
   */
  public function findElements(WebDriverBy $by) {
    $params = array('using' => $by->getMechanism(), 'value' => $by->getValue());
    $raw_elements = $this->execute(
      DriverCommand::FIND_ELEMENTS,
      $params
    );

    $elements = array();
    foreach ($raw_elements as $raw_element) {
      $elements[] = $this->newElement($raw_element['ELEMENT']);
    }
    return $elements;
  }

    /**
     * Load a new web page in the current browser window.
     *
     * @param string $url
     *
     * @return RemoteWebDriver The current instance.
     */
  public function get($url) {
    $params = array('url' => (string)$url);
    $this->execute(DriverCommand::GET, $params);

    return $this;
  }

  /**
   * Get a string representing the current URL that the browser is looking at.
   *
   * @return string The current URL.
   */
  public function getCurrentURL() {
    return $this->execute(DriverCommand::GET_CURRENT_URL);
  }

  /**
   * Get the source of the last loaded page.
   *
   * @return string The current page source.
   */
  public function getPageSource() {
    return $this->execute(DriverCommand::GET_PAGE_SOURCE);
  }

  /**
   * Get the title of the current page.
   *
   * @return string The title of the current page.
   */
  public function getTitle() {
    return $this->execute(DriverCommand::GET_TITLE);
  }

  /**
   * Return an opaque handle to this window that uniquely identifies it within
   * this driver instance.
   *
   * @return string The current window handle.
   */
  public function getWindowHandle() {
    return $this->execute(
      DriverCommand::GET_CURRENT_WINDOW_HANDLE,
      array()
    );
  }

  /**
   * Get all window handles available to the current session.
   *
   * @return array An array of string containing all available window handles.
   */
  public function getWindowHandles() {
    return $this->execute(DriverCommand::GET_WINDOW_HANDLES, array());
  }

  /**
   * Quits this driver, closing every associated window.
   *
   * @return void
   */
  public function quit() {
    $this->execute(DriverCommand::QUIT);
    $this->executor = null;
  }

  /**
   * Prepare arguments for JavaScript injection
   *
   * @param array $arguments
   * @return array
   */
  private function prepareScriptArguments(array $arguments) {
    $args = array();
    foreach ($arguments as $key => $value) {
      if ($value instanceof WebDriverElement) {
        $args[$key] = array('ELEMENT'=>$value->getID());
      } else {
        if (is_array($value)) {
          $value = $this->prepareScriptArguments($value);
        }
        $args[$key] = $value;
      }
    }
    return $args;
  }

  /**
   * Inject a snippet of JavaScript into the page for execution in the context
   * of the currently selected frame. The executed script is assumed to be
   * synchronous and the result of evaluating the script will be returned.
   *
   * @param string $script The script to inject.
   * @param array $arguments The arguments of the script.
   * @return mixed The return value of the script.
   */
  public function executeScript($script, array $arguments = array()) {
    $params = array(
      'script' => $script,
      'args' => $this->prepareScriptArguments($arguments),
    );
    return $this->execute(DriverCommand::EXECUTE_SCRIPT, $params);
  }

  /**
   * Inject a snippet of JavaScript into the page for asynchronous execution in
   * the context of the currently selected frame.
   *
   * The driver will pass a callback as the last argument to the snippet, and
   * block until the callback is invoked.
   *
   * @see WebDriverExecuteAsyncScriptTestCase
   *
   * @param string $script The script to inject.
   * @param array $arguments The arguments of the script.
   * @return mixed The value passed by the script to the callback.
   */
  public function executeAsyncScript($script, array $arguments = array()) {
    $params = array(
      'script' => $script,
      'args' => $this->prepareScriptArguments($arguments),
    );
    return $this->execute(
      DriverCommand::EXECUTE_ASYNC_SCRIPT,
      $params
    );
  }

  /**
   * Take a screenshot of the current page.
   *
   * @param string $save_as The path of the screenshot to be saved.
   * @return string The screenshot in PNG format.
   */
  public function takeScreenshot($save_as = null) {
    $screenshot = base64_decode(
      $this->execute(DriverCommand::SCREENSHOT)
    );
    if ($save_as) {
      file_put_contents($save_as, $screenshot);
    }
    return $screenshot;
  }

  /**
   * Construct a new WebDriverWait by the current WebDriver instance.
   * Sample usage:
   *
   *   $driver->wait(20, 1000)->until(
   *     WebDriverExpectedCondition::titleIs('WebDriver Page')
   *   );
   *
   * @param int $timeout_in_second
   * @param int $interval_in_millisecond
   *
   * @return WebDriverWait
   */
  public function wait(
      $timeout_in_second = 30,
      $interval_in_millisecond = 250) {
    return new WebDriverWait(
      $this, $timeout_in_second, $interval_in_millisecond
    );
  }

  /**
   * An abstraction for managing stuff you would do in a browser menu. For
   * example, adding and deleting cookies.
   *
   * @return WebDriverOptions
   */
  public function manage() {
    return new WebDriverOptions($this->getExecuteMethod());
  }

  /**
   * An abstraction allowing the driver to access the browser's history and to
   * navigate to a given URL.
   *
   * @return WebDriverNavigation
   * @see WebDriverNavigation
   */
  public function navigate() {
    return new WebDriverNavigation($this->getExecuteMethod());
  }

  /**
   * Switch to a different window or frame.
   *
   * @return WebDriverTargetLocator
   * @see WebDriverTargetLocator
   */
  public function switchTo() {
    return new WebDriverTargetLocator($this->getExecuteMethod(), $this);
  }

  /**
   * @return RemoteMouse
   */
  public function getMouse() {
    if (!$this->mouse) {
      $this->mouse = new RemoteMouse($this->getExecuteMethod());
    }
    return $this->mouse;
  }

  /**
   * @return RemoteKeyboard
   */
  public function getKeyboard() {
    if (!$this->keyboard) {
      $this->keyboard = new RemoteKeyboard($this->getExecuteMethod());
    }
    return $this->keyboard;
  }

  /**
   * @return RemoteTouchScreen
   */
  public function getTouch() {
    if (!$this->touch) {
      $this->touch = new RemoteTouchScreen($this->getExecuteMethod());
    }
    return $this->touch;
  }

  protected function getExecuteMethod() {
    if (!$this->executeMethod) {
      $this->executeMethod = new RemoteExecuteMethod($this);
    }
    return $this->executeMethod;
  }

  /**
   * Construct a new action builder.
   *
   * @return WebDriverActions
   */
  public function action() {
    return new WebDriverActions($this);
  }

  /**
   * Get the element on the page that currently has focus.
   *
   * @return RemoteWebElement
   */
  public function getActiveElement() {
    $response = $this->execute(DriverCommand::GET_ACTIVE_ELEMENT);
    return $this->newElement($response['ELEMENT']);
  }

  /**
   * Return the WebDriverElement with the given id.
   *
   * @param string $id The id of the element to be created.
   * @return RemoteWebElement
   */
  private function newElement($id) {
    return new RemoteWebElement($this->getExecuteMethod(), $id);
  }

  /**
   * Set the command executor of this RemoteWebdrver
   *
   * @param WebDriverCommandExecutor $executor
   * @return RemoteWebDriver
   */
  public function setCommandExecutor(WebDriverCommandExecutor $executor) {
    $this->executor = $executor;
    return $this;
  }

  /**
   * Set the command executor of this RemoteWebdriver
   *
   * @return HttpCommandExecutor
   */
  public function getCommandExecutor() {
    return $this->executor;
  }

  /**
   * Set the session id of the RemoteWebDriver.
   *
   * @param string $session_id
   * @return RemoteWebDriver
   */
  public function setSessionID($session_id) {
    $this->sessionID = $session_id;
    return $this;
  }

  /**
   * Get current selenium sessionID
   *
   * @return string sessionID
   */
  public function getSessionID() {
    return $this->sessionID;
  }

  public function execute($command_name, $params = array()) {
    $command = new WebDriverCommand(
      $this->sessionID,
      $command_name,
      $params
    );

    $response = $this->executor->execute($command);
    return $response->getValue();
  }
}

