// 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';
import 'package:flutter/rendering.dart';

import 'basic.dart';
import 'framework.dart';

export 'package:flutter/gestures.dart' show
  GestureTapDownCallback,
  GestureTapUpCallback,
  GestureTapCallback,
  GestureTapCancelCallback,
  GestureLongPressCallback,
  GestureDragStartCallback,
  GestureDragUpdateCallback,
  GestureDragEndCallback,
  GestureDragStartCallback,
  GestureDragUpdateCallback,
  GestureDragEndCallback,
  GesturePanStartCallback,
  GesturePanUpdateCallback,
  GesturePanEndCallback,
  GestureScaleStartCallback,
  GestureScaleUpdateCallback,
  GestureScaleEndCallback,
  Velocity;

typedef GestureRecognizer GestureRecognizerFactory(GestureRecognizer recognizer);

/// A widget that detects gestures.
///
/// Attempts to recognize gestures that correspond to its non-null callbacks.
///
/// GestureDetector also listens for accessibility events and maps
/// them to the callbacks. To ignore accessibility events, set
/// [excludeFromSemantics] to true.
///
/// See http://flutter.io/gestures/ for additional information.
class GestureDetector extends StatelessComponent {
  GestureDetector({
    Key key,
    this.child,
    this.onTapDown,
    this.onTapUp,
    this.onTap,
    this.onTapCancel,
    this.onDoubleTap,
    this.onLongPress,
    this.onVerticalDragStart,
    this.onVerticalDragUpdate,
    this.onVerticalDragEnd,
    this.onHorizontalDragStart,
    this.onHorizontalDragUpdate,
    this.onHorizontalDragEnd,
    this.onPanStart,
    this.onPanUpdate,
    this.onPanEnd,
    this.onScaleStart,
    this.onScaleUpdate,
    this.onScaleEnd,
    this.behavior,
    this.excludeFromSemantics: false
  }) : super(key: key) {
    assert(excludeFromSemantics != null);
    assert(() {
      bool haveVerticalDrag = onVerticalDragStart != null || onVerticalDragUpdate != null || onVerticalDragEnd != null;
      bool haveHorizontalDrag = onHorizontalDragStart != null || onHorizontalDragUpdate != null || onHorizontalDragEnd != null;
      bool havePan = onPanStart != null || onPanUpdate != null || onPanEnd != null;
      bool haveScale = onScaleStart != null || onScaleUpdate != null || onScaleEnd != null;
      if (havePan || haveScale) {
        if (havePan && haveScale)
          throw new WidgetError('Having both a pan gesture recognizer and a scale gesture recognizer is redundant; scale is a superset of pan. Just use the scale gesture recognizer.');
        String recognizer = havePan ? 'pan' : 'scale';
        if (haveVerticalDrag && haveHorizontalDrag)
          throw new WidgetError('Simultaneously having a vertical drag gesture recognizer, a horizontal drag gesture recognizer, and a $recognizer gesture recognizer will result in the $recognizer gesture recognizer being ignored, since the other two will catch all drags.');
      }
      return true;
    });
  }

  final Widget child;

  /// A pointer that might cause a tap has contacted the screen at a particular
  /// location.
  final GestureTapDownCallback onTapDown;

  /// A pointer that will trigger a tap has stopped contacting the screen at a
  /// particular location.
  final GestureTapUpCallback onTapUp;

  /// A tap has occurred.
  final GestureTapCallback onTap;

  /// The pointer that previously triggered the [onTapDown] will not end up
  /// causing a tap.
  final GestureTapCancelCallback onTapCancel;

  /// The user has tapped the screen at the same location twice in quick
  /// succession.
  final GestureTapCallback onDoubleTap;

  /// A pointer has remained in contact with the screen at the same location for
  /// a long period of time.
  final GestureLongPressCallback onLongPress;

  /// A pointer has contacted the screen and might begin to move vertically.
  final GestureDragStartCallback onVerticalDragStart;

  /// A pointer that is in contact with the screen and moving vertically has
  /// moved in the vertical direction.
  final GestureDragUpdateCallback onVerticalDragUpdate;

  /// A pointer that was previously in contact with the screen and moving
  /// vertically is no longer in contact with the screen and was moving at a
  /// specific velocity when it stopped contacting the screen.
  final GestureDragEndCallback onVerticalDragEnd;

  /// A pointer has contacted the screen and might begin to move horizontally.
  final GestureDragStartCallback onHorizontalDragStart;

  /// A pointer that is in contact with the screen and moving horizontally has
  /// moved in the horizontal direction.
  final GestureDragUpdateCallback onHorizontalDragUpdate;

  /// A pointer that was previously in contact with the screen and moving
  /// horizontally is no longer in contact with the screen and was moving at a
  /// specific velocity when it stopped contacting the screen.
  final GestureDragEndCallback onHorizontalDragEnd;

  final GesturePanStartCallback onPanStart;
  final GesturePanUpdateCallback onPanUpdate;
  final GesturePanEndCallback onPanEnd;

  final GestureScaleStartCallback onScaleStart;
  final GestureScaleUpdateCallback onScaleUpdate;
  final GestureScaleEndCallback onScaleEnd;

  /// How this gesture detector should behave during hit testing.
  final HitTestBehavior behavior;

  /// Whether to exclude these gestures from the semantics tree. For
  /// example, the long-press gesture for showing a tooltip is
  /// excluded because the tooltip itself is included in the semantics
  /// tree directly and so having a gesture to show it would result in
  /// duplication of information.
  final bool excludeFromSemantics;

  Widget build(BuildContext context) {
    Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};

    if (onTapDown != null || onTapUp != null || onTap != null || onTapCancel != null) {
      gestures[TapGestureRecognizer] = (TapGestureRecognizer recognizer) {
        return (recognizer ??= new TapGestureRecognizer())
          ..onTapDown = onTapDown
          ..onTapUp = onTapUp
          ..onTap = onTap
          ..onTapCancel = onTapCancel;
      };
    }

    if (onDoubleTap != null) {
      gestures[DoubleTapGestureRecognizer] = (DoubleTapGestureRecognizer recognizer) {
        return (recognizer ??= new DoubleTapGestureRecognizer())
          ..onDoubleTap = onDoubleTap;
      };
    }

    if (onLongPress != null) {
      gestures[LongPressGestureRecognizer] = (LongPressGestureRecognizer recognizer) {
        return (recognizer ??= new LongPressGestureRecognizer())
          ..onLongPress = onLongPress;
      };
    }

    if (onVerticalDragStart != null || onVerticalDragUpdate != null || onVerticalDragEnd != null) {
      gestures[VerticalDragGestureRecognizer] = (VerticalDragGestureRecognizer recognizer) {
        return (recognizer ??= new VerticalDragGestureRecognizer())
          ..onStart = onVerticalDragStart
          ..onUpdate = onVerticalDragUpdate
          ..onEnd = onVerticalDragEnd;
      };
    }

    if (onHorizontalDragStart != null || onHorizontalDragUpdate != null || onHorizontalDragEnd != null) {
      gestures[HorizontalDragGestureRecognizer] = (HorizontalDragGestureRecognizer recognizer) {
        return (recognizer ??= new HorizontalDragGestureRecognizer())
          ..onStart = onHorizontalDragStart
          ..onUpdate = onHorizontalDragUpdate
          ..onEnd = onHorizontalDragEnd;
      };
    }

    if (onPanStart != null || onPanUpdate != null || onPanEnd != null) {
      gestures[PanGestureRecognizer] = (PanGestureRecognizer recognizer) {
        return (recognizer ??= new PanGestureRecognizer())
          ..onStart = onPanStart
          ..onUpdate = onPanUpdate
          ..onEnd = onPanEnd;
      };
    }

    if (onScaleStart != null || onScaleUpdate != null || onScaleEnd != null) {
      gestures[ScaleGestureRecognizer] = (ScaleGestureRecognizer recognizer) {
        return (recognizer ??= new ScaleGestureRecognizer())
          ..onStart = onScaleStart
          ..onUpdate = onScaleUpdate
          ..onEnd = onScaleEnd;
      };
    }

    return new RawGestureDetector(
      gestures: gestures,
      behavior: behavior,
      excludeFromSemantics: excludeFromSemantics,
      child: child
    );
  }
}

/// A widget that detects gestures described by the given gesture
/// factories.
///
/// For common gestures, use a [GestureRecognizer].
/// RawGestureDetector is useful primarily when developing your
/// own gesture recognizers.
class RawGestureDetector extends StatefulComponent {
  RawGestureDetector({
    Key key,
    this.child,
    this.gestures: const <Type, GestureRecognizerFactory>{},
    this.behavior,
    this.excludeFromSemantics: false
  }) : super(key: key) {
    assert(gestures != null);
    assert(excludeFromSemantics != null);
  }

  final Widget child;

  final Map<Type, GestureRecognizerFactory> gestures;

  /// How this gesture detector should behave during hit testing.
  final HitTestBehavior behavior;

  /// Whether to exclude these gestures from the semantics tree. For
  /// example, the long-press gesture for showing a tooltip is
  /// excluded because the tooltip itself is included in the semantics
  /// tree directly and so having a gesture to show it would result in
  /// duplication of information.
  final bool excludeFromSemantics;

  RawGestureDetectorState createState() => new RawGestureDetectorState();
}

class RawGestureDetectorState extends State<RawGestureDetector> {

  Map<Type, GestureRecognizer> _recognizers = const <Type, GestureRecognizer>{};

  void initState() {
    super.initState();
    _syncAll(config.gestures);
  }

  void didUpdateConfig(RawGestureDetector oldConfig) {
    _syncAll(config.gestures);
  }

  /// This method can be called after the build phase, during the
  /// layout of the nearest descendant RenderObjectWidget of the
  /// gesture detector, to update the list of active gesture
  /// recognizers.
  ///
  /// The typical use case is [Scrollable]s, which put their viewport
  /// in their gesture detector, and then need to know the dimensions
  /// of the viewport and the viewport's child to determine whether
  /// the gesture detector should be enabled.
  void replaceGestureRecognizers(Map<Type, GestureRecognizerFactory> gestures) {
    assert(() {
      RenderObject renderObject = context.findRenderObject();
      if (!config.excludeFromSemantics) {
        assert(renderObject is RenderSemanticsGestureHandler);
        RenderSemanticsGestureHandler semanticsGestureHandler = renderObject;
        renderObject = semanticsGestureHandler.child;
      }
      assert(renderObject is RenderPointerListener);
      RenderPointerListener pointerListener = renderObject;
      renderObject = pointerListener.child;
      if (!renderObject.debugDoingThisLayout) {
        throw new WidgetError(
          'replaceGestureRecognizers() can only be called during the layout phase of the GestureDetector\'s nearest descendant RenderObjectWidget.\n'
          'In this particular case, that is:\n'
          '  $renderObject'
        );
      }
      return true;
    });
    _syncAll(gestures);
    if (!config.excludeFromSemantics) {
      RenderSemanticsGestureHandler semanticsGestureHandler = context.findRenderObject();
      context.visitChildElements((RenderObjectElement element) {
        element.widget.updateRenderObject(semanticsGestureHandler, null);
      });
    }
  }

  void dispose() {
    for (GestureRecognizer recognizer in _recognizers.values)
      recognizer.dispose();
    _recognizers = null;
    super.dispose();
  }

  void _syncAll(Map<Type, GestureRecognizerFactory> gestures) {
    assert(_recognizers != null);
    Map<Type, GestureRecognizer> oldRecognizers = _recognizers;
    _recognizers = <Type, GestureRecognizer>{};
    for (Type type in gestures.keys) {
      assert(!_recognizers.containsKey(type));
      _recognizers[type] = gestures[type](oldRecognizers[type]);
      assert(_recognizers[type].runtimeType == type);
    }
    for (Type type in oldRecognizers.keys) {
      if (!_recognizers.containsKey(type))
        oldRecognizers[type].dispose();
    }
  }

  void _handlePointerDown(PointerDownEvent event) {
    assert(_recognizers != null);
    for (GestureRecognizer recognizer in _recognizers.values)
      recognizer.addPointer(event);
  }

  HitTestBehavior get _defaultBehavior {
    return config.child == null ? HitTestBehavior.translucent : HitTestBehavior.deferToChild;
  }

  Widget build(BuildContext context) {
    Widget result = new Listener(
      onPointerDown: _handlePointerDown,
      behavior: config.behavior ?? _defaultBehavior,
      child: config.child
    );
    if (!config.excludeFromSemantics)
      result = new _GestureSemantics(owner: this, child: result);
    return result;
  }

  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    if (_recognizers == null) {
      description.add('DISPOSED');
    } else {
      List<String> gestures = _recognizers.values.map/*<String>*/((GestureRecognizer recognizer) => recognizer.toStringShort());
      if (gestures.isEmpty)
        gestures.add('<none>');
      description.add('gestures: ${gestures.join(", ")}');
    }
    switch (config.behavior) {
      case HitTestBehavior.translucent:
        description.add('behavior: translucent');
        break;
      case HitTestBehavior.opaque:
        description.add('behavior: opaque');
        break;
      case HitTestBehavior.deferToChild:
        description.add('behavior: defer-to-child');
        break;
    }
  }
}

class _GestureSemantics extends OneChildRenderObjectWidget {
  _GestureSemantics({
    Key key,
    Widget child,
    this.owner
  }) : super(key: key, child: child);

  final RawGestureDetectorState owner;

  void _handleTap() {
    TapGestureRecognizer recognizer = owner._recognizers[TapGestureRecognizer];
    assert(recognizer != null);
    if (recognizer.onTapDown != null)
      recognizer.onTapDown(Point.origin);
    if (recognizer.onTapUp != null)
      recognizer.onTapUp(Point.origin);
    if (recognizer.onTap != null)
      recognizer.onTap();
  }

  void _handleLongPress() {
    LongPressGestureRecognizer recognizer = owner._recognizers[LongPressGestureRecognizer];
    assert(recognizer != null);
    if (recognizer.onLongPress != null)
      recognizer.onLongPress();
  }

  void _handleHorizontalDragUpdate(double delta) {
    {
      HorizontalDragGestureRecognizer recognizer = owner._recognizers[HorizontalDragGestureRecognizer];
      if (recognizer != null) {
        if (recognizer.onStart != null)
          recognizer.onStart(Point.origin);
        if (recognizer.onUpdate != null)
          recognizer.onUpdate(delta);
        if (recognizer.onEnd != null)
          recognizer.onEnd(Velocity.zero);
        return;
      }
    }
    {
      PanGestureRecognizer recognizer = owner._recognizers[PanGestureRecognizer];
      if (recognizer != null) {
        if (recognizer.onStart != null)
          recognizer.onStart(Point.origin);
        if (recognizer.onUpdate != null)
          recognizer.onUpdate(new Offset(delta, 0.0));
        if (recognizer.onEnd != null)
          recognizer.onEnd(Velocity.zero);
        return;
      }
    }
    assert(false);
  }

  void _handleVerticalDragUpdate(double delta) {
    {
      VerticalDragGestureRecognizer recognizer = owner._recognizers[VerticalDragGestureRecognizer];
      if (recognizer != null) {
        if (recognizer.onStart != null)
          recognizer.onStart(Point.origin);
        if (recognizer.onUpdate != null)
          recognizer.onUpdate(delta);
        if (recognizer.onEnd != null)
          recognizer.onEnd(Velocity.zero);
        return;
      }
    }
    {
      PanGestureRecognizer recognizer = owner._recognizers[PanGestureRecognizer];
      if (recognizer != null) {
        if (recognizer.onStart != null)
          recognizer.onStart(Point.origin);
        if (recognizer.onUpdate != null)
          recognizer.onUpdate(new Offset(0.0, delta));
        if (recognizer.onEnd != null)
          recognizer.onEnd(Velocity.zero);
        return;
      }
    }
    assert(false);
  }

  RenderSemanticsGestureHandler createRenderObject() {
    RenderSemanticsGestureHandler result = new RenderSemanticsGestureHandler();
    updateRenderObject(result, null);
    return result;
  }

  void updateRenderObject(RenderSemanticsGestureHandler renderObject, _GestureSemantics oldWidget) {
    Map<Type, GestureRecognizer> recognizers = owner._recognizers;
    renderObject.onTap = recognizers.containsKey(TapGestureRecognizer) ? _handleTap : null;
    renderObject.onLongPress = recognizers.containsKey(LongPressGestureRecognizer) ? _handleLongPress : null;
    renderObject.onHorizontalDragUpdate = recognizers.containsKey(VerticalDragGestureRecognizer) ||
                                          recognizers.containsKey(PanGestureRecognizer) ? _handleHorizontalDragUpdate : null;
    renderObject.onVerticalDragUpdate = recognizers.containsKey(VerticalDragGestureRecognizer) ||
                                        recognizers.containsKey(PanGestureRecognizer) ? _handleVerticalDragUpdate : null;
  }
}
