// 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:math' show Point, Rectangle;

import '../../async_core.dart' as async_core;
import '../common/by.dart';
import '../common/request_client.dart';
import '../common/web_element.dart' as common;
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 'web_driver.dart';

/// WebDriver representation and interactions with an HTML element.
class WebElement extends common.WebElement implements SearchContext {
  @override
  final String id;

  /// Produces a compatible [async_core.WebElement]. Allows backwards
  /// compatibility with other frameworks.
  async_core.WebElement get asyncElement => createAsyncWebElement(this);

  @override
  async_core.SearchContext get asyncContext => asyncElement;

  /// The context from which this element was found.
  final SearchContext? context;

  @override
  final WebDriver driver;

  final SyncRequestClient _client;

  final WebDriverHandler _handler;

  /// How the element was located from the context.
  final dynamic /* String | Finder */ locator;

  /// The index of this element in the set of element founds. If the method
  /// used to find this element always returns one element, then this is null.
  final int? index;

  WebElement(
    this.driver,
    this._client,
    this._handler,
    this.id, [
    this.context,
    this.locator,
    this.index,
  ]);

  WebElement? get parent {
    final parentId = _parentId;
    if (parentId == null) {
      return null;
    }
    return WebElement(
      driver,
      _client,
      _handler,
      parentId,
    );
  }

  String? get _parentId => _client.send(
        _handler.element.buildPropertyRequest(id, 'parentElement'),
        _handler.elementFinder.parseFindElementResponseCore,
      );

  static final _parentCache = <String, String?>{};

  /// Gets a chain of parent elements, including the element itself.
  List<String> get parents {
    WebElement? p = this;
    final result = <String>[];
    while (p != null) {
      final id = p.id;
      if (_parentCache.containsKey(id)) {
        break;
      }
      result.add(id);
      _parentCache[id] = (p = p.parent)?.id;
    }

    if (p != null) {
      // Hit cache in the previous loop.
      String? id = p.id;
      while (id != null) {
        result.add(id);
        id = _parentCache[id];
      }
    }

    return result;
  }

  /// Click on this element.
  void click() {
    _client.send(_handler.element.buildClickRequest(id),
        _handler.element.parseClickResponse);
  }

  /// Send [keysToSend] to this element.
  void sendKeys(String keysToSend) {
    _client.send(_handler.element.buildSendKeysRequest(id, keysToSend),
        _handler.element.parseSendKeysResponse);
  }

  /// Clear the content of a text element.
  void clear() {
    _client.send(_handler.element.buildClearRequest(id),
        _handler.element.parseClearResponse);
  }

  /// Is this radio button/checkbox selected?
  bool get selected => _client.send(_handler.element.buildSelectedRequest(id),
      _handler.element.parseSelectedResponse);

  /// Is this form element enabled?
  bool get enabled => _client.send(_handler.element.buildEnabledRequest(id),
      _handler.element.parseEnabledResponse);

  /// Is this element visible in the page?
  bool get displayed => _client.send(_handler.element.buildDisplayedRequest(id),
      _handler.element.parseDisplayedResponse);

  /// The location of the element.
  ///
  /// This is assumed to be the upper left corner of the element, but its
  /// implementation is not well defined in the JSON spec.
  Point<int> get location => _client.send(
      _handler.element.buildLocationRequest(id),
      _handler.element.parseLocationResponse);

  /// The size of this element.
  Rectangle<int> get size => _client.send(_handler.element.buildSizeRequest(id),
      _handler.element.parseSizeResponse);

  /// The bounds of this element.
  Rectangle<int> get rect {
    final location = this.location;
    final size = this.size;
    return Rectangle<int>(location.x, location.y, size.width, size.height);
  }

  /// The tag name for this element.
  String get name => _client.send(_handler.element.buildNameRequest(id),
      _handler.element.parseNameResponse);

  ///  Visible text within this element.
  String get text => _client.send(_handler.element.buildTextRequest(id),
      _handler.element.parseTextResponse);

  ///Find an element nested within this element.
  ///
  /// Throws [NoSuchElementException] if matching element is not found.
  @override
  WebElement findElement(By by) => WebElement(
      driver,
      _client,
      _handler,
      _client.send(_handler.elementFinder.buildFindElementRequest(by, id),
          _handler.elementFinder.parseFindElementResponse),
      this,
      by);

  /// Find multiple elements nested within this element.
  @override
  List<WebElement> findElements(By by) {
    final ids = _client.send(
        _handler.elementFinder.buildFindElementsRequest(by, id),
        _handler.elementFinder.parseFindElementsResponse);

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

    return elements;
  }

  /// Access to the HTML attributes of this tag.
  Attributes get attributes => Attributes((name) => _client.send(
      _handler.element.buildAttributeRequest(id, name),
      _handler.element.parseAttributeResponse));

  /// Access to the HTML properties of this tag.
  Attributes get properties => Attributes((name) => _client.send(
      _handler.element.buildPropertyRequest(id, name),
      _handler.element.parsePropertyResponse));

  /// Access to the cssProperties of this element.
  Attributes get cssProperties => Attributes((name) => _client.send(
      _handler.element.buildCssPropertyRequest(id, name),
      _handler.element.parseCssPropertyResponse));

  /// Are these two elements the same underlying element in the DOM.
  bool equals(WebElement other) => other.driver == driver && other.id == id;

  @override
  int get hashCode => driver.hashCode * 3 + id.hashCode;

  @override
  bool operator ==(Object other) =>
      other is WebElement && other.driver == driver && other.id == id;

  @override
  String toString() {
    final out = StringBuffer()..write(context);
    if (locator is By) {
      if (index == null) {
        out.write('.findElement(');
      } else {
        out.write('.findElements(');
      }
      out
        ..write(locator)
        ..write(')');
    } else {
      out
        ..write('.')
        ..write(locator);
    }
    if (index != null) {
      out
        ..write('[')
        ..write(index)
        ..write(']');
    }
    return out.toString();
  }

  String toStringDeep() => "<$name>\n\nHTML:\n${properties['outerHTML']}";
}
