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

/// Flutter widget for displaying fading edge at start/end of scroll views
class SCFadingEdgeScrollView extends StatefulWidget {
  /// child widget
  final Widget child;

  /// scroll controller of child widget
  ///
  /// Look for more documentation at [ScrollView.scrollController]
  final ScrollController scrollController;

  /// Whether the scroll view scrolls in the reading direction.
  ///
  /// Look for more documentation at [ScrollView.reverse]
  final bool reverse;

  /// The axis along which child view scrolls
  ///
  /// Look for more documentation at [ScrollView.scrollDirection]
  final Axis scrollDirection;

  /// what part of screen on start half should be covered by fading edge gradient
  /// [gradientFractionOnStart] must be 0 <= [gradientFractionOnStart] <= 1
  /// 0 means no gradient,
  /// 1 means gradients on start half of widget fully covers it
  final double gradientFractionOnStart;

  /// what part of screen on end half should be covered by fading edge gradient
  /// [gradientFractionOnEnd] must be 0 <= [gradientFractionOnEnd] <= 1
  /// 0 means no gradient,
  /// 1 means gradients on start half of widget fully covers it
  final double gradientFractionOnEnd;

  /// set to true if you want scrollController passed to widget to be disposed when widget's state is disposed
  final bool shouldDisposeScrollController;

  const SCFadingEdgeScrollView._internal({
    Key? key,
    required this.child,
    required this.scrollController,
    required this.reverse,
    required this.scrollDirection,
    required this.gradientFractionOnStart,
    required this.gradientFractionOnEnd,
    required this.shouldDisposeScrollController,
  })  : assert(child != null),
        assert(scrollController != null),
        assert(reverse != null),
        assert(scrollDirection != null),
        assert(gradientFractionOnStart >= 0 && gradientFractionOnStart <= 1),
        assert(gradientFractionOnEnd >= 0 && gradientFractionOnEnd <= 1),
        super(key: key);

  /// Constructor for creating [SCFadingEdgeScrollView] with [ScrollView] as child
  /// child must have [ScrollView.controller] set
  factory SCFadingEdgeScrollView.fromScrollView({
    Key? key,
    required ScrollView child,
    double gradientFractionOnStart = 0.1,
    double gradientFractionOnEnd = 0.1,
    bool shouldDisposeScrollController = false,
  }) {
    assert(child.controller != null, "Child must have controller set");

    return SCFadingEdgeScrollView._internal(
      key: key,
      child: child,
      scrollController: child.controller!,
      scrollDirection: child.scrollDirection,
      reverse: child.reverse,
      gradientFractionOnStart: gradientFractionOnStart,
      gradientFractionOnEnd: gradientFractionOnEnd,
      shouldDisposeScrollController: shouldDisposeScrollController,
    );
  }

  /// Constructor for creating [SCFadingEdgeScrollView] with [SingleChildScrollView] as child
  /// child must have [SingleChildScrollView.controller] set
  factory SCFadingEdgeScrollView.fromSingleChildScrollView({
    Key? key,
    required SingleChildScrollView child,
    double gradientFractionOnStart = 0.1,
    double gradientFractionOnEnd = 0.1,
    bool shouldDisposeScrollController = false,
  }) {
    assert(child.controller != null, "Child must have controller set");

    return SCFadingEdgeScrollView._internal(
      key: key,
      child: child,
      scrollController: child.controller!,
      scrollDirection: child.scrollDirection,
      reverse: child.reverse,
      gradientFractionOnStart: gradientFractionOnStart,
      gradientFractionOnEnd: gradientFractionOnEnd,
      shouldDisposeScrollController: shouldDisposeScrollController,
    );
  }

  /// Constructor for creating [SCFadingEdgeScrollView] with [PageView] as child
  /// child must have [PageView.controller] set
  factory SCFadingEdgeScrollView.fromPageView({
    Key? key,
    required PageView child,
    double gradientFractionOnStart = 0.1,
    double gradientFractionOnEnd = 0.1,
    bool shouldDisposeScrollController = false,
  }) {
    assert(child.controller != null, "Child must have controller set");

    return SCFadingEdgeScrollView._internal(
      key: key,
      child: child,
      scrollController: child.controller!,
      scrollDirection: child.scrollDirection,
      reverse: child.reverse,
      gradientFractionOnStart: gradientFractionOnStart,
      gradientFractionOnEnd: gradientFractionOnEnd,
      shouldDisposeScrollController: shouldDisposeScrollController,
    );
  }

  /// Constructor for creating [SCFadingEdgeScrollView] with [AnimatedList] as child
  /// child must have [AnimatedList.controller] set
  factory SCFadingEdgeScrollView.fromAnimatedList({
    Key? key,
    required AnimatedList child,
    double gradientFractionOnStart = 0.1,
    double gradientFractionOnEnd = 0.1,
    bool shouldDisposeScrollController = false,
  }) {
    assert(child.controller != null, "Child must have controller set");

    return SCFadingEdgeScrollView._internal(
      key: key,
      child: child,
      scrollController: child.controller!,
      scrollDirection: child.scrollDirection,
      reverse: child.reverse,
      gradientFractionOnStart: gradientFractionOnStart,
      gradientFractionOnEnd: gradientFractionOnEnd,
      shouldDisposeScrollController: shouldDisposeScrollController,
    );
  }

  @override
  _FadingEdgeScrollViewState createState() => _FadingEdgeScrollViewState();
}

class _FadingEdgeScrollViewState extends State<SCFadingEdgeScrollView>
    with WidgetsBindingObserver {
 late ScrollController _controller;
   bool _isScrolledToStart = false;
   bool _isScrolledToEnd = false;

  @override
  void initState() {
    super.initState();

    _controller = widget.scrollController;
    _isScrolledToStart = _controller.initialScrollOffset == 0;
    _controller.addListener(_onScroll);

    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (_controller == null) {
        return;
      }

      if (_isScrolledToEnd == null &&
          _controller.position.maxScrollExtent == 0) {
        setState(() {
          _isScrolledToEnd = true;
        });
      }
    });
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
    _controller.removeListener(_onScroll);
    if (widget.shouldDisposeScrollController) {
      _controller.dispose();
    }
  }

  void _onScroll() {
    final offset = _controller.offset;
    final minOffset = _controller.position.minScrollExtent;
    final maxOffset = _controller.position.maxScrollExtent;

    final isScrolledToEnd = offset >= maxOffset;
    final isScrolledToStart = offset <= minOffset;

    if (isScrolledToEnd != _isScrolledToEnd ||
        isScrolledToStart != _isScrolledToStart) {
      setState(() {
        _isScrolledToEnd = isScrolledToEnd;
        _isScrolledToStart = isScrolledToStart;
      });
    }
  }

  @override
  void didChangeMetrics() {
    super.didChangeMetrics();
    setState(() {
      // Add the shading or remove it when the screen resize (web/desktop) or mobile is rotated
      if (_controller.hasClients) {
        final offset = _controller.offset;
        final maxOffset = _controller.position.maxScrollExtent;
        if (maxOffset == 0 && offset == 0) {
          // Not scrollable
          _isScrolledToStart = true;
          _isScrolledToEnd = true;
        } else if (maxOffset == offset) {
          // Scrollable but at end
          _isScrolledToStart = false;
          _isScrolledToEnd = true;
        } else if (maxOffset > 0 && offset == 0) {
          // Scrollable but at start
          _isScrolledToStart = true;
          _isScrolledToEnd = false;
        } else {
          // Scroll in progress/not are either end
          _isScrolledToStart = false;
          _isScrolledToEnd = false;
        }
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_isScrolledToStart == null && _controller.hasClients) {
      final offset = _controller.offset;
      final minOffset = _controller.position.minScrollExtent;
      final maxOffset = _controller.position.maxScrollExtent;

      _isScrolledToEnd = offset >= maxOffset;
      _isScrolledToStart = offset <= minOffset;
    }

    return ShaderMask(
      shaderCallback: (bounds) => LinearGradient(
        begin: _gradientStart,
        end: _gradientEnd,
        stops: [
          0,
          widget.gradientFractionOnStart * 0.5,
          1 - widget.gradientFractionOnEnd * 0.5,
          1,
        ],
        colors: _getColors(
            widget.gradientFractionOnStart > 0 && !(_isScrolledToStart ?? true),
            widget.gradientFractionOnEnd > 0 && !(_isScrolledToEnd ?? false)),
      ).createShader(
        bounds.shift(Offset(-bounds.left, -bounds.top)),
        textDirection: Directionality.of(context),
      ),
      child: widget.child,
      blendMode: BlendMode.dstIn,
    );
  }

  AlignmentGeometry get _gradientStart =>
      widget.scrollDirection == Axis.vertical
          ? _verticalStart
          : _horizontalStart;

  AlignmentGeometry get _gradientEnd =>
      widget.scrollDirection == Axis.vertical ? _verticalEnd : _horizontalEnd;

  Alignment get _verticalStart =>
      widget.reverse ? Alignment.bottomCenter : Alignment.topCenter;

  Alignment get _verticalEnd =>
      widget.reverse ? Alignment.topCenter : Alignment.bottomCenter;

  AlignmentDirectional get _horizontalStart => widget.reverse
      ? AlignmentDirectional.centerEnd
      : AlignmentDirectional.centerStart;

  AlignmentDirectional get _horizontalEnd => widget.reverse
      ? AlignmentDirectional.centerStart
      : AlignmentDirectional.centerEnd;

  List<Color> _getColors(bool isStartEnabled, bool isEndEnabled) => [
    (isStartEnabled ? Colors.transparent : Colors.white),
    Colors.white,
    Colors.white,
    (isEndEnabled ? Colors.transparent : Colors.white)
  ];
}
