/*
    Author: Jpeng
    Email: peng8350@gmail.com
    createTime:2018-05-14 15:39
 */

// ignore_for_file: INVALID_USE_OF_PROTECTED_MEMBER
// ignore_for_file: INVALID_USE_OF_VISIBLE_FOR_TESTING_MEMBER
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'dart:math' as math;
import '../smart_refresher.dart';
import 'slivers.dart';

typedef VoidFutureCallBack = Future<void> Function();

typedef void OffsetCallBack(double offset);

typedef void ModeChangeCallBack<T>(T mode);

/// a widget  implements ios pull down refresh effect and Android material RefreshIndicator overScroll effect
abstract class RefreshIndicator extends StatefulWidget {
  /// refresh display style
  final RefreshStyle refreshStyle;

  /// the visual extent indicator
  final double height;

  //layout offset
  final double offset;

  /// the stopped time when refresh complete or fail
  final Duration completeDuration;

  const RefreshIndicator(
      {Key key,
      this.height: 60.0,
      this.offset: 0.0,
      this.completeDuration: const Duration(milliseconds: 500),
      this.refreshStyle: RefreshStyle.Follow})
      : super(key: key);
}

/// a widget  implements  pull up load
abstract class LoadIndicator extends StatefulWidget {
  /// load more display style
  final LoadStyle loadStyle;

  /// the visual extent indicator
  final double height;

  /// callback when user click footer
  final VoidCallback onClick;

  const LoadIndicator(
      {Key key,
      this.onClick,
      this.loadStyle: LoadStyle.ShowAlways,
      this.height: 60.0})
      : super(key: key);
}

/// Internal Implementation of Head Indicator
///
/// you can extends RefreshIndicatorState for custom header,if you want to active complex animation effect
///
/// here is the most simple example
///
/// ```dart
///
/// class RunningHeaderState extends RefreshIndicatorState<RunningHeader>
///    with TickerProviderStateMixin {
///  AnimationController _scaleAnimation;
///  AnimationController _offsetController;
///  Tween<Offset> offsetTween;
///
///  @override
///  void initState() {
///    // TODO: implement initState
///    _scaleAnimation = AnimationController(vsync: this);
///    _offsetController = AnimationController(
///        vsync: this, duration: Duration(milliseconds: 1000));
///    offsetTween = Tween(end: Offset(0.6, 0.0), begin: Offset(0.0, 0.0));
///    super.initState();
///  }
///
///  @override
///  void onOffsetChange(double offset) {
///    // TODO: implement onOffsetChange
///    if (!floating) {
///      _scaleAnimation.value = offset / 80.0;
///    }
///    super.onOffsetChange(offset);
///  }
///
///  @override
///  void resetValue() {
///    // TODO: implement handleModeChange
///    _scaleAnimation.value = 0.0;
///    _offsetController.value = 0.0;
///  }
///
///  @override
///  void dispose() {
///    // TODO: implement dispose
///    _scaleAnimation.dispose();
///    _offsetController.dispose();
///    super.dispose();
///  }
///
///  @override
///  Future<void> endRefresh() {
///    // TODO: implement endRefresh
///    return _offsetController.animateTo(1.0).whenComplete(() {});
///  }
///
///  @override
/// Widget buildContent(BuildContext context, RefreshStatus mode) {
///    // TODO: implement buildContent
///    return SlideTransition(
///      child: ScaleTransition(
///        child: (mode != RefreshStatus.idle || mode != RefreshStatus.canRefresh)
///            ? Image.asset("images/custom_2.gif")
///            : Image.asset("images/custom_1.jpg"),
///        scale: _scaleAnimation,
///      ),
///      position: offsetTween.animate(_offsetController),
///    );
///  }
/// }
/// ```
abstract class RefreshIndicatorState<T extends RefreshIndicator>
    extends State<T>
    with IndicatorStateMixin<T, RefreshStatus>, RefreshProcessor {
  bool _inVisual() {
    return _position.pixels < 0.0;
  }

  double _calculateScrollOffset() {
    return (floating
            ? (mode == RefreshStatus.twoLeveling ||
                    mode == RefreshStatus.twoLevelOpening ||
                    mode == RefreshStatus.twoLevelClosing
                ? SmartRefresher.ofState(context).viewportExtent
                : widget.height)
            : 0.0) -
        _position?.pixels;
  }

  @override
  void _handleOffsetChange() {
    // TODO: implement _handleOffsetChange
    super._handleOffsetChange();
    final double overscrollPast = _calculateScrollOffset();
    onOffsetChange(overscrollPast);
  }

  // handle the  state change between canRefresh and idle canRefresh  before refreshing
  void _dispatchModeByOffset(double offset) {
    if (mode == RefreshStatus.twoLeveling) {
      if (_position.pixels > configuration.closeTwoLevelDistance &&
          activity is BallisticScrollActivity) {
        refresher.controller.twoLevelComplete();
        return;
      }
    }
    if (RefreshStatus.twoLevelOpening == mode ||
        mode == RefreshStatus.twoLevelClosing) {
      return;
    }
    if (floating) return;
    // no matter what activity is done, when offset ==0.0 and !floating,it should be set to idle for setting ifCanDrag
    if (offset == 0.0) {
      mode = RefreshStatus.idle;
    }

    // If FrontStyle overScroll,it shouldn't disable gesture in scrollable
    if (_position.extentBefore == 0.0 &&
        widget.refreshStyle == RefreshStyle.Front) {
      _position.context.setIgnorePointer(false);
    }
    // Sometimes different devices return velocity differently, so it's impossible to judge from velocity whether the user
    // has invoked animateTo (0.0) or the user is dragging the view.Sometimes animateTo (0.0) does not return velocity = 0.0
    // velocity < 0.0 may be spring up,>0.0 spring down
    if ((configuration.enableBallisticRefresh && activity.velocity < 0.0) ||
        activity is DragScrollActivity ||
        activity is DrivenScrollActivity) {
      if (refresher.enablePullDown &&
          offset >= configuration.headerTriggerDistance) {
        if (!configuration.skipCanRefresh) {
          mode = RefreshStatus.canRefresh;
        } else {
          floating = true;
          update();
          readyToRefresh().then((_) {
            if (!mounted) return;
            mode = RefreshStatus.refreshing;
          });
        }
      } else if (refresher.enablePullDown) {
        mode = RefreshStatus.idle;
      }
      if (refresher.enableTwoLevel &&
          offset >= configuration.twiceTriggerDistance) {
        mode = RefreshStatus.canTwoLevel;
      } else if (refresher.enableTwoLevel && !refresher.enablePullDown) {
        mode = RefreshStatus.idle;
      }
    }
    //mostly for spring back
    else if (activity is BallisticScrollActivity) {
      if (RefreshStatus.canRefresh == mode) {
        // refreshing
        floating = true;
        update();
        readyToRefresh().then((_) {
          if (!mounted) return;
          mode = RefreshStatus.refreshing;
        });
      }
      if (mode == RefreshStatus.canTwoLevel) {
        // enter twoLevel
        floating = true;
        update();
        if (!mounted) return;

        mode = RefreshStatus.twoLevelOpening;
      }
    }
  }

  void _handleModeChange() {
    if (!mounted) {
      return;
    }
    update();
    if (mode == RefreshStatus.idle || mode == RefreshStatus.canRefresh) {
      floating = false;

      resetValue();

      if (mode == RefreshStatus.idle)
        SmartRefresher.ofState(context).setCanDrag(true);
    }
    if (mode == RefreshStatus.completed || mode == RefreshStatus.failed) {
      endRefresh().then((_) {
        if (!mounted) return;
        floating = false;
        if (mode == RefreshStatus.completed || mode == RefreshStatus.failed) {
          SmartRefresher.ofState(context)
              .setCanDrag(configuration.enableScrollWhenRefreshCompleted);
        }
        update();
        /*
          handle two Situation:
          1.when user dragging to refreshing, then user scroll down not to see the indicator,then it will not spring back,
          the _onOffsetChange didn't callback,it will keep failed or success state.
          2. As FrontStyle,when user dragging in 0~100 in refreshing state,it should be reset after the state change
          */
        WidgetsBinding.instance.addPostFrameCallback((_) {
          if (!mounted) {
            return;
          }
          if (widget.refreshStyle == RefreshStyle.Front) {
            if (_inVisual()) {
              _position.jumpTo(0.0);
            }
            mode = RefreshStatus.idle;
          } else {
            if (!_inVisual()) {
              mode = RefreshStatus.idle;
            } else {
              activity.delegate.goBallistic(0.0);
            }
          }
        });
      });
    } else if (mode == RefreshStatus.refreshing) {
      if (!floating) {
        floating = true;
        readyToRefresh();
      }
      if (configuration.enableRefreshVibrate ?? false) {
        HapticFeedback.vibrate();
      }
      if (refresher.onRefresh != null) refresher.onRefresh();
    } else if (mode == RefreshStatus.twoLevelOpening) {
      floating = true;
      SmartRefresher.ofState(context).setCanDrag(false);
      WidgetsBinding.instance.addPostFrameCallback((_) {
        if (!mounted) return;
        activity.resetActivity();
        _position
            .animateTo(0.0,
                duration: const Duration(milliseconds: 500),
                curve: Curves.linear)
            .whenComplete(() {
          mode = RefreshStatus.twoLeveling;
        });
        if (refresher.onTwoLevel != null) refresher.onTwoLevel();
      });
    } else if (mode == RefreshStatus.twoLevelClosing) {
      floating = false;
      SmartRefresher.ofState(context).setCanDrag(false);
      update();
    } else if (mode == RefreshStatus.twoLeveling) {
      SmartRefresher.ofState(context)
          .setCanDrag(configuration.enableScrollWhenTwoLevel);
    }
    onModeChange(mode);
  }

  // the method can provide a callback to implements some animation
  Future<void> readyToRefresh() {
    return Future.value();
  }

  // it mean the state will enter success or fail
  Future<void> endRefresh() {
    return Future.delayed(widget.completeDuration);
  }

  bool needReverseAll() {
    return true;
  }

  void resetValue() {}

  @override
  Widget build(BuildContext context) {
    return SliverRefresh(
        paintOffsetY: widget.offset,
        child: RotatedBox(
          child: buildContent(context, mode),
          quarterTurns: needReverseAll() &&
                  Scrollable.of(context).axisDirection == AxisDirection.up
              ? 10
              : 0,
        ),
        floating: floating,
        refreshIndicatorLayoutExtent: mode == RefreshStatus.twoLeveling ||
                mode == RefreshStatus.twoLevelOpening ||
                mode == RefreshStatus.twoLevelClosing
            ? SmartRefresher.ofState(context).viewportExtent
            : widget.height,
        refreshStyle: widget.refreshStyle);
  }
}

abstract class LoadIndicatorState<T extends LoadIndicator> extends State<T>
    with IndicatorStateMixin<T, LoadStatus>, LoadingProcessor {
  // use to update between one page and above one page
  bool _isHide = false;
  bool _enableLoading = false;
  LoadStatus _lastMode = LoadStatus.idle;

  double _calculateScrollOffset() {
    final double overScrollPastEnd =
        math.max(_position.pixels - _position.maxScrollExtent, 0.0);
    return overScrollPastEnd;
  }

  void enterLoading() {
    setState(() {
      floating = true;
    });
    _enableLoading = false;
    readyToLoad().then((_) {
      if (!mounted) {
        return;
      }
      mode = LoadStatus.loading;
    });
  }

  @override
  Future endLoading() {
    // TODO: implement endLoading
    return Future.delayed(Duration(milliseconds: 0));
  }

  void finishLoading() {
    if (!floating) {
      return;
    }
    endLoading().then((_) {
      if (!mounted) {
        return;
      }

      // this line for patch bug temporary:indicator disappears fastly when load more complete
      if (mounted) Scrollable.of(context).position.correctBy(0.00001);
      WidgetsBinding.instance.addPostFrameCallback((_) {
        if (mounted && _position?.outOfRange == true) {
          activity.delegate.goBallistic(0);
        }
      });
      setState(() {
        floating = false;
      });
    });
  }

  bool _checkIfCanLoading() {
    if (_position.maxScrollExtent - _position.pixels <=
            configuration.footerTriggerDistance &&
        _position.extentBefore > 2.0 &&
        _enableLoading) {
      if (!configuration.autoLoad && mode == LoadStatus.idle) {
        return false;
      }
      if (!configuration.enableLoadingWhenFailed && mode == LoadStatus.failed) {
        return false;
      }
      if (!configuration.enableLoadingWhenNoData && mode == LoadStatus.noMore) {
        return false;
      }
      if (mode != LoadStatus.canLoading &&
          _position.userScrollDirection == ScrollDirection.forward) {
        return false;
      }
      return true;
    }
    return false;
  }

  void _handleModeChange() {
    if (!mounted || _isHide) {
      return;
    }

    update();
    if (mode == LoadStatus.idle ||
        mode == LoadStatus.failed ||
        mode == LoadStatus.noMore) {
      // #292,#265,#208
      // stop the slow bouncing when load more too fast
      if (_position.activity.velocity < 0 &&
          _lastMode == LoadStatus.loading &&
          !_position.outOfRange &&
          _position is ScrollActivityDelegate) {
        _position.beginActivity(
            IdleScrollActivity(_position as ScrollActivityDelegate));
      }

      finishLoading();
    }
    if (mode == LoadStatus.loading) {
      if (!floating) {
        enterLoading();
      }
      if (configuration.enableLoadMoreVibrate ?? false) {
        HapticFeedback.vibrate();
      }
      if (refresher.onLoading != null) {
        refresher.onLoading();
      }
      if (widget.loadStyle == LoadStyle.ShowWhenLoading) {
        floating = true;
      }
    } else {
      if (activity is! DragScrollActivity) _enableLoading = false;
    }
    _lastMode = mode;
    onModeChange(mode);
  }

  void _dispatchModeByOffset(double offset) {
    if (!mounted || _isHide || LoadStatus.loading == mode || floating) {
      return;
    }
    if (activity is DragScrollActivity) {
      if (_checkIfCanLoading()) {
        mode = LoadStatus.canLoading;
      } else {
        mode = _lastMode;
      }
    }
    if (activity is BallisticScrollActivity) {
      if (configuration.enableBallisticLoad ?? true) {
        if (_checkIfCanLoading()) enterLoading();
      } else if (mode == LoadStatus.canLoading) {
        enterLoading();
      }
    }
  }

  void _handleOffsetChange() {
    if (_isHide) {
      return;
    }
    super._handleOffsetChange();
    final double overscrollPast = _calculateScrollOffset();
    onOffsetChange(overscrollPast);
  }

  void _listenScrollEnd() {
    if (!_position.isScrollingNotifier.value) {
      // when user release gesture from screen
      if (_isHide || mode == LoadStatus.loading || mode == LoadStatus.noMore) {
        return;
      }

      if (_checkIfCanLoading()) {
        if (activity is IdleScrollActivity) {
          if ((configuration.enableBallisticLoad ?? true) ||
              ((!configuration.enableBallisticLoad ?? true) &&
                  mode == LoadStatus.canLoading)) enterLoading();
        }
      }
    } else {
      if (activity is DragScrollActivity || activity is DrivenScrollActivity) {
        _enableLoading = true;
      }
    }
  }

  void _onPositionUpdated(ScrollPosition newPosition) {
    _position?.isScrollingNotifier?.removeListener(_listenScrollEnd);
    newPosition?.isScrollingNotifier?.addListener(_listenScrollEnd);
    super._onPositionUpdated(newPosition);
  }

  @override
  void didChangeDependencies() {
    // TODO: implement didChangeDependencies
    super.didChangeDependencies();
    _lastMode = mode;
  }

  @override
  void dispose() {
    // TODO: implement dispose
    _position?.isScrollingNotifier?.removeListener(_listenScrollEnd);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return SliverLoading(
        hideWhenNotFull: configuration.hideFooterWhenNotFull,
        floating: widget.loadStyle == LoadStyle.ShowAlways
            ? true
            : widget.loadStyle == LoadStyle.HideAlways
                ? false
                : floating,
        shouldFollowContent: configuration.shouldFooterFollowWhenNotFull != null
            ? configuration.shouldFooterFollowWhenNotFull(mode)
            : mode == LoadStatus.noMore,
        layoutExtent: widget.height,
        mode: mode,
        child: LayoutBuilder(
          builder: (BuildContext context, BoxConstraints cons) {
            _isHide = cons.biggest.height == 0.0;
            return GestureDetector(
              behavior: HitTestBehavior.opaque,
              onTap: () {
                if ((mode == LoadStatus.idle && !configuration.autoLoad) ||
                    (_mode.value == LoadStatus.failed)) {
                  enterLoading();
                }
                if (widget.onClick != null) {
                  widget.onClick();
                }
              },
              child: buildContent(context, mode),
            );
          },
        ));
  }
}

/// mixin in IndicatorState,it will get position and remove when dispose,init mode state
///
/// help to finish the work that the header indicator and footer indicator need to do
mixin IndicatorStateMixin<T extends StatefulWidget, V> on State<T> {
  SmartRefresher refresher;

  RefreshConfiguration configuration;

  bool _floating = false;

  set floating(floating) => _floating = floating;

  get floating => _floating;

  set mode(mode) => _mode?.value = mode;

  get mode => _mode?.value;

  ValueNotifier<V> _mode;

  ScrollActivity get activity => _position.activity;

  // it doesn't support get the ScrollController as the listener, because it will cause "multiple scrollview use one ScollController"
  // error,only replace the ScrollPosition to listen the offset
  ScrollPosition _position;

  // update ui
  void update() {
    if (mounted) setState(() {});
  }

  void _handleOffsetChange() {
    if (!mounted) {
      return;
    }
    final double overscrollPast = _calculateScrollOffset();
    if (overscrollPast < 0.0) {
      return;
    }
    if (refresher.onOffsetChange != null) {
      refresher.onOffsetChange(V == RefreshStatus, overscrollPast);
    }
    _dispatchModeByOffset(overscrollPast);
  }

  void disposeListener() {
    _mode?.removeListener(_handleModeChange);
    _position?.removeListener(_handleOffsetChange);
    _position = null;
    _mode = null;
  }

  void _updateListener() {
    configuration = RefreshConfiguration.of(context);
    refresher = SmartRefresher.of(context);
    ValueNotifier<V> newMode = V == RefreshStatus
        ? refresher.controller.headerMode
        : refresher.controller.footerMode;
    final ScrollPosition newPosition = Scrollable.of(context).position;
    if (newMode != _mode) {
      _mode?.removeListener(_handleModeChange);
      _mode = newMode;
      _mode?.addListener(_handleModeChange);
    }
    if (newPosition != _position) {
      _position?.removeListener(_handleOffsetChange);
      _onPositionUpdated(newPosition);
      _position = newPosition;
      _position?.addListener(_handleOffsetChange);
    }
  }

  @override
  void initState() {
    // TODO: implement initState
    if (V == RefreshStatus) {
      SmartRefresher.of(context)?.controller?.headerMode?.value =
          RefreshStatus.idle;
    }
    super.initState();
  }

  @override
  void dispose() {
    // TODO: implement dispose
    //1.3.7: here need to careful after add asSliver builder
    disposeListener();
    super.dispose();
  }

  @override
  void didChangeDependencies() {
    // TODO: implement didChangeDependencies
    _updateListener();
    super.didChangeDependencies();
  }

  @override
  void didUpdateWidget(T oldWidget) {
    // TODO: implement didUpdateWidget
    // needn't to update _headerMode,because it's state will never change
    // 1.3.7: here need to careful after add asSliver builder
    _updateListener();
    super.didUpdateWidget(oldWidget);
  }

  void _onPositionUpdated(ScrollPosition newPosition) {
    refresher.controller.onPositionUpdated(newPosition);
  }

  void _handleModeChange();

  double _calculateScrollOffset();

  void _dispatchModeByOffset(double offset);

  Widget buildContent(BuildContext context, V mode);
}

/// head Indicator exposure interface
abstract class RefreshProcessor {
  /// out of edge offset callback
  void onOffsetChange(double offset) {}

  /// mode change callback
  void onModeChange(RefreshStatus mode) {}

  /// when indicator is ready into refresh,it will call back and waiting for this function finish,then callback onRefresh
  Future readyToRefresh() {
    return Future.value();
  }

  // when indicator is ready to dismiss layout ,it will callback and then spring back after finish
  Future endRefresh() {
    return Future.value();
  }

  // when indicator has been spring back,it  need to reset value
  void resetValue() {}
}

/// footer Indicator exposure interface
abstract class LoadingProcessor {
  void onOffsetChange(double offset) {}

  void onModeChange(LoadStatus mode) {}

  /// when indicator is ready into refresh,it will call back and waiting for this function finish,then callback onRefresh
  Future readyToLoad() {
    return Future.value();
  }

  // when indicator is ready to dismiss layout ,it will callback and then spring back after finish
  Future endLoading() {
    return Future.value();
  }

  // when indicator has been spring back,it  need to reset value
  void resetValue() {}
}