// Copyright 2015 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/gestures.dart';

export 'dart:ui' show Point;

/// A class for generating coherent artificial pointer events.
///
/// You can use this to manually simulate individual events, but the
/// simplest way to generate coherent gestures is to use [TestGesture].
class TestPointer {
  /// Creates a [TestPointer]. By default, the pointer identifier used is 1, however
  /// this can be overridden by providing an argument to the constructor.
  TestPointer([ this.pointer = 1 ]);

  /// The pointer identifier used for events generated by this object.
  ///
  /// Set when the object is constructed. Defaults to 1.
  final int pointer;

  /// Whether the pointer simulated by this object is currently down.
  ///
  /// A pointer is released (goes up) by calling [up] or [cancel].
  ///
  /// Once a pointer is released, it can no longer generate events.
  bool get isDown => _isDown;
  bool _isDown = false;

  /// The position of the last event sent by this object.
  ///
  /// If no event has ever been sent by this object, returns null.
  Point get location => _location;
  Point _location;

  /// Create a [PointerDownEvent] at the given location.
  ///
  /// By default, the time stamp on the event is [Duration.ZERO]. You
  /// can give a specific time stamp by passing the `timeStamp`
  /// argument.
  PointerDownEvent down(Point newLocation, { Duration timeStamp: Duration.ZERO }) {
    assert(!isDown);
    _isDown = true;
    _location = newLocation;
    return new PointerDownEvent(
      timeStamp: timeStamp,
      pointer: pointer,
      position: location
    );
  }

  /// Create a [PointerMoveEvent] to the given location.
  ///
  /// By default, the time stamp on the event is [Duration.ZERO]. You
  /// can give a specific time stamp by passing the `timeStamp`
  /// argument.
  PointerMoveEvent move(Point newLocation, { Duration timeStamp: Duration.ZERO }) {
    assert(isDown);
    Offset delta = newLocation - location;
    _location = newLocation;
    return new PointerMoveEvent(
      timeStamp: timeStamp,
      pointer: pointer,
      position: newLocation,
      delta: delta
    );
  }

  /// Create a [PointerUpEvent].
  ///
  /// By default, the time stamp on the event is [Duration.ZERO]. You
  /// can give a specific time stamp by passing the `timeStamp`
  /// argument.
  ///
  /// The object is no longer usable after this method has been called.
  PointerUpEvent up({ Duration timeStamp: Duration.ZERO }) {
    assert(isDown);
    _isDown = false;
    return new PointerUpEvent(
      timeStamp: timeStamp,
      pointer: pointer,
      position: location
    );
  }

  /// Create a [PointerCancelEvent].
  ///
  /// By default, the time stamp on the event is [Duration.ZERO]. You
  /// can give a specific time stamp by passing the `timeStamp`
  /// argument.
  ///
  /// The object is no longer usable after this method has been called.
  PointerCancelEvent cancel({ Duration timeStamp: Duration.ZERO }) {
    assert(isDown);
    _isDown = false;
    return new PointerCancelEvent(
      timeStamp: timeStamp,
      pointer: pointer,
      position: location
    );
  }
}

/// A class for performing gestures in tests.
///
/// The simplest way to create a [TestGesture] is to call
/// [WidgetTester.startGesture].
class TestGesture {
  /// Create a [TestGesture] by starting with a pointerDown at the
  /// given point.
  ///
  /// By default, the pointer ID used is 1. This can be overridden by
  /// providing the `pointer` argument.
  ///
  /// By default, the global binding is used both for hit testing and
  /// for dispatching of events. The object to use for hit testing can
  /// be overridden by providing `hitTestTarget`, and the object to
  /// use for dispatching events can be overridden by providing an
  /// `dispatcher`.
  factory TestGesture(Point downLocation, {
    int pointer: 1,
    HitTestable target,
    HitTestDispatcher dispatcher
  }) {
    // hit test
    final HitTestResult result = new HitTestResult();
    target ??= GestureBinding.instance;
    assert(target != null);
    target.hitTest(result, downLocation);

    // dispatch down event
    final TestPointer testPointer = new TestPointer(pointer);
    dispatcher ??= GestureBinding.instance;
    assert(dispatcher != null);
    dispatcher.dispatchEvent(testPointer.down(downLocation), result);

    // create a TestGesture
    return new TestGesture._(dispatcher, result, testPointer);
  }

  const TestGesture._(this._dispatcher, this._result, this._pointer);

  final HitTestDispatcher _dispatcher;
  final HitTestResult _result;
  final TestPointer _pointer;

  /// Send a move event moving the pointer by the given offset.
  void moveBy(Offset offset) {
    assert(_pointer._isDown);
    moveTo(_pointer.location + offset);
  }

  /// Send a move event moving the pointer to the given location.
  void moveTo(Point location) {
    assert(_pointer._isDown);
    _dispatcher.dispatchEvent(_pointer.move(location), _result);
  }

  /// End the gesture by releasing the pointer.
  ///
  /// The object is no longer usable after this method has been called.
  void up() {
    assert(_pointer._isDown);
    _dispatcher.dispatchEvent(_pointer.up(), _result);
    assert(!_pointer._isDown);
  }

  /// End the gesture by canceling the pointer (as would happen if the
  /// system showed a modal dialog on top of the Flutter application,
  /// for instance).
  ///
  /// The object is no longer usable after this method has been called.
  void cancel() {
    assert(_pointer._isDown);
    _dispatcher.dispatchEvent(_pointer.cancel(), _result);
    assert(!_pointer._isDown);
  }
}