// 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:math' as math;

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

/// An implementation of scroll physics that matches iOS.
///
/// See also:
///
///  * [ClampingScrollSimulation], which implements Android scroll physics.
class BouncingScrollSimulation extends Simulation {
  /// Creates a simulation group for scrolling on iOS, with the given
  /// parameters.
  ///
  /// The position and velocity arguments must use the same units as will be
  /// expected from the [x] and [dx] methods respectively (typically logical
  /// pixels and logical pixels per second respectively).
  ///
  /// The leading and trailing extents must use the unit of length, the same
  /// unit as used for the position argument and as expected from the [x]
  /// method (typically logical pixels).
  ///
  /// The units used with the provided [SpringDescription] must similarly be
  /// consistent with the other arguments. A default set of constants is used
  /// for the `spring` description if it is omitted; these defaults assume
  /// that the unit of length is the logical pixel.
  BouncingScrollSimulation({
    required double position,
    required double velocity,
    required this.leadingExtent,
    required this.trailingExtent,
    required this.spring,
    Tolerance tolerance = Tolerance.defaultTolerance,
  }) : assert(position != null),
       assert(velocity != null),
       assert(leadingExtent != null),
       assert(trailingExtent != null),
       assert(leadingExtent <= trailingExtent),
       assert(spring != null),
       super(tolerance: tolerance) {
    if (position < leadingExtent) {
      _springSimulation = _underscrollSimulation(position, velocity);
      _springTime = double.negativeInfinity;
    } else if (position > trailingExtent) {
      _springSimulation = _overscrollSimulation(position, velocity);
      _springTime = double.negativeInfinity;
    } else {
      // Taken from UIScrollView.decelerationRate (.normal = 0.998)
      // 0.998^1000 = ~0.135
      _frictionSimulation = FrictionSimulation(0.135, position, velocity);
      final double finalX = _frictionSimulation.finalX;
      if (velocity > 0.0 && finalX > trailingExtent) {
        _springTime = _frictionSimulation.timeAtX(trailingExtent);
        _springSimulation = _overscrollSimulation(
          trailingExtent,
          math.min(_frictionSimulation.dx(_springTime), maxSpringTransferVelocity),
        );
        assert(_springTime.isFinite);
      } else if (velocity < 0.0 && finalX < leadingExtent) {
        _springTime = _frictionSimulation.timeAtX(leadingExtent);
        _springSimulation = _underscrollSimulation(
          leadingExtent,
          math.min(_frictionSimulation.dx(_springTime), maxSpringTransferVelocity),
        );
        assert(_springTime.isFinite);
      } else {
        _springTime = double.infinity;
      }
    }
    assert(_springTime != null);
  }

  /// The maximum velocity that can be transferred from the inertia of a ballistic
  /// scroll into overscroll.
  static const double maxSpringTransferVelocity = 5000.0;

  /// When [x] falls below this value the simulation switches from an internal friction
  /// model to a spring model which causes [x] to "spring" back to [leadingExtent].
  final double leadingExtent;

  /// When [x] exceeds this value the simulation switches from an internal friction
  /// model to a spring model which causes [x] to "spring" back to [trailingExtent].
  final double trailingExtent;

  /// The spring used to return [x] to either [leadingExtent] or [trailingExtent].
  final SpringDescription spring;

  late FrictionSimulation _frictionSimulation;
  late Simulation _springSimulation;
  late double _springTime;
  double _timeOffset = 0.0;

  Simulation _underscrollSimulation(double x, double dx) {
    return ScrollSpringSimulation(spring, x, leadingExtent, dx);
  }

  Simulation _overscrollSimulation(double x, double dx) {
    return ScrollSpringSimulation(spring, x, trailingExtent, dx);
  }

  Simulation _simulation(double time) {
    final Simulation simulation;
    if (time > _springTime) {
      _timeOffset = _springTime.isFinite ? _springTime : 0.0;
      simulation = _springSimulation;
    } else {
      _timeOffset = 0.0;
      simulation = _frictionSimulation;
    }
    return simulation..tolerance = tolerance;
  }

  @override
  double x(double time) => _simulation(time).x(time - _timeOffset);

  @override
  double dx(double time) => _simulation(time).dx(time - _timeOffset);

  @override
  bool isDone(double time) => _simulation(time).isDone(time - _timeOffset);

  @override
  String toString() {
    return '${objectRuntimeType(this, 'BouncingScrollSimulation')}(leadingExtent: $leadingExtent, trailingExtent: $trailingExtent)';
  }
}

const double _inflexion = 0.35;

/// An implementation of scroll physics that matches Android.
///
/// See also:
///
///  * [BouncingScrollSimulation], which implements iOS scroll physics.
//
// This class is based on Scroller.java from Android:
//   https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget
//
// The "See..." comments below refer to Scroller methods and values. Some
// simplifications have been made.
class ClampingScrollSimulation extends Simulation {
  /// Creates a scroll physics simulation that matches Android scrolling.
  ClampingScrollSimulation({
    required this.position,
    required this.velocity,
    this.friction = 0.015,
    Tolerance tolerance = Tolerance.defaultTolerance,
  }) : super(tolerance: tolerance) {
    _duration = _splineFlingDuration(velocity);
    _distance = _splineFlingDistance(velocity);
  }

  /// The position of the particle at the beginning of the simulation.
  final double position;

  /// The velocity at which the particle is traveling at the beginning of the
  /// simulation.
  final double velocity;

  /// The amount of friction the particle experiences as it travels.
  ///
  /// The more friction the particle experiences, the sooner it stops.
  final double friction;

  late int _duration;
  late double _distance;

  // See DECELERATION_RATE.
  static final double _kDecelerationRate = math.log(0.78) / math.log(0.9);

  // See computeDeceleration().
  static double _decelerationForFriction(double friction) {
    return 9.80665 *
        39.37 *
        friction *
        1.0 * // Flutter operates on logical pixels so the DPI should be 1.0.
        160.0;
  }

  // See getSplineDeceleration().
  double _splineDeceleration(double velocity) {
    return math.log(_inflexion * velocity.abs() / (friction * _decelerationForFriction(0.84)));
  }

  // See getSplineFlingDuration().
  int _splineFlingDuration(double velocity) {
    final double deceleration = _splineDeceleration(velocity);
    return (1000 * math.exp(deceleration / (_kDecelerationRate - 1.0))).round();
  }

  // See getSplineFlingDistance().
  double _splineFlingDistance(double velocity) {
    final double l = _splineDeceleration(velocity);
    final double decelMinusOne = _kDecelerationRate - 1.0;
    return friction *
        _decelerationForFriction(0.84) *
        math.exp(_kDecelerationRate / decelMinusOne * l);
  }

  @override
  double x(double time) {
    if (time == 0) {
      return position;
    }
    final _NBSample sample = _NBSample(time, _duration);
    return position + (sample.distanceCoef * _distance) * velocity.sign;
  }

  @override
  double dx(double time) {
    if (time == 0) {
      return velocity;
    }
    final _NBSample sample = _NBSample(time, _duration);
    return sample.velocityCoef * _distance / _duration * velocity.sign * 1000.0;
  }

  @override
  bool isDone(double time) {
    return time * 1000.0 >= _duration;
  }
}

class _NBSample {
  _NBSample(double time, int duration) {
    // See computeScrollOffset().
    final double t = time * 1000.0 / duration;
    final int index = (_nbSamples * t).clamp(0, _nbSamples).round();
    _distanceCoef = 1.0;
    _velocityCoef = 0.0;
    if (index < _nbSamples) {
      final double tInf = index / _nbSamples;
      final double tSup = (index + 1) / _nbSamples;
      final double dInf = _splinePosition[index];
      final double dSup = _splinePosition[index + 1];
      _velocityCoef = (dSup - dInf) / (tSup - tInf);
      _distanceCoef = dInf + (t - tInf) * _velocityCoef;
    }
  }

  late double _velocityCoef;
  double get velocityCoef => _velocityCoef;

  late double _distanceCoef;
  double get distanceCoef => _distanceCoef;

  static const int _nbSamples = 100;

  // Generated from dev/tools/generate_android_spline_data.dart.
  static final List<double> _splinePosition = <double>[
    0.000022888183591973643,
    0.028561000304762274,
    0.05705195792956655,
    0.08538917797618413,
    0.11349556286812107,
    0.14129881694635613,
    0.16877157254923383,
    0.19581093511175632,
    0.22239649722992452,
    0.24843841866631658,
    0.2740024733220569,
    0.298967680744136,
    0.32333234658228116,
    0.34709556909569184,
    0.3702249257894571,
    0.39272483400399893,
    0.41456988647721615,
    0.43582889025419114,
    0.4564192786416,
    0.476410299013587,
    0.4957560715637827,
    0.5145493169954743,
    0.5327205670880077,
    0.5502846891191615,
    0.5673274324802855,
    0.583810881323224,
    0.5997478744397482,
    0.615194045299478,
    0.6301165005270208,
    0.6445484042257972,
    0.6585198219185201,
    0.6720397744233084,
    0.6850997688076114,
    0.6977281404741683,
    0.7099506591298411,
    0.7217749311525871,
    0.7331784038850426,
    0.7442308394229518,
    0.7549087205105974,
    0.7652471277371271,
    0.7752251637549381,
    0.7848768260203478,
    0.7942056937103814,
    0.8032299679689082,
    0.8119428702388629,
    0.8203713516576219,
    0.8285187880808974,
    0.8363794492831295,
    0.8439768562813565,
    0.851322799855549,
    0.8584111051351724,
    0.8652534074722162,
    0.8718525580962131,
    0.8782333271742155,
    0.8843892099362031,
    0.8903155590440985,
    0.8960465359221951,
    0.9015574505919048,
    0.9068736766459904,
    0.9119951682409297,
    0.9169321898723632,
    0.9216747065581234,
    0.9262420604674766,
    0.9306331858366086,
    0.9348476990715433,
    0.9389007110754832,
    0.9427903495057521,
    0.9465220679845756,
    0.9500943036519721,
    0.9535176728088761,
    0.9567898524767604,
    0.959924306623116,
    0.9629127700159108,
    0.9657622101750765,
    0.9684818726275105,
    0.9710676079044347,
    0.9735231939498,
    0.9758514437576309,
    0.9780599066560445,
    0.9801485715370128,
    0.9821149805689633,
    0.9839677526782791,
    0.9857085499421516,
    0.9873347811966005,
    0.9888547171706613,
    0.9902689443512227,
    0.9915771042095881,
    0.9927840651641069,
    0.9938913963715834,
    0.9948987305580712,
    0.9958114963810524,
    0.9966274782266875,
    0.997352148697352,
    0.9979848677523623,
    0.9985285021374979,
    0.9989844084453229,
    0.9993537595844986,
    0.999638729860106,
    0.9998403888004533,
    0.9999602810470701,
    1.0,
  ];
}