// Copyright 2014 The Flutter 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:ui';

import 'framework.dart';

/// The interface for defining the algorithm for a boundary that a specified shape is dragged within.
///
/// See also:
///  * [DragBoundary], an [InheritedWidget] that provides a [DragBoundaryDelegate] to its descendants.
///
/// `T` is a data class that defines the shape being dragged. For example, when dragging a rectangle within the boundary,
/// `T` should be a `Rect`.
abstract class DragBoundaryDelegate<T> {
  /// Returns whether the specified dragged object is within the boundary.
  bool isWithinBoundary(T draggedObject);

  /// Returns the given dragged object after moving it fully inside
  /// the boundary with the shortest distance.
  ///
  /// If the bounds cannot contain the dragged object, an exception is thrown.
  T nearestPositionWithinBoundary(T draggedObject);
}

class _DragBoundaryDelegateForRect extends DragBoundaryDelegate<Rect> {
  _DragBoundaryDelegateForRect(this.boundary);
  final Rect? boundary;
  @override
  bool isWithinBoundary(Rect draggedObject) {
    if (boundary == null) {
      return true;
    }
    return boundary!.contains(draggedObject.topLeft) &&
        boundary!.contains(draggedObject.bottomRight);
  }

  @override
  Rect nearestPositionWithinBoundary(Rect draggedObject) {
    if (boundary == null) {
      return draggedObject;
    }
    if (boundary!.right - draggedObject.width < boundary!.left ||
        boundary!.bottom - draggedObject.height < boundary!.top) {
      throw FlutterError(
        'The rect is larger than the boundary. '
        'The rect width must be less than the boundary width, and the rect height must be less than the boundary height.',
      );
    }
    final double left = clampDouble(
      draggedObject.left,
      boundary!.left,
      boundary!.right - draggedObject.width,
    );
    final double top = clampDouble(
      draggedObject.top,
      boundary!.top,
      boundary!.bottom - draggedObject.height,
    );
    return Rect.fromLTWH(left, top, draggedObject.width, draggedObject.height);
  }
}

/// Provides a [DragBoundaryDelegate] for its descendants whose bounds are those defined by this widget.
///
/// [forRectOf] and [forRectMaybeOf] returns a delegate for a drag object of type [Rect].
///
/// {@tool dartpad}
/// This example demonstrates dragging a red box, constrained within the bounds
/// of a green box.
///
/// ** See code in examples/api/lib/widgets/gesture_detector/gesture_detector.3.dart **
/// {@end-tool}
class DragBoundary extends InheritedWidget {
  /// Creates a widget that provides a boundary to its descendants.
  const DragBoundary({required super.child, super.key});

  /// {@template flutter.widgets.DragBoundary.forRectOf}
  /// Retrieve the [DragBoundary] from the nearest ancestor to
  /// get its [DragBoundaryDelegate] of [Rect].
  ///
  /// The [useGlobalPosition] specifies whether to retrieve the [DragBoundaryDelegate] of type
  /// [Rect] in global coordinates. If false, the local coordinates of the boundary are used. Defaults to true.
  /// {@endtemplate}
  ///
  /// If no [DragBoundary] ancestor is found, the delegate will return a delegate that allows the drag object to move freely.
  static DragBoundaryDelegate<Rect> forRectOf(
    BuildContext context, {
    bool useGlobalPosition = true,
  }) {
    return forRectMaybeOf(context, useGlobalPosition: useGlobalPosition) ??
        _DragBoundaryDelegateForRect(null);
  }

  /// {@macro flutter.widgets.DragBoundary.forRectOf}
  ///
  /// returns null if not ancestor is found.
  static DragBoundaryDelegate<Rect>? forRectMaybeOf(
    BuildContext context, {
    bool useGlobalPosition = true,
  }) {
    final InheritedElement? element = context
        .getElementForInheritedWidgetOfExactType<DragBoundary>();
    if (element == null) {
      return null;
    }
    final rb = element.findRenderObject() as RenderBox?;
    assert(rb != null && rb.hasSize, 'DragBoundary is not available');
    final Rect boundary = useGlobalPosition
        ? Rect.fromPoints(
            rb!.localToGlobal(Offset.zero),
            rb.localToGlobal(rb.size.bottomRight(Offset.zero)),
          )
        : Offset.zero & rb!.size;
    return _DragBoundaryDelegateForRect(boundary);
  }

  @override
  bool updateShouldNotify(covariant InheritedWidget oldWidget) {
    return true;
  }
}
