const { ccclass, property } = cc._decorator;
const EPSILON = 1e-4;
const MOVEMENT_FACTOR = .7;

let quintEaseOut = function (time) {
    time -= 1;
    return (time * time * time * time * time + 1);
};


@ccclass
export default class AutoScroll extends cc.Component {

    @property({
        tooltip: '最大可放大倍数'
    })
    maxScale = 1.5;
    @property({
        tooltip: '最小可缩小倍数'
    })
    minScale = .5;

    @property({ tooltip: '多点触摸缩放增量' })
    zoomSpace = 1;

    @property(cc.Node)
    bigMap: cc.Node = null;


    private _touchMovePreviousTimestamp = 0;
    //是否在自动滚动
    private _autoScrolling = false;

    private _touchMoveDisplacements = [];
    private _touchMoveTimeDeltas = [];
    private brake = 0.75;
    private _autoScrollAccumulatedTime = 0;
    private _autoScrollTotalTime = 0;
    private _autoScrollStartPosition = cc.v2(0, 0);
    private _autoScrollTargetDelta: cc.Vec2 = cc.Vec2.ZERO;
    private _isHandleMultiTouch = false;
    //是否在手动聚焦坐标
    private _isOnDestScrolling = false;

    onLoad() {
        this.bigMap.on(cc.Node.EventType.TOUCH_START, this.onTouchStart, this);
        this.bigMap.on(cc.Node.EventType.TOUCH_MOVE, this.onTouchMove, this);
        this.bigMap.on(cc.Node.EventType.TOUCH_END, this.onTouchEnd, this);
        this.bigMap.on(cc.Node.EventType.TOUCH_CANCEL, this.onTouchCancel, this);

    }

    onDestroy() {
        this.bigMap.off(cc.Node.EventType.TOUCH_START, this.onTouchStart, this);
        this.bigMap.off(cc.Node.EventType.TOUCH_MOVE, this.onTouchMove, this);
        this.bigMap.off(cc.Node.EventType.TOUCH_END, this.onTouchEnd, this);
        this.bigMap.off(cc.Node.EventType.TOUCH_CANCEL, this.onTouchCancel, this);
    }

    start() {
    }

    onTouchStart(e: cc.Event.EventTouch) {
        this.resetTouchInfos();
    }

    onTouchMove(e: cc.Event.EventTouch) {
        if (this._isOnDestScrolling) {
            return;
        }
        let touches = e.getTouches();
        if (touches.length == 1) {
            let deltaMove = e.getDelta();
            this.moveContent(deltaMove);
            this.gatherTouchMove(deltaMove);
        }
        else {
            this._isHandleMultiTouch = true;
            let t0: cc.Touch = touches[0];
            let t1: cc.Touch = touches[1];
            let preSpace = t0.getPreviousLocation().sub(t1.getPreviousLocation()).mag();
            let curSpace = t0.getLocation().sub(t1.getLocation()).mag();
            let space = curSpace - preSpace;
            if (true) {
                let center = t0.getStartLocation().add(t1.getStartLocation()).divide(2);
                let n = this.node.convertToNodeSpaceAR(center);
                let w = this.node.convertToWorldSpaceAR(n);
                let nb = this.bigMap.convertToNodeSpaceAR(w);

                let targetScale = this.bigMap.scale + (space >= 0 ? this.zoomSpace : -this.zoomSpace);
                if (targetScale > this.maxScale) {
                    targetScale = this.maxScale;
                }
                else if (targetScale < this.minScale) {
                    targetScale = this.minScale;
                }
                let scaleSpace = targetScale - this.bigMap.scale;
                let v = nb.mul(-scaleSpace);
                //缩放后地图需要反向移动
                this.bigMap.scale = targetScale;
                this.moveContent(v)
            }
        }
    }

    onTouchEnd(e: cc.Event.EventTouch) {
        if (this._isOnDestScrolling) {
            return;
        }
        if (!this._isHandleMultiTouch) {
            this.gatherTouchMove(e.getDelta());
            this.startInertiaScroll();
        }
        this._isHandleMultiTouch = false;
    }
    onTouchCancel(e: cc.Event.EventTouch) {
        if (this._isOnDestScrolling) {
            return;
        }
        if (!this._isHandleMultiTouch) {
            this.gatherTouchMove(e.getDelta());
            this.startInertiaScroll();
        }
        this._isHandleMultiTouch = false;
    }

    update(dt: number): void {
        if (this._autoScrolling) {
            this.processAutoScrolling(dt);
        }
    }

    calculateTouchMoveVelocity(): cc.Vec2 {
        let totalTime = 0;
        totalTime = this._touchMoveTimeDeltas.reduce(function (a, b) {
            return a + b;
        }, totalTime);

        if (totalTime <= 0 || totalTime >= 0.5) {
            return cc.v2(0, 0);
        }

        let totalMovement = cc.v2(0, 0);
        totalMovement = this._touchMoveDisplacements.reduce(function (a, b) {
            return a.add(b);
        }, totalMovement);

        return cc.v2(totalMovement.x * (1 - this.brake) / totalTime,
            totalMovement.y * (1 - this.brake) / totalTime);
    }

    processAutoScrolling(dt) {

        this._autoScrollAccumulatedTime += dt;
        let percentage = Math.min(1, this._autoScrollAccumulatedTime / this._autoScrollTotalTime);
        percentage = quintEaseOut(percentage);
        let newPosition = this._autoScrollStartPosition.add(this._autoScrollTargetDelta.mul(percentage));
        let reachedEnd = Math.abs(percentage - 1) <= EPSILON;
        if (reachedEnd) {
            this._autoScrolling = false;
        }

        let deltaMove = newPosition.sub(this.bigMap.getPosition());
        this.moveContent(this._clampDelta(deltaMove));
    }

    startAttenuatingAutoScroll(deltaMove, initialVelocity) {

        let time = Math.sqrt(Math.sqrt(initialVelocity.mag() / 5));

        let targetDelta = deltaMove.normalize();
        let contentSize = this.bigMap.getContentSize();
        let scrollviewSize = this.node.getContentSize();

        let totalMoveWidth = (contentSize.width - scrollviewSize.width);
        let totalMoveHeight = (contentSize.height - scrollviewSize.height);

        let attenuatedFactorX = this.calculateAttenuatedFactor(totalMoveWidth);
        let attenuatedFactorY = this.calculateAttenuatedFactor(totalMoveHeight);

        targetDelta = cc.v2(targetDelta.x * totalMoveWidth * (1 - this.brake) * attenuatedFactorX, targetDelta.y * totalMoveHeight * attenuatedFactorY * (1 - this.brake));

        let originalMoveLength = deltaMove.mag();
        let factor = targetDelta.mag() / originalMoveLength;
        targetDelta = targetDelta.add(deltaMove);

        if (this.brake > 0 && factor > 7) {
            factor = Math.sqrt(factor);
            targetDelta = deltaMove.mul(factor).add(deltaMove);
        }

        if (this.brake > 0 && factor > 3) {
            factor = 3;
            time = time * factor;
        }

        if (this.brake === 0 && factor > 1) {
            time = time * factor;
        }
        this.startAutoScroll(targetDelta, time);
    }

    calculateAttenuatedFactor(distance) {
        if (this.brake <= 0) {
            return (1 - this.brake);
        }
        return (1 - this.brake) * (1 / (1 + distance * 0.000014 + distance * distance * 0.000000008));
    }

    startInertiaScroll() {
        let touchMoveVelocity = this.calculateTouchMoveVelocity();
        if (!touchMoveVelocity.fuzzyEquals(cc.v2(0, 0), EPSILON) && this.brake < 1) {
            let inertiaTotalMovement = touchMoveVelocity.mul(MOVEMENT_FACTOR);
            this.startAttenuatingAutoScroll(inertiaTotalMovement, touchMoveVelocity);
        }
    }

    startAutoScroll(deltaMove, timeInSecond) {
        this._autoScrolling = true;
        this._autoScrollTargetDelta = deltaMove;
        this._autoScrollStartPosition = this.bigMap.getPosition();
        this._autoScrollTotalTime = timeInSecond;
        this._autoScrollAccumulatedTime = 0;
    }
    _clampDelta(delta) {
        let contentSize = this.bigMap.getContentSize();
        let scrollViewSize = this.node.getContentSize();
        if (contentSize.width < scrollViewSize.width) {
            delta.x = 0;
        }
        if (contentSize.height < scrollViewSize.height) {
            delta.y = 0;
        }

        return delta;
    }

    moveContent(deltaMove, duration: number = 0): Promise<void> {
        return new Promise((resolve) => {
            let adjustedMove = deltaMove;
            let newPosition = this.bigMap.getPosition().add(adjustedMove);

            if (newPosition.x > this.boundX) {
                newPosition.x = this.boundX;
                this._autoScrolling = false;
            }
            else if (newPosition.x < -this.boundX) {
                newPosition.x = -this.boundX;
                this._autoScrolling = false;
            }
            if (newPosition.y > this.boundY) {
                newPosition.y = this.boundY;
                this._autoScrolling = false;
            }
            else if (newPosition.y < -this.boundY) {
                newPosition.y = -this.boundY;
                this._autoScrolling = false;
            }
            if (duration > 0) {
                cc.tween(this.bigMap)
                    .to(duration, { position: cc.v3(newPosition.x, newPosition.y) })
                    .call(() => {
                        resolve();
                    })
                    .start();
            }
            else {
                this.bigMap.setPosition(newPosition);
                resolve();
            }
        })
    }

    getTimeInMilliseconds() {
        let currentTime = new Date();
        return currentTime.getMilliseconds();
    }

    gatherTouchMove(delta) {
        delta = this._clampDelta(delta);

        while (this._touchMoveDisplacements.length >= 5) {
            this._touchMoveDisplacements.shift();
            this._touchMoveTimeDeltas.shift();
        }

        this._touchMoveDisplacements.push(delta);

        let timeStamp = this.getTimeInMilliseconds();
        this._touchMoveTimeDeltas.push((timeStamp - this._touchMovePreviousTimestamp) / 1000);
        this._touchMovePreviousTimestamp = timeStamp;
    }

    get boundY() {
        return this.bigMap.height * this.bigMap.scaleY / 2 - this.node.height / 2;
    }
    get boundX() {
        return this.bigMap.width * this.bigMap.scaleX / 2 - this.node.width / 2;
    }

    /**
     * 地图滚动
     * @param dest 目标地点
     * @param duration 动画时间
     */
    scrollTo(dest: cc.Vec2, duration: number = 0): Promise<void> {
        this._isOnDestScrolling = true;
        this.resetTouchInfos();
        return new Promise((resolve) => {
            this.moveContent(dest.mul(-1), duration).then(() => {
                this._isOnDestScrolling = false;
                resolve();
            })
        })
    }

    resetTouchInfos() {
        this._autoScrolling = false;
        this._touchMovePreviousTimestamp = this.getTimeInMilliseconds();
        this._touchMoveDisplacements.length = 0;
        this._touchMoveTimeDeltas.length = 0;
        this._isHandleMultiTouch = false;
    }
}
