// Copyright 2017 Google Inc. 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.

import 'dart:convert' show base64;

import '../../async_core.dart' as async_core;
import '../common/by.dart';
import '../common/request.dart';
import '../common/request_client.dart';
import '../common/spec.dart';
import '../common/utils.dart';
import '../common/webdriver_handler.dart';
import 'common.dart';
// ignore: uri_does_not_exist
import 'common_stub.dart'
// ignore: uri_does_not_exist
    if (dart.library.io) 'common_io.dart';
import 'cookies.dart';
import 'keyboard.dart';
import 'logs.dart';
import 'mouse.dart';
import 'target_locator.dart';
import 'timeouts.dart';
import 'web_element.dart';
import 'window.dart';

/// Interacts with WebDriver.
class WebDriver implements SearchContext {
  final SyncRequestClient _client;

  final WebDriverHandler _handler;

  final Map<String, dynamic> capabilities;

  final WebDriverSpec spec;

  final String id;

  final Uri uri;

  WebDriver(this.uri, this.id, this.capabilities, this._client, this.spec)
      : _handler = getHandler(spec);

  /// Produces a [async_core.WebDriver] with the same session ID. Allows
  /// backwards compatibility with other frameworks.
  async_core.WebDriver get asyncDriver => createAsyncWebDriver(this);

  @override
  async_core.SearchContext get asyncContext => asyncDriver;

  /// Preferred method for registering listeners. Listeners are expected to
  /// return a Future. Use new Future.value() for synchronous listeners.
  void addEventListener(SyncWebDriverListener listener) {
    _client.addEventListener(listener);
  }

  /// The current url.
  String get currentUrl => _client.send(_handler.core.buildCurrentUrlRequest(),
      _handler.core.parseCurrentUrlResponse);

  /// Navigates to the specified url
  void get(Object /* Uri | String */ url) {
    _client.send(
        _handler.navigation.buildNavigateToRequest(
          (url is Uri) ? url.toString() : url as String,
        ),
        _handler.navigation.parseNavigateToResponse);
  }

  ///  Navigates forwards in the browser history, if possible.
  void forward() {
    _client.send(_handler.navigation.buildForwardRequest(),
        _handler.navigation.parseForwardResponse);
  }

  /// Navigates backwards in the browser history, if possible.
  void back() {
    _client.send(_handler.navigation.buildBackRequest(),
        _handler.navigation.parseBackResponse);
  }

  /// Refreshes the current page.
  void refresh() {
    _client.send(_handler.navigation.buildRefreshRequest(),
        _handler.navigation.parseRefreshResponse);
  }

  /// The title of the current page.
  String get title => _client.send(
      _handler.core.buildTitleRequest(), _handler.core.parseTitleResponse);

  /// Search for multiple elements within the entire current page.
  @override
  List<WebElement> findElements(By by) {
    final ids = _client.send(
        _handler.elementFinder.buildFindElementsRequest(by),
        _handler.elementFinder.parseFindElementsResponse);

    final elements = <WebElement>[];
    var i = 0;
    for (final id in ids) {
      elements.add(WebElement(this, _client, _handler, id, this, by, i++));
    }

    return elements;
  }

  /// Search for an element within the entire current page.
  /// Throws [NoSuchElementException] if a matching element is not found.
  @override
  WebElement findElement(By by) => WebElement(
      this,
      _client,
      _handler,
      _client.send(_handler.elementFinder.buildFindElementRequest(by),
          _handler.elementFinder.parseFindElementResponse),
      this,
      by);

  /// Search for an element by xpath within the entire current page.
  /// Throws [NoSuchElementException] if a matching element is not found.
  WebElement findElementByXpath(String by) => findElement(By.xpath(by));

  /// An artist's rendition of the current page's source.
  String get pageSource => _client.send(_handler.core.buildPageSourceRequest(),
      _handler.core.parsePageSourceResponse);

  /// Quits the browser.
  void quit({bool closeSession = true}) {
    if (closeSession) {
      _client.send(_handler.core.buildDeleteSessionRequest(),
          _handler.core.parseDeleteSessionResponse);
    }
  }

  /// Closes the current window.
  ///
  /// This is rather confusing and will be removed.
  /// Should replace all usages with [window.close()] or [quit()].
  @Deprecated('Use `window.close()` or `quit()` instead.')
  void close() {
    window.close();
  }

  /// Handles for all of the currently displayed tabs/windows.
  List<Window> get windows => _client.send(
      _handler.window.buildGetWindowsRequest(),
      (response) => _handler.window
          .parseGetWindowsResponse(response)
          .map<Window>((w) => Window(_client, _handler, w))
          .toList());

  /// Handle for the active tab/window.
  Window get window => _client.send(
      _handler.window.buildGetActiveWindowRequest(),
      (response) => Window(_client, _handler,
          _handler.window.parseGetActiveWindowResponse(response)));

  /// The currently focused element, or the body element if no element has
  /// focus.
  WebElement? get activeElement {
    final id = _client.send(
        _handler.elementFinder.buildFindActiveElementRequest(),
        _handler.elementFinder.parseFindActiveElementResponse);
    if (id != null) {
      return WebElement(this, _client, _handler, id, this, 'activeElement');
    }
    return null;
  }

  /// Changes focus to specified targets.
  ///
  /// Available targets are window, frame, and the current alert.
  TargetLocator get switchTo => TargetLocator(this, _client, _handler);

  Cookies get cookies => Cookies(_client, _handler);

  /// [logs.get(logType)] will give list of logs captured in browser.
  ///
  /// Note that for W3C/Firefox, this is not supported and will produce empty
  /// list of logs, as the spec for this in W3C is not agreed on and Firefox
  /// refuses to support non-spec features. See
  /// https://github.com/w3c/webdriver/issues/406.
  Logs get logs => Logs(_client, _handler);

  Timeouts get timeouts => Timeouts(_client, _handler);

  Keyboard get keyboard => Keyboard(_client, _handler);

  Mouse get mouse => Mouse(_client, _handler);

  /// Take a screenshot of the current page as PNG and return it as
  /// base64-encoded string.
  String captureScreenshotAsBase64() => _client.send(
      _handler.core.buildScreenshotRequest(),
      _handler.core.parseScreenshotResponse);

  /// Take a screenshot of the specified element as PNG and return it as
  /// base64-encoded string.
  String captureElementScreenshotAsBase64(WebElement element) => _client.send(
      _handler.core.buildElementScreenshotRequest(element.id),
      _handler.core.parseScreenshotResponse);

  /// Take a screenshot of the current page as PNG as list of uint8.
  List<int> captureScreenshotAsList() {
    final base64Encoded = captureScreenshotAsBase64();
    return base64.decode(base64Encoded);
  }

  /// Take a screenshot of the specified element as PNG as list of uint8.
  List<int> captureElementScreenshotAsList(WebElement element) {
    final base64Encoded = captureElementScreenshotAsBase64(element);
    return base64.decode(base64Encoded);
  }

  /// 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
  /// asynchronous and must signal that is done by invoking the provided
  /// callback, which is always provided as the final argument to the function.
  /// The value to this callback will be returned to the client.
  ///
  /// Asynchronous script commands may not span page loads. If an unload event
  /// is fired while waiting for a script result, an error will be thrown.
  ///
  /// The script argument defines the script to execute in the form of a
  /// function body. The function will be invoked with the provided args array
  /// and the values may be accessed via the arguments object in the order
  /// specified. The final argument will always be a callback function that must
  /// be invoked to signal that the script has finished.
  ///
  /// Arguments may be any JSON-able object. WebElements will be converted to
  /// the corresponding DOM element. Likewise, any DOM Elements in the script
  /// result will be converted to WebElements.
  dynamic executeAsync(String script, List args) => _client.send(
      _handler.core.buildExecuteAsyncRequest(script, args),
      (response) => _handler.core.parseExecuteAsyncResponse(
          response,
          (elementId) => WebElement(
              this, _client, _handler, elementId, this, 'javascript')));

  /// 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 is returned.
  ///
  /// The script argument defines the script to execute in the form of a
  /// function body. The value returned by that function will be returned to the
  /// client. The function will be invoked with the provided args array and the
  /// values may be accessed via the arguments object in the order specified.
  ///
  /// Arguments may be any JSON-able object. WebElements will be converted to
  /// the corresponding DOM element. Likewise, any DOM Elements in the script
  /// result will be converted to WebElements.
  dynamic execute(String script, List args) => _client.send(
      _handler.core.buildExecuteRequest(script, args),
      (response) => _handler.core.parseExecuteResponse(
          response,
          (elementId) => WebElement(
              this, _client, _handler, elementId, this, 'javascript')));

  /// Performs post request on command to the WebDriver server.
  ///
  /// For use by supporting WebDriver packages.
  dynamic postRequest(String command, [Object? params]) => _client.send(
      _handler.buildGeneralRequest(HttpMethod.httpPost, command, params),
      (response) => _handler.parseGeneralResponse(
          response, (elementId) => getElement(elementId, this)));

  /// Performs get request on command to the WebDriver server.
  ///
  /// For use by supporting WebDriver packages.
  dynamic getRequest(String command) => _client.send(
      _handler.buildGeneralRequest(HttpMethod.httpGet, command),
      (response) => _handler.parseGeneralResponse(
          response, (elementId) => getElement(elementId, this)));

  /// Performs delete request on command to the WebDriver server.
  ///
  /// For use by supporting WebDriver packages.
  dynamic deleteRequest(String command) => _client.send(
        _handler.buildGeneralRequest(HttpMethod.httpDelete, command),
        (response) => _handler.parseGeneralResponse(
          response,
          (elementId) => getElement(elementId, this),
        ),
      );

  WebElement getElement(
    String elementId, [
    SearchContext? context,
    Object? locator,
    int? index,
  ]) =>
      WebElement(
        this,
        _client,
        _handler,
        elementId,
        context,
        locator,
        index,
      );

  @override
  WebDriver get driver => this;

  @override
  String toString() => '$_handler.webdriver($_client)';
}
