// Copyright 2018 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 'dart:collection' show LinkedHashSet;
import 'dart:ui';

import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';

import 'events.dart';
import 'pointer_router.dart';

/// Signature for listening to [PointerEnterEvent] events.
///
/// Used by [MouseTrackerAnnotation], [Listener] and [RenderPointerListener].
typedef PointerEnterEventListener = void Function(PointerEnterEvent event);

/// Signature for listening to [PointerExitEvent] events.
///
/// Used by [MouseTrackerAnnotation], [Listener] and [RenderPointerListener].
typedef PointerExitEventListener = void Function(PointerExitEvent event);

/// Signature for listening to [PointerHoverEvent] events.
///
/// Used by [MouseTrackerAnnotation], [Listener] and [RenderPointerListener].
typedef PointerHoverEventListener = void Function(PointerHoverEvent event);

/// The annotation object used to annotate layers that are interested in mouse
/// movements.
///
/// This is added to a layer and managed by the [Listener] widget.
class MouseTrackerAnnotation {
  /// Creates an annotation that can be used to find layers interested in mouse
  /// movements.
  const MouseTrackerAnnotation({this.onEnter, this.onHover, this.onExit});

  /// Triggered when a pointer has entered the bounding box of the annotated
  /// layer.
  final PointerEnterEventListener onEnter;

  /// Triggered when a pointer has moved within the bounding box of the
  /// annotated layer.
  final PointerHoverEventListener onHover;

  /// Triggered when a pointer has exited the bounding box of the annotated
  /// layer.
  final PointerExitEventListener onExit;

  @override
  String toString() {
    final List<String> callbacks = <String>[];
    if (onEnter != null)
      callbacks.add('enter');
    if (onHover != null)
      callbacks.add('hover');
    if (onExit != null)
      callbacks.add('exit');
    final String describeCallbacks = callbacks.isEmpty
      ? '<none>'
      : callbacks.join(' ');
    return '${describeIdentity(this)}(callbacks: $describeCallbacks)';
  }
}

/// Signature for searching for [MouseTrackerAnnotation]s at the given offset.
///
/// It is used by the [MouseTracker] to fetch annotations for the mouse
/// position.
typedef MouseDetectorAnnotationFinder = Iterable<MouseTrackerAnnotation> Function(Offset offset);

// Various states of each connected mouse device.
//
// It is used by [MouseTracker] to compute which callbacks should be triggered
// by each event.
class _MouseState {
  _MouseState({
    @required PointerEvent mostRecentEvent,
  }) : assert(mostRecentEvent != null),
       _mostRecentEvent = mostRecentEvent;

  // The list of annotations that contains this device during the last frame.
  //
  // It uses [LinkedHashSet] to keep the insertion order.
  LinkedHashSet<MouseTrackerAnnotation> lastAnnotations = LinkedHashSet<MouseTrackerAnnotation>();

  // The most recent mouse event observed from this device.
  //
  // The [mostRecentEvent] is never null.
  PointerEvent get mostRecentEvent => _mostRecentEvent;
  PointerEvent _mostRecentEvent;
  set mostRecentEvent(PointerEvent value) {
    assert(value != null);
    assert(value.device == _mostRecentEvent.device);
    _mostRecentEvent = value;
  }

  int get device => _mostRecentEvent.device;

  @override
  String toString() {
    final String describeEvent = '${_mostRecentEvent.runtimeType}(device: ${_mostRecentEvent.device})';
    final String describeAnnotations = '[list of ${lastAnnotations.length}]';
    return '${describeIdentity(this)}(event: $describeEvent, annotations: $describeAnnotations)';
  }
}

/// Maintains the relationship between mouse devices and
/// [MouseTrackerAnnotation]s, and notifies interested callbacks of the changes
/// thereof.
///
/// This class is a [ChangeNotifier] that notifies its listeners if the value of
/// [mouseIsConnected] changes.
///
/// An instance of [MouseTracker] is owned by the global singleton of
/// [RendererBinding].
class MouseTracker extends ChangeNotifier {
  /// Creates a mouse tracker to keep track of mouse locations.
  ///
  /// The first parameter is a [PointerRouter], which [MouseTracker] will
  /// subscribe to and receive events from. Usually it is the global singleton
  /// instance [GestureBinding.pointerRouter].
  ///
  /// The second parameter is a function with which the [MouseTracker] can
  /// search for [MouseTrackerAnnotation]s at a given position.
  /// Usually it is [Layer.findAllAnnotations] of the root layer.
  ///
  /// All of the parameters must not be null.
  MouseTracker(this._router, this.annotationFinder)
      : assert(_router != null),
        assert(annotationFinder != null) {
    _router.addGlobalRoute(_handleEvent);
  }

  @override
  void dispose() {
    super.dispose();
    _router.removeGlobalRoute(_handleEvent);
  }

  /// Find annotations at a given offset in global logical coordinate space
  /// in visual order from front to back.
  ///
  /// [MouseTracker] uses this callback to know which annotations are affected
  /// by each device.
  ///
  /// The annotations should be returned in visual order from front to
  /// back, so that the callbacks are called in an correct order.
  final MouseDetectorAnnotationFinder annotationFinder;

  // The pointer router that the mouse tracker listens to, and receives new
  // mouse events from.
  final PointerRouter _router;

  // Tracks the state of connected mouse devices.
  //
  // It is the source of truth for the list of connected mouse devices.
  final Map<int, _MouseState> _mouseStates = <int, _MouseState>{};

  // Returns the mouse state of a device. If it doesn't exist, create one using
  // `mostRecentEvent`.
  //
  // The returned value is never null.
  _MouseState _guaranteeMouseState(int device, PointerEvent mostRecentEvent) {
    final _MouseState currentState = _mouseStates[device];
    if (currentState == null) {
      _addMouseDevice(device, mostRecentEvent);
    }
    final _MouseState result = currentState ?? _mouseStates[device];
    assert(result != null);
    return result;
  }

  // The collection of annotations that are currently being tracked.
  // It is operated on by [attachAnnotation] and [detachAnnotation].
  final Set<MouseTrackerAnnotation> _trackedAnnotations = <MouseTrackerAnnotation>{};
  bool get _hasAttachedAnnotations => _trackedAnnotations.isNotEmpty;

  void _addMouseDevice(int device, PointerEvent event) {
    final bool wasConnected = mouseIsConnected;
    assert(!_mouseStates.containsKey(device));
    _mouseStates[device] = _MouseState(mostRecentEvent: event);
    // Schedule a check to enter annotations that might contain this pointer.
    _checkDeviceUpdates(device: device);
    if (mouseIsConnected != wasConnected) {
      notifyListeners();
    }
  }

  void _removeMouseDevice(int device, PointerEvent event) {
    final bool wasConnected = mouseIsConnected;
    assert(_mouseStates.containsKey(device));
    final _MouseState disconnectedMouseState = _mouseStates.remove(device);
    disconnectedMouseState.mostRecentEvent = event;
    // Schedule a check to exit annotations that used to contain this pointer.
    _checkDeviceUpdates(
      device: device,
      disconnectedMouseState: disconnectedMouseState,
    );
    if (mouseIsConnected != wasConnected) {
      notifyListeners();
    }
  }

  // Handler for events coming from the PointerRouter.
  void _handleEvent(PointerEvent event) {
    if (event.kind != PointerDeviceKind.mouse) {
      return;
    }
    final int device = event.device;
    if (event is PointerAddedEvent) {
      _addMouseDevice(device, event);
    } else if (event is PointerRemovedEvent) {
      _removeMouseDevice(device, event);
    } else if (event is PointerHoverEvent) {
      final _MouseState mouseState = _guaranteeMouseState(device, event);
      final PointerEvent previousEvent = mouseState.mostRecentEvent;
      mouseState.mostRecentEvent = event;
      if (previousEvent is PointerAddedEvent || previousEvent.position != event.position) {
        // Only send notifications if we have our first event, or if the
        // location of the mouse has changed
        _checkDeviceUpdates(device: device);
      }
    }
  }

  bool _scheduledPostFramePositionCheck = false;
  // Schedules a position check at the end of this frame.
  // It is only called during a frame during which annotations have been added.
  void _scheduleMousePositionCheck() {
    // If we're not tracking anything, then there is no point in registering a
    // frame callback or scheduling a frame. By definition there are no active
    // annotations that need exiting, either.
    if (!_scheduledPostFramePositionCheck) {
      _scheduledPostFramePositionCheck = true;
      SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
        _checkAllDevicesUpdates();
        _scheduledPostFramePositionCheck = false;
      });
    }
  }

  // Collect the latest states of the given mouse device `device`, and call
  // interested callbacks.
  //
  // The enter or exit events are called for annotations that the pointer
  // enters or leaves, while hover events are always called for each
  // annotations that the pointer stays in, even if the pointer has not moved
  // since the last call. Therefore it's caller's responsibility to check if
  // the pointer has moved.
  //
  // If `disconnectedMouseState` is provided, this state will be used instead,
  // but this mouse will be hovering no annotations.
  void _checkDeviceUpdates({
    int device,
    _MouseState disconnectedMouseState,
  }) {
    final _MouseState mouseState = disconnectedMouseState ?? _mouseStates[device];
    final bool thisDeviceIsConnected = mouseState != disconnectedMouseState;
    assert(mouseState != null);

    final LinkedHashSet<MouseTrackerAnnotation> nextAnnotations =
      (_hasAttachedAnnotations && thisDeviceIsConnected)
        ? LinkedHashSet<MouseTrackerAnnotation>.from(
            annotationFinder(mouseState.mostRecentEvent.position)
          )
        : <MouseTrackerAnnotation>{} as LinkedHashSet<MouseTrackerAnnotation>;

    _dispatchDeviceCallbacks(
      currentState: mouseState,
      nextAnnotations: nextAnnotations,
    );

    mouseState.lastAnnotations = nextAnnotations;
  }

  // Collect the latest states of all mouse devices, and call interested
  // callbacks.
  //
  // For detailed behaviors, see [_checkDeviceUpdates].
  void _checkAllDevicesUpdates() {
    for (final int device in _mouseStates.keys) {
      _checkDeviceUpdates(device: device);
    }
  }

  // Dispatch callbacks related to a device after all necessary information
  // has been collected.
  //
  // This function should not change the provided states, and should not access
  // information that is not provided in parameters (hence being static).
  static void _dispatchDeviceCallbacks({
    @required LinkedHashSet<MouseTrackerAnnotation> nextAnnotations,
    @required _MouseState currentState,
  }) {
    // Order is important for mouse event callbacks. The `findAnnotations`
    // returns annotations in the visual order from front to back. We call
    // it the "visual order", and the opposite one "reverse visual order".
    // The algorithm here is explained in
    // https://github.com/flutter/flutter/issues/41420

    // The `nextAnnotations` is annotations that contains this device in the
    // coming frame in visual order.
    // Order is preserved with the help of [LinkedHashSet].

    final PointerEvent mostRecentEvent = currentState.mostRecentEvent;
    // The `lastAnnotations` is annotations that contains this device in the
    // previous frame in visual order.
    final LinkedHashSet<MouseTrackerAnnotation> lastAnnotations = currentState.lastAnnotations;

    // Send exit events in visual order.
    final Iterable<MouseTrackerAnnotation> exitingAnnotations =
      lastAnnotations.difference(nextAnnotations);
    for (final MouseTrackerAnnotation annotation in exitingAnnotations) {
      if (annotation.onExit != null) {
        annotation.onExit(PointerExitEvent.fromMouseEvent(mostRecentEvent));
      }
    }

    // Send enter events in reverse visual order.
    final Iterable<MouseTrackerAnnotation> enteringAnnotations =
      nextAnnotations.difference(lastAnnotations).toList().reversed;
    for (final MouseTrackerAnnotation annotation in enteringAnnotations) {
      if (annotation.onEnter != null) {
        annotation.onEnter(PointerEnterEvent.fromMouseEvent(mostRecentEvent));
      }
    }

    // Send hover events in reverse visual order.
    // For now the order between the hover events is designed this way for no
    // solid reasons but to keep it aligned with enter events for simplicity.
    if (mostRecentEvent is PointerHoverEvent) {
      final Iterable<MouseTrackerAnnotation> hoveringAnnotations =
        nextAnnotations.toList().reversed;
      for (final MouseTrackerAnnotation annotation in hoveringAnnotations) {
        if (annotation.onHover != null) {
          annotation.onHover(mostRecentEvent);
        }
      }
    }
  }

  /// Checks if the given [MouseTrackerAnnotation] is attached to this
  /// [MouseTracker].
  ///
  /// This function is only public to allow for proper testing of the
  /// MouseTracker. Do not call in other contexts.
  @visibleForTesting
  bool isAnnotationAttached(MouseTrackerAnnotation annotation) {
    return _trackedAnnotations.contains(annotation);
  }

  /// Whether or not a mouse is connected and has produced events.
  bool get mouseIsConnected => _mouseStates.isNotEmpty;

  /// Notify [MouseTracker] that a new mouse tracker annotation has started to
  /// take effect.
  ///
  /// This should be called as soon as the layer that owns this annotation is
  /// added to the layer tree.
  ///
  /// This triggers [MouseTracker] to schedule a mouse position check during the
  /// post frame to see if this new annotation might trigger enter events.
  ///
  /// The [MouseTracker] also uses this to track the number of attached
  /// annotations, and will skip mouse position checks if there is no
  /// annotations attached.
  void attachAnnotation(MouseTrackerAnnotation annotation) {
    // Schedule a check so that we test this new annotation to see if any mouse
    // is currently inside its region. It has to happen after the frame is
    // complete so that the annotation layer has been added before the check.
    _trackedAnnotations.add(annotation);
    if (mouseIsConnected) {
      _scheduleMousePositionCheck();
    }
  }


  /// Notify [MouseTracker] that a mouse tracker annotation that was previously
  /// attached has stopped taking effect.
  ///
  /// This should be called as soon as the layer that owns this annotation is
  /// removed from the layer tree. An assertion error will be thrown if the
  /// associated layer is not removed and receives another mouse hit.
  ///
  /// This triggers [MouseTracker] to perform a mouse position check immediately
  /// to see if this annotation removal triggers any exit events.
  ///
  /// The [MouseTracker] also uses this to track the number of attached
  /// annotations, and will skip mouse position checks if there is no
  /// annotations attached.
  void detachAnnotation(MouseTrackerAnnotation annotation) {
    _mouseStates.forEach((int device, _MouseState mouseState) {
      if (mouseState.lastAnnotations.contains(annotation)) {
        if (annotation.onExit != null) {
          final PointerEvent event = mouseState.mostRecentEvent;
          assert(event != null);
          annotation.onExit(PointerExitEvent.fromMouseEvent(event));
        }
        mouseState.lastAnnotations.remove(annotation);
      }
    });
    _trackedAnnotations.remove(annotation);
  }
}
