/* eslint-disable */
'use strict';

/**
 * Inspired by Android Scroller
 */

import ViscousFluidInterpolator from './ViscousFluidInterpolator';
import {currentAnimationTimeMillis} from './AnimationUtils';

/**
 * The coefficient of friction applied to flings/scrolls.
 * @type {number}
 */
// const SCROLL_FRICTION = 0.015;
const SCROLL_FRICTION = 0.03;

const DEFAULT_DURATION = 250;
const SCROLL_MODE = 0;
const FLING_MODE = 1;

const DECELERATION_RATE = Math.log(0.78) / Math.log(0.9);
const INFLEXION = 0.35; // Tension lines cross at (INFLEXION, 1)
const START_TENSION = 0.5;
const END_TENSION = 1.0;
const P1 = START_TENSION * INFLEXION;
const P2 = 1.0 - END_TENSION * (1.0 - INFLEXION);

const NB_SAMPLES = 100;
const SPLINE_POSITION = [];
const SPLINE_TIME = [];

const GRAVITY_EARTH = 9.80665;

(function () {
	var x_min = 0;
	var y_min = 0;
	for (let i = 0; i < NB_SAMPLES; i++) {
		let alpha = i / NB_SAMPLES;

		let x_max = 1;
		let x, tx, coef;
		while (true) {
			x = x_min + (x_max - x_min) / 2.0;
			coef = 3.0 * x * (1.0 - x);
			tx = coef * ((1.0 - x) * P1 + x * P2) + x * x * x;
			if (Math.abs(tx - alpha) < 1E-5) break;
			if (tx > alpha) x_max = x;
			else x_min = x;
		}
		SPLINE_POSITION[i] = coef * ((1.0 - x) * START_TENSION + x) + x * x * x;

		let y_max = 1.0;
		let y, dy;
		while (true) {
			y = y_min + (y_max - y_min) / 2.0;
			coef = 3.0 * y * (1.0 - y);
			dy = coef * ((1.0 - y) * START_TENSION + y) + y * y * y;
			if (Math.abs(dy - alpha) < 1E-5) break;
			if (dy > alpha) y_max = y;
			else y_min = y;
		}
		SPLINE_TIME[i] = coef * ((1.0 - y) * P1 + y * P2) + y * y * y;
	}
	SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0;
})();

function signum (number) {
	if (isNaN(number)) {
		return NaN;
	}
	var sig = number;
	if (number > 0) {
		sig = 1;
	} else if (number < 0) {
		sig = -1;
	}
	return sig;
}

export default class Scroller {
  /**
   *
   * @param flywheel specify whether or not to support progressive "flywheel" behavior in flinging.
   */
	constructor (flywheel, onScrollCallback) {
		this.mCurrX = 0;
		this.mCurrY = 0;
		this.mFinished = true;
		this.mInterpolator = ViscousFluidInterpolator;
	// this.mPpi = PixelRatio.get() * 160;
		this.mPpi = 160;
		this.mDeceleration = this.computeDeceleration(SCROLL_FRICTION);
		this.mFlywheel = flywheel;

		this.mPhysicalCoeff = this.computeDeceleration(0.84); // look and feel tuning

		this.mFlingFriction = SCROLL_FRICTION;
		this.onScrollCallback = onScrollCallback;
	}

	computeDeceleration (friction) {
		return GRAVITY_EARTH * 39.37 * this.mPpi * friction;
	}

  /**
   * Returns whether the scroller has finished scrolling.
   * @returns {Boolean} True if the scroller has finished scrolling, false otherwise.
   */
	isFinished () {
		return this.mFinished;
	}

  /**
   * Force the finished field to a particular value.
   * @param finished The new finished value.
   */
	forceFinished (finished) {
		this.mFinished = finished;
	}

  /**
   * Returns the current X offset in the scroll.
   * @returns {*} The new X offset as an absolute distance from the origin.
   */
	getCurrX () {
		return this.mCurrX;
	}

  /**
   * Returns the current Y offset in the scroll.
   * @returns {*} The new Y offset as an absolute distance from the origin.
   */
	getCurrY () {
		return this.mCurrY;
	}

	getCurrVelocity () {
		return this.mMode === FLING_MODE ?
	  this.mCurrVelocity : this.mVelocity - this.mDeceleration * this.timePassed() / 2000.0;
	}

	computeScrollOffset () {
		if (this.mFinished) {
			this.onScrollCallback && this.onScrollCallback(0, 0, this);
			return false;
		}

		var timePassed = currentAnimationTimeMillis() - this.mStartTime;

		if (timePassed < this.mDuration) {
			switch (this.mMode) {
			case SCROLL_MODE:
				let x = this.mInterpolator.getInterpolation(timePassed * this.mDurationReciprocal);
				this.mCurrX = this.mStartX + Math.round(x * this.mDeltaX);
				this.mCurrY = this.mStartY + Math.round(x * this.mDeltaY);
				break;
			case FLING_MODE:
				let t = timePassed / this.mDuration;
				let index = parseInt(NB_SAMPLES * t);
				let distanceCoef = 1;
				let velocityCoef = 0;
				if (index < NB_SAMPLES) {
					let t_inf = index / NB_SAMPLES;
					let t_sup = (index + 1) / NB_SAMPLES;
					let d_inf = SPLINE_POSITION[index];
					let d_sup = SPLINE_POSITION[index + 1];
					velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
					distanceCoef = d_inf + (t - t_inf) * velocityCoef;
				}

				this.mCurrVelocity = velocityCoef * this.mDistance / this.mDuration * 1000;

				this.mCurrX = this.mStartX + Math.round(distanceCoef * (this.mFinalX - this.mStartX));
		  // Pin to mMinX <= mCurrX <= mMaxX
		  // this.mCurrX = Math.min(this.mCurrX, this.mMaxX);
		  // this.mCurrX = Math.max(this.mCurrX, this.mMinX);

				this.mCurrY = this.mStartY + Math.round(distanceCoef * (this.mFinalY - this.mStartY));
		  // Pin to mMinY <= mCurrY <= mMaxY
				this.mCurrY = Math.min(this.mCurrY, this.mMaxY);
				this.mCurrY = Math.max(this.mCurrY, this.mMinY);

				if (this.mCurrX == this.mFinalX && this.mCurrY == this.mFinalY) {
					this.mFinished = true;
				}

				break;
			}
		} else {
			this.mCurrX = this.mFinalX;
			this.mCurrY = this.mFinalY;
			this.mFinished = true;
		}

		var dx = this.mCurrX - this.mLastX;
		var dy = this.mCurrY - this.mLastY;

		this.mLastX = this.mCurrX;
		this.mLastY = this.mCurrY;

		this.onScrollCallback && this.onScrollCallback(dx, dy, this);

		if (dx === 0 && dy === 0 && this.mFinished) {
			return false;
		}
		return true;
	}

	startScroll (startX, startY, dx, dy, duration = DEFAULT_DURATION) {
		this.mMode = SCROLL_MODE;
		this.mFinished = false;
		this.mDuration = duration;
		this.mStartTime = currentAnimationTimeMillis();
		this.mStartX = startX;
		this.mStartY = startY;
		this.mFinalX = startX + dx;
		this.mFinalY = startY + dy;
		this.mDeltaX = dx;
		this.mDeltaY = dy;
		this.mDurationReciprocal = 1.0 / this.mDuration;

		this.mLastX = this.mStartX;
		this.mLastY = this.mStartY;

		this.performAnimation();
	}

  /**
   * Start scrolling based on a fling gesture. The distance travelled will
   * depend on the initial velocity of the fling.
   * @param startX
   * @param startY
   * @param velocityX Initial velocity of the fling (X) measured in dp or pt per second
   * @param velocityY Initial velocity of the fling (Y) measured in dp or pt per second
   * @param minX
   * @param maxX
   * @param minY
   * @param maxY
   */
	fling (startX, startY, velocityX, velocityY,
		minX, maxX, minY, maxY) {
	// Continue a scroll or fling in progress
		if (this.mFlywheel && !this.mFinished) {
			let oldVel = this.getCurrVelocity();

			let dx = this.mFinalX - this.mStartX;
			let dy = this.mFinalY - this.mStartY;
			let hyp = Math.hypot(dx, dy);

			let ndx = dx / hyp;
			let ndy = dy / hyp;

			let oldVelocityX = ndx * oldVel;
			let oldVelocityY = ndy * oldVel;
			if (signum(velocityX) === signum(oldVelocityX) &&
		signum(velocityY) === signum(oldVelocityY)) {
				velocityX += oldVelocityX;
				velocityY += oldVelocityY;
			}
		}

		this.mMode = FLING_MODE;
		this.mFinished = false;

		let velocity = Math.hypot(velocityX, velocityY);

		this.mVelocity = velocity;
		this.mDuration = this.getSplineFlingDuration(velocity);
		this.mStartTime = currentAnimationTimeMillis();
		this.mStartX = startX;
		this.mStartY = startY;

		let coeffX = velocity == 0 ? 1.0 : velocityX / velocity;
		let coeffY = velocity == 0 ? 1.0 : velocityY / velocity;

		let totalDistance = this.getSplineFlingDistance(velocity);
		this.mDistance = totalDistance * signum(velocity);

		this.mMinX = minX;
		this.mMaxX = maxX;
		this.mMinY = minY;
		this.mMaxY = maxY;

		this.mFinalX = startX + Math.round(totalDistance * coeffX);
	// Pin to mMinX <= mFinalX <= mMaxX
		this.mFinalX = Math.min(this.mFinalX, this.mMaxX);
		this.mFinalX = Math.max(this.mFinalX, this.mMinX);

		this.mFinalY = startY + Math.round(totalDistance * coeffY);
	// Pin to mMinY <= mFinalY <= mMaxY
		this.mFinalY = Math.min(this.mFinalY, this.mMaxY);
		this.mFinalY = Math.max(this.mFinalY, this.mMinY);

		this.mLastX = this.mStartX;
		this.mLastY = this.mStartY;

		this.performAnimation();
	}

	getSplineDeceleration (velocity) {
		return Math.log(INFLEXION * Math.abs(velocity) / (this.mFlingFriction * this.mPhysicalCoeff));
	}

	getSplineFlingDuration (velocity) {
		var l = this.getSplineDeceleration(velocity);
		var decelMinusOne = DECELERATION_RATE - 1.0;
		return 1000.0 * Math.exp(l / decelMinusOne);
	}

	getSplineFlingDistance (velocity) {
		var l = this.getSplineDeceleration(velocity);
		var decelMinusOne = DECELERATION_RATE - 1.0;
		return this.mFlingFriction * this.mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
	}

	performAnimation () {
		if (this.computeScrollOffset()) {
			requestAnimationFrame(this.performAnimation.bind(this));
		} else {
		}
	}

	abortAnimation () {
		this.mCurrX = this.mFinalX;
		this.mCurrY = this.mFinalY;
		this.mFinished = true;
	}

	extendDuration (extend) {
		var passed = timePassed();
		this.mDuration = passed + extend;
		this.mDurationReciprocal = 1.0 / this.mDuration;
		this.mFinished = false;
	}

	timePassed () {
		return currentAnimationTimeMillis() - this.mStartTime;
	}

	setFinalX (newX) {
		this.mFinalX = newX;
		this.mDeltaX = this.mFinalX - this.mStartX;
		this.mFinished = false;
	}

	setFinalY (newY) {
		this.mFinalY = newY;
		this.mDeltaY = this.mFinalY - this.mStartY;
		this.mFinished = false;
	}

	debugInfo () {
		return 'cur=' + this.mCurrX + ' ' + this.mCurrY + ', final=' + this.mFinalX + ' ' + this.mFinalY;
	}
}
/* eslint-enable */
