// 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.

// ignore_for_file: prefer_initializing_formals

import 'package:PiliPlus/common/widgets/flutter/page/scrollable.dart';
import 'package:flutter/gestures.dart'
    show DragStartBehavior, HorizontalDragGestureRecognizer;
import 'package:flutter/material.dart'
    hide PageView, Scrollable, ScrollableState;
import 'package:flutter/rendering.dart';

class _ForceImplicitScrollPhysics extends ScrollPhysics {
  const _ForceImplicitScrollPhysics({
    required this.allowImplicitScrolling,
    super.parent,
  });

  @override
  _ForceImplicitScrollPhysics applyTo(ScrollPhysics? ancestor) {
    return _ForceImplicitScrollPhysics(
      allowImplicitScrolling: allowImplicitScrolling,
      parent: buildParent(ancestor),
    );
  }

  @override
  final bool allowImplicitScrolling;
}

const PageScrollPhysics _kPagePhysics = PageScrollPhysics();

/// A scrollable list that works page by page.
///
/// Each child of a page view is forced to be the same size as the viewport.
///
/// You can use a [PageController] to control which page is visible in the view.
/// In addition to being able to control the pixel offset of the content inside
/// the [PageView], a [PageController] also lets you control the offset in terms
/// of pages, which are increments of the viewport size.
///
/// The [PageController] can also be used to control the
/// [PageController.initialPage], which determines which page is shown when the
/// [PageView] is first constructed, and the [PageController.viewportFraction],
/// which determines the size of the pages as a fraction of the viewport size.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=J1gE9xvph-A}
///
/// {@tool dartpad}
/// Here is an example of [PageView]. It creates a centered [Text] in each of the three pages
/// which scroll horizontally.
///
/// ** See code in examples/api/lib/widgets/page_view/page_view.0.dart **
/// {@end-tool}
///
/// ## Persisting the scroll position during a session
///
/// Scroll views attempt to persist their scroll position using [PageStorage].
/// For a [PageView], this can be disabled by setting [PageController.keepPage]
/// to false on the [controller]. If it is enabled, using a [PageStorageKey] for
/// the [key] of this widget is recommended to help disambiguate different
/// scroll views from each other.
///
/// See also:
///
///  * [PageController], which controls which page is visible in the view.
///  * [SingleChildScrollView], when you need to make a single child scrollable.
///  * [ListView], for a scrollable list of boxes.
///  * [GridView], for a scrollable grid of boxes.
///  * [ScrollNotification] and [NotificationListener], which can be used to watch
///    the scroll position without using a [ScrollController].
class PageView<T extends HorizontalDragGestureRecognizer>
    extends StatefulWidget {
  /// Creates a scrollable list that works page by page from an explicit [List]
  /// of widgets.
  ///
  /// This constructor is appropriate for page views with a small number of
  /// children because constructing the [List] requires doing work for every
  /// child that could possibly be displayed in the page view, instead of just
  /// those children that are actually visible.
  ///
  /// Like other widgets in the framework, this widget expects that
  /// the [children] list will not be mutated after it has been passed in here.
  /// See the documentation at [SliverChildListDelegate.children] for more details.
  ///
  /// {@template flutter.widgets.PageView.allowImplicitScrolling}
  /// If [allowImplicitScrolling] is true, the [PageView] will participate in
  /// accessibility scrolling more like a [ListView], where implicit scroll
  /// actions will move to the next page rather than into the contents of the
  /// [PageView].
  /// {@endtemplate}
  PageView({
    super.key,
    this.scrollDirection = Axis.horizontal,
    this.reverse = false,
    this.controller,
    this.physics,
    this.pageSnapping = true,
    this.onPageChanged,
    List<Widget> children = const <Widget>[],
    this.dragStartBehavior = DragStartBehavior.start,
    this.allowImplicitScrolling = false,
    ScrollCacheExtent? scrollCacheExtent,
    this.restorationId,
    this.clipBehavior = Clip.hardEdge,
    this.hitTestBehavior = HitTestBehavior.opaque,
    this.scrollBehavior,
    this.padEnds = true,
    required this.horizontalDragGestureRecognizer,
  }) : assert(
         scrollCacheExtent == null ||
             (scrollCacheExtent.value > 0.0) == allowImplicitScrolling,
         'scrollCacheExtent and allowImplicitScrolling must be consistent: '
         'scrollCacheExtent must be greater than 0.0 when allowImplicitScrolling is true, '
         'and must be 0.0 when allowImplicitScrolling is false.',
       ),
       scrollCacheExtent =
           scrollCacheExtent ??
           ScrollCacheExtent.viewport(allowImplicitScrolling ? 1.0 : 0.0),
       childrenDelegate = SliverChildListDelegate(children);

  final GestureRecognizerFactoryConstructor<T> horizontalDragGestureRecognizer;

  /// Creates a scrollable list that works page by page using widgets that are
  /// created on demand.
  ///
  /// This constructor is appropriate for page views with a large (or infinite)
  /// number of children because the builder is called only for those children
  /// that are actually visible.
  ///
  /// Providing a non-null [itemCount] lets the [PageView] compute the maximum
  /// scroll extent.
  ///
  /// [itemBuilder] will be called only with indices greater than or equal to
  /// zero and less than [itemCount].
  ///
  /// {@macro flutter.widgets.ListView.builder.itemBuilder}
  ///
  /// {@template flutter.widgets.PageView.findChildIndexCallback}
  /// The [findChildIndexCallback] corresponds to the
  /// [SliverChildBuilderDelegate.findChildIndexCallback] property. If null,
  /// a child widget may not map to its existing [RenderObject] when the order
  /// of children returned from the children builder changes.
  /// This may result in state-loss. This callback needs to be implemented if
  /// the order of the children may change at a later time.
  /// {@endtemplate}
  ///
  /// {@macro flutter.widgets.PageView.allowImplicitScrolling}
  PageView.builder({
    super.key,
    this.scrollDirection = Axis.horizontal,
    this.reverse = false,
    this.controller,
    this.physics,
    this.pageSnapping = true,
    this.onPageChanged,
    required NullableIndexedWidgetBuilder itemBuilder,
    ChildIndexGetter? findChildIndexCallback,
    int? itemCount,
    this.dragStartBehavior = DragStartBehavior.start,
    this.allowImplicitScrolling = false,
    ScrollCacheExtent? scrollCacheExtent,
    this.restorationId,
    this.clipBehavior = Clip.hardEdge,
    this.hitTestBehavior = HitTestBehavior.opaque,
    this.scrollBehavior,
    this.padEnds = true,
    required this.horizontalDragGestureRecognizer,
  }) : assert(
         scrollCacheExtent == null ||
             (scrollCacheExtent.value > 0.0) == allowImplicitScrolling,
         'scrollCacheExtent and allowImplicitScrolling must be consistent: '
         'scrollCacheExtent must be greater than 0.0 when allowImplicitScrolling is true, '
         'and must be 0.0 when allowImplicitScrolling is false.',
       ),
       scrollCacheExtent =
           scrollCacheExtent ??
           ScrollCacheExtent.viewport(allowImplicitScrolling ? 1.0 : 0.0),
       childrenDelegate = SliverChildBuilderDelegate(
         itemBuilder,
         findChildIndexCallback: findChildIndexCallback,
         childCount: itemCount,
       );

  /// Creates a scrollable list that works page by page with a custom child
  /// model.
  ///
  /// {@tool dartpad}
  /// This example shows a [PageView] that uses a custom [SliverChildBuilderDelegate] to support child
  /// reordering.
  ///
  /// ** See code in examples/api/lib/widgets/page_view/page_view.1.dart **
  /// {@end-tool}
  ///
  /// {@macro flutter.widgets.PageView.allowImplicitScrolling}
  PageView.custom({
    super.key,
    this.scrollDirection = Axis.horizontal,
    this.reverse = false,
    this.controller,
    this.physics,
    this.pageSnapping = true,
    this.onPageChanged,
    required this.childrenDelegate,
    this.dragStartBehavior = DragStartBehavior.start,
    this.allowImplicitScrolling = false,
    ScrollCacheExtent? scrollCacheExtent,
    this.restorationId,
    this.clipBehavior = Clip.hardEdge,
    this.hitTestBehavior = HitTestBehavior.opaque,
    this.scrollBehavior,
    this.padEnds = true,
    required this.horizontalDragGestureRecognizer,
  }) : assert(
         scrollCacheExtent == null ||
             (scrollCacheExtent.value > 0.0) == allowImplicitScrolling,
         'scrollCacheExtent and allowImplicitScrolling must be consistent: '
         'scrollCacheExtent must be greater than 0.0 when allowImplicitScrolling is true, '
         'and must be 0.0 when allowImplicitScrolling is false.',
       ),
       scrollCacheExtent =
           scrollCacheExtent ??
           ScrollCacheExtent.viewport(allowImplicitScrolling ? 1.0 : 0.0);

  /// {@template flutter.widgets.PageView.allowImplicitScrolling}
  /// Controls whether the widget's pages will respond to
  /// [RenderObject.showOnScreen], which will allow for implicit accessibility
  /// scrolling.
  ///
  /// With this flag set to false, when accessibility focus reaches the end of
  /// the current page and the user attempts to move it to the next element, the
  /// focus will traverse to the next widget outside of the page view.
  ///
  /// With this flag set to true, when accessibility focus reaches the end of
  /// the current page and user attempts to move it to the next element, focus
  /// will traverse to the next page in the page view.
  /// {@endtemplate}
  final bool allowImplicitScrolling;

  /// {@macro flutter.rendering.RenderViewportBase.scrollCacheExtent}
  ///
  /// In [PageView], the default [scrollCacheExtent] uses
  /// [ScrollCacheExtent.viewport], where the value represents the number of
  /// viewport lengths to cache beyond the visible area.
  ///
  /// When [PageController.viewportFraction] is 1.0 (the default), this is
  /// equivalent to the number of pages. For example,
  /// `ScrollCacheExtent.viewport(2.0)` caches 2 pages before and after the
  /// visible page.
  ///
  /// When [PageController.viewportFraction] is less than 1.0, multiple pages
  /// may be visible in a single viewport, so `ScrollCacheExtent.viewport(1.0)`
  /// may cache more than one additional page in each direction.
  ///
  /// [ScrollCacheExtent.pixels] can also be used to specify the cache extent
  /// in logical pixels instead of viewport sizes.
  ///
  /// If [scrollCacheExtent] is specified, its value must be consistent with
  /// [allowImplicitScrolling]: the value must be greater than 0.0 when
  /// [allowImplicitScrolling] is true, and must be 0.0 when
  /// [allowImplicitScrolling] is false.
  ///
  /// Defaults to `ScrollCacheExtent.viewport(1.0)` if
  /// [allowImplicitScrolling] is true, and `ScrollCacheExtent.viewport(0.0)` if
  /// [allowImplicitScrolling] is false.
  final ScrollCacheExtent scrollCacheExtent;

  /// {@macro flutter.widgets.scrollable.restorationId}
  final String? restorationId;

  /// The [Axis] along which the scroll view's offset increases with each page.
  ///
  /// For the direction in which active scrolling may be occurring, see
  /// [ScrollDirection].
  ///
  /// Defaults to [Axis.horizontal].
  final Axis scrollDirection;

  /// Whether the page view scrolls in the reading direction.
  ///
  /// For example, if the reading direction is left-to-right and
  /// [scrollDirection] is [Axis.horizontal], then the page view scrolls from
  /// left to right when [reverse] is false and from right to left when
  /// [reverse] is true.
  ///
  /// Similarly, if [scrollDirection] is [Axis.vertical], then the page view
  /// scrolls from top to bottom when [reverse] is false and from bottom to top
  /// when [reverse] is true.
  ///
  /// Defaults to false.
  final bool reverse;

  /// An object that can be used to control the position to which this page
  /// view is scrolled.
  final PageController? controller;

  /// How the page view should respond to user input.
  ///
  /// For example, determines how the page view continues to animate after the
  /// user stops dragging the page view.
  ///
  /// The physics are modified to snap to page boundaries using
  /// [PageScrollPhysics] prior to being used.
  ///
  /// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
  /// [ScrollPhysics] provided by that behavior will take precedence after
  /// [physics].
  ///
  /// Defaults to matching platform conventions.
  final ScrollPhysics? physics;

  /// Set to false to disable page snapping, useful for custom scroll behavior.
  ///
  /// If the [padEnds] is false and [PageController.viewportFraction] < 1.0,
  /// the page will snap to the beginning of the viewport; otherwise, the page
  /// will snap to the center of the viewport.
  final bool pageSnapping;

  /// Called whenever the page in the center of the viewport changes.
  final ValueChanged<int>? onPageChanged;

  /// A delegate that provides the children for the [PageView].
  ///
  /// The [PageView.custom] constructor lets you specify this delegate
  /// explicitly. The [PageView] and [PageView.builder] constructors create a
  /// [childrenDelegate] that wraps the given [List] and [IndexedWidgetBuilder],
  /// respectively.
  final SliverChildDelegate childrenDelegate;

  /// {@macro flutter.widgets.scrollable.dragStartBehavior}
  final DragStartBehavior dragStartBehavior;

  /// {@macro flutter.material.Material.clipBehavior}
  ///
  /// Defaults to [Clip.hardEdge].
  final Clip clipBehavior;

  /// {@macro flutter.widgets.scrollable.hitTestBehavior}
  ///
  /// Defaults to [HitTestBehavior.opaque].
  final HitTestBehavior hitTestBehavior;

  /// {@macro flutter.widgets.scrollable.scrollBehavior}
  ///
  /// The [ScrollBehavior] of the inherited [ScrollConfiguration] will be
  /// modified by default to not apply a [Scrollbar].
  final ScrollBehavior? scrollBehavior;

  /// Whether to add padding to both ends of the list.
  ///
  /// If this is set to true and [PageController.viewportFraction] < 1.0, padding will be added
  /// such that the first and last child slivers will be in the center of
  /// the viewport when scrolled all the way to the start or end, respectively.
  ///
  /// If [PageController.viewportFraction] >= 1.0, this property has no effect.
  ///
  /// This property defaults to true.
  final bool padEnds;

  @override
  State<PageView<T>> createState() => _PageViewState<T>();
}

class _PageViewState<T extends HorizontalDragGestureRecognizer>
    extends State<PageView<T>> {
  int _lastReportedPage = 0;

  late PageController _controller;

  @override
  void initState() {
    super.initState();
    _initController();
    _lastReportedPage = _controller.initialPage;
  }

  @override
  void dispose() {
    if (widget.controller == null) {
      _controller.dispose();
    }
    super.dispose();
  }

  void _initController() {
    _controller = widget.controller ?? PageController();
  }

  @override
  void didUpdateWidget(PageView<T> oldWidget) {
    if (oldWidget.controller != widget.controller) {
      if (oldWidget.controller == null) {
        _controller.dispose();
      }
      _initController();
    }
    super.didUpdateWidget(oldWidget);
  }

  AxisDirection _getDirection(BuildContext context) {
    switch (widget.scrollDirection) {
      case Axis.horizontal:
        assert(debugCheckHasDirectionality(context));
        final TextDirection textDirection = Directionality.of(context);
        final AxisDirection axisDirection = textDirectionToAxisDirection(
          textDirection,
        );
        return widget.reverse
            ? flipAxisDirection(axisDirection)
            : axisDirection;
      case Axis.vertical:
        return widget.reverse ? AxisDirection.up : AxisDirection.down;
    }
  }

  @override
  Widget build(BuildContext context) {
    final AxisDirection axisDirection = _getDirection(context);
    final ScrollPhysics physics =
        _ForceImplicitScrollPhysics(
          allowImplicitScrolling: widget.allowImplicitScrolling,
        ).applyTo(
          widget.pageSnapping
              ? _kPagePhysics.applyTo(
                  widget.physics ??
                      widget.scrollBehavior?.getScrollPhysics(context),
                )
              : widget.physics ??
                    widget.scrollBehavior?.getScrollPhysics(context),
        );

    return NotificationListener<ScrollNotification>(
      onNotification: (ScrollNotification notification) {
        if (notification.depth == 0 &&
            widget.onPageChanged != null &&
            notification is ScrollUpdateNotification) {
          final metrics = notification.metrics as PageMetrics;
          final int currentPage = metrics.page!.round();
          if (currentPage != _lastReportedPage) {
            _lastReportedPage = currentPage;
            widget.onPageChanged!(currentPage);
          }
        }
        return false;
      },
      child: Scrollable<T>(
        dragStartBehavior: widget.dragStartBehavior,
        axisDirection: axisDirection,
        controller: _controller,
        physics: physics,
        restorationId: widget.restorationId,
        hitTestBehavior: widget.hitTestBehavior,
        scrollBehavior:
            widget.scrollBehavior ??
            ScrollConfiguration.of(context).copyWith(scrollbars: false),
        viewportBuilder: (BuildContext context, ViewportOffset position) {
          return Viewport(
            scrollCacheExtent: widget.scrollCacheExtent,
            axisDirection: axisDirection,
            offset: position,
            clipBehavior: widget.clipBehavior,
            slivers: <Widget>[
              SliverFillViewport(
                viewportFraction: _controller.viewportFraction,
                delegate: widget.childrenDelegate,
                padEnds: widget.padEnds,
                allowImplicitScrolling: widget.allowImplicitScrolling,
              ),
            ],
          );
        },
        horizontalDragGestureRecognizer: widget.horizontalDragGestureRecognizer,
      ),
    );
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
    super.debugFillProperties(description);
    description
      ..add(EnumProperty<Axis>('scrollDirection', widget.scrollDirection))
      ..add(FlagProperty('reverse', value: widget.reverse, ifTrue: 'reversed'))
      ..add(
        DiagnosticsProperty<PageController>(
          'controller',
          _controller,
          showName: false,
        ),
      )
      ..add(
        DiagnosticsProperty<ScrollPhysics>(
          'physics',
          widget.physics,
          showName: false,
        ),
      )
      ..add(
        FlagProperty(
          'pageSnapping',
          value: widget.pageSnapping,
          ifFalse: 'snapping disabled',
        ),
      )
      ..add(
        FlagProperty(
          'allowImplicitScrolling',
          value: widget.allowImplicitScrolling,
          ifTrue: 'allow implicit scrolling',
        ),
      )
      ..add(
        DiagnosticsProperty<ScrollCacheExtent>(
          'scrollCacheExtent',
          widget.scrollCacheExtent,
          defaultValue: ScrollCacheExtent.viewport(
            widget.allowImplicitScrolling ? 1.0 : 0.0,
          ),
        ),
      );
  }
}
