// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/widgets.dart';

import 'all_elements.dart';

/// Signature for [CommonFinders.byPredicate].
typedef bool WidgetPredicate(Widget widget);

/// Signature for [CommonFinders.byElement].
typedef bool ElementPredicate(Element element);

/// Some frequently used widget [Finder]s.
final CommonFinders find = const CommonFinders._();

/// Provides lightweight syntax for getting frequently used widget [Finder]s.
///
/// This class is instantiated once, as [find].
class CommonFinders {
  const CommonFinders._();

  /// Finds [Text] widgets containing string equal to the `text`
  /// argument.
  ///
  /// Example:
  ///
  ///     expect(tester, hasWidget(find.text('Back')));
  Finder text(String text) => new _TextFinder(text);

  /// Looks for widgets that contain a [Text] descendant with `text`
  /// in it.
  ///
  /// Example:
  ///
  ///     // Suppose you have a button with text 'Update' in it:
  ///     new Button(
  ///       child: new Text('Update')
  ///     )
  ///
  ///     // You can find and tap on it like this:
  ///     tester.tap(find.widgetWithText(Button, 'Update'));
  Finder widgetWithText(Type widgetType, String text) {
    return new _WidgetWithTextFinder(widgetType, text);
  }

  /// Finds widgets by searching for one with a particular [Key].
  ///
  /// Example:
  ///
  ///     expect(tester, hasWidget(find.byKey(backKey)));
  Finder byKey(Key key) => new _KeyFinder(key);

  /// Finds widgets by searching for widgets with a particular type.
  ///
  /// This does not do subclass tests, so for example
  /// `byType(StatefulWidget)` will never find anything since that's
  /// an abstract class.
  ///
  /// The `type` argument must be a subclass of [Widget].
  ///
  /// Example:
  ///
  ///     expect(tester, hasWidget(find.byType(IconButton)));
  Finder byType(Type type) => new _WidgetTypeFinder(type);

  /// Finds widgets by searching for elements with a particular type.
  ///
  /// This does not do subclass tests, so for example
  /// `byElementType(VirtualViewportElement)` will never find anything
  /// since that's an abstract class.
  ///
  /// The `type` argument must be a subclass of [Element].
  ///
  /// Example:
  ///
  ///     expect(tester, hasWidget(find.byElementType(SingleChildRenderObjectElement)));
  Finder byElementType(Type type) => new _ElementTypeFinder(type);

  /// Finds widgets whose current widget is the instance given by the
  /// argument.
  ///
  /// Example:
  ///
  ///     // Suppose you have a button created like this:
  ///     Widget myButton = new Button(
  ///       child: new Text('Update')
  ///     );
  ///
  ///     // You can find and tap on it like this:
  ///     tester.tap(find.byConfig(myButton));
  Finder byConfig(Widget config) => new _ConfigFinder(config);

  /// Finds widgets using a widget predicate.
  ///
  /// Example:
  ///
  ///     expect(tester, hasWidget(find.byWidgetPredicate(
  ///       (Widget widget) => widget is Tooltip && widget.message == 'Back'
  ///     )));
  Finder byWidgetPredicate(WidgetPredicate predicate) {
    return new _WidgetPredicateFinder(predicate);
  }

  /// Finds widgets using an element predicate.
  ///
  /// Example:
  ///
  ///     expect(tester, hasWidget(find.byWidgetPredicate(
  ///       // finds elements of type SingleChildRenderObjectElement, including
  ///       // those that are actually subclasses of that type.
  ///       // (contrast with byElementType, which only returns exact matches)
  ///       (Element element) => element is SingleChildRenderObjectElement
  ///     )));
  Finder byElementPredicate(ElementPredicate predicate) {
    return new _ElementPredicateFinder(predicate);
  }
}

/// Searches a widget tree and returns nodes that match a particular
/// pattern.
abstract class Finder {
  /// Describes what the finder is looking for. The description should be
  /// a brief English noun phrase describing the finder's pattern.
  String get description;

  /// Returns all the elements in the given list that match this
  /// finder's pattern.
  ///
  /// When implementing your own Finders that inherit directly from
  /// [Finder], this is the main method to override. If your finder
  /// can efficiently be described just in terms of a predicate
  /// function, consider extending [MatchFinder] instead.
  Iterable<Element> apply(Iterable<Element> candidates);

  // Right now this is hard-coded to just grab the elements from the binding.
  //
  // One could imagine a world where CommonFinders and Finder can be configured
  // to work from a specific subtree, but we'll implement that when it's needed.
  static Iterable<Element> get _allElements => collectAllElementsFrom(WidgetsBinding.instance.renderViewElement);

  Iterable<Element> _cachedResult;

  /// Returns the current result. If [precache] was called and returned true, this will
  /// cheaply return the result that was computed then. Otherwise, it creates a new
  /// iterable to compute the answer.
  ///
  /// Calling this clears the cache from [precache].
  Iterable<Element> evaluate() {
    final Iterable<Element> result = _cachedResult ?? apply(_allElements);
    _cachedResult = null;
    return result;
  }

  /// Attempts to evaluate the finder. Returns whether any elements in the tree
  /// matched the finder. If any did, then the result is cached and can be obtained
  /// from [evaluate].
  ///
  /// If this returns true, you must call [evaluate] before you call [precache] again.
  bool precache() {
    assert(_cachedResult == null);
    final Iterable<Element> result = apply(_allElements);
    if (result.isNotEmpty) {
      _cachedResult = result;
      return true;
    }
    _cachedResult = null;
    return false;
  }

  @override
  String toString() {
    final List<Element> widgets = evaluate().toList();
    final int count = widgets.length;
    if (count == 0)
      return 'zero widgets with $description';
    if (count == 1)
      return 'exactly one widget with $description: ${widgets.single}';
    if (count < 4)
      return '$count widgets with $description: $widgets';
    return '$count widgets with $description: ${widgets[0]}, ${widgets[1]}, ${widgets[2]}, ...';
  }
}

/// Searches a widget tree and returns nodes that match a particular
/// pattern.
abstract class MatchFinder extends Finder {
  /// Returns true if the given element matches the pattern.
  ///
  /// When implementing your own MatchFinder, this is the main method to override.
  bool matches(Element candidate);

  @override
  Iterable<Element> apply(Iterable<Element> candidates) {
    return candidates.where(matches);
  }
}

class _TextFinder extends MatchFinder {
  _TextFinder(this.text);

  final String text;

  @override
  String get description => 'text "$text"';

  @override
  bool matches(Element candidate) {
    if (candidate.widget is! Text)
      return false;
    Text textWidget = candidate.widget;
    return textWidget.data == text;
  }
}

class _WidgetWithTextFinder extends Finder {
  _WidgetWithTextFinder(this.widgetType, this.text);

  final Type widgetType;
  final String text;

  @override
  String get description => 'type $widgetType with text "$text"';

  @override
  Iterable<Element> apply(Iterable<Element> candidates) {
    return candidates
      .map((Element textElement) {
        if (textElement.widget is! Text)
          return null;

        Text textWidget = textElement.widget;
        if (textWidget.data == text) {
          try {
            textElement.visitAncestorElements((Element element) {
              if (element.widget.runtimeType == widgetType)
                throw element;
              return true;
            });
          } on Element catch (result) {
            return result;
          }
        }
        return null;
      })
      .where((Element element) => element != null);
  }
}

class _KeyFinder extends MatchFinder {
  _KeyFinder(this.key);

  final Key key;

  @override
  String get description => 'key $key';

  @override
  bool matches(Element candidate) {
    return candidate.widget.key == key;
  }
}

class _WidgetTypeFinder extends MatchFinder {
  _WidgetTypeFinder(this.widgetType);

  final Type widgetType;

  @override
  String get description => 'type "$widgetType"';

  @override
  bool matches(Element candidate) {
    return candidate.widget.runtimeType == widgetType;
  }
}

class _ElementTypeFinder extends MatchFinder {
  _ElementTypeFinder(this.elementType);

  final Type elementType;

  @override
  String get description => 'type "$elementType"';

  @override
  bool matches(Element candidate) {
    return candidate.runtimeType == elementType;
  }
}

class _ConfigFinder extends MatchFinder {
  _ConfigFinder(this.config);

  final Widget config;

  @override
  String get description => 'the given configuration ($config)';

  @override
  bool matches(Element candidate) {
    return candidate.widget == config;
  }
}

class _WidgetPredicateFinder extends MatchFinder {
  _WidgetPredicateFinder(this.predicate);

  final WidgetPredicate predicate;

  @override
  String get description => 'widget matching predicate ($predicate)';

  @override
  bool matches(Element candidate) {
    return predicate(candidate.widget);
  }
}

class _ElementPredicateFinder extends MatchFinder {
  _ElementPredicateFinder(this.predicate);

  final ElementPredicate predicate;

  @override
  String get description => 'element matching predicate ($predicate)';

  @override
  bool matches(Element candidate) {
    return predicate(candidate);
  }
}