import { Ease } from "../../tween";
import { Container } from "../display";
import { Event, MouseEvent } from "../events";
import { Graphics } from "../graphics";


/**
 * 滚动视图
 */
export class ScrollView extends Container {

    protected _scrollWidth: number;
    /**
     * 滚动宽度
     * 设置0表示不让滚动
     */
    get scrollWidth() {
        return this._scrollWidth
    }
    set scrollWidth(v: number) {
        if (this._scrollWidth == v) return;
        this._scrollWidth = v;
        //改变
        this.checkPosition(this.bounceTime)
    }

    protected _scrollHeight: number;
    /**
     * 滚动高度
     * 设置0表示不让滚动
     */
    get scrollHeight() {
        return this._scrollHeight
    }
    set scrollHeight(v: number) {
        if (this._scrollHeight == v) return;
        this._scrollHeight = v;
        //改变
        this.checkPosition(this.bounceTime)
    }
    /**
     * 最大滚动坐标x，为负
     */
    private get maxScrollX(): number {
        //不能滚动，就是0
        if (!this.scrollWidth) return 0;
        //
        return Math.min(this.viewWidth - this.scrollWidth, 0);
    };
    /**
     * 最大滚动坐标y，为负
     */
    private get maxScrollY(): number {
        //不能滚动，就是0
        if (!this.scrollHeight) return 0;

        return Math.min(this.viewHeight - this.scrollHeight, 0);
    };

    //容器
    protected _content: Container;
    /**
     * 滚动内容的节点容器
     * 注意滚动内容都往这上面加
     */
    get view() {
        return this._content;
    }
    /**
     * 可见区域的宽
     * @property viewWidth
     * @type {number}
     * @protected
     * @since 1.0.0
     * @default 0
     */
    protected viewWidth: number = 0;
    /**
     * 可见区域的高
     * @property viewHeight
     * @type {number}
     * @protected
     * @since 1.0.0
     * @default 0
     */
    protected viewHeight: number = 0;

    /**
     * 遮罩对象
     * @property maskObj
     * @since 1.0.0
     * @private
     * @type {Graphics}
     */
    private maskObj: Graphics;

    private downX: number
    private downY: number
    private distX: number;
    private distY: number;

    private startX: number
    private startY: number
    private destX: number
    private destY: number
    private isRunning: boolean;
    private startTime: number;
    private destTime: number;
    private duration: number;
    private easeFn: (t: number) => number;

    private static weight = [1, 1.33, 1.66, 2, 2.33];
    private velocitys: [number, number][] = [];
    private lastTouchTime: number
    /**
     * 手指拖动超过边界时是否允许回弹。默认true
     */
    public isBounce: boolean = true
    /**
     * 回弹时间，视图内容在不合理的位置时恢复的时间，比如超过边界时
     */
    public bounceTime: number = 300;
    /**
     * 手指抬起后是否存在惯性。默认true
     */
    public hasMomentum: boolean = true;
    /**
     * 惯性的阻尼
     * 默认0.002
     */
    public damping: number = 0.002
    /**
     * 0无交互状态
     * 1点击状态
     * 2移动状态
     */
    private mouseStatus: 0 | 1 | 2 = 0;

    /**
     * 注意滚动内容在view里加
     * @method  ScrollPage
     * @param {number} vW 可视区域宽
     * @param {number} vH 可视区域高
     * @param {number} sW 最大横向滚动距离
     * @param {number} sH 最大纵向滚动距离
     * @param {boolean} isFullScreen 是否全屏
     * @example
     *      var scroll=new ScrollView(750,1000,0,2000);
     *      stage.addChild(scroll);
     *      //加滚动内容
     *      scroll.view.addChild(new Sprite());
     */
    constructor(
        vW: number,
        vH: number,
        sW: number,
        sH: number,
        isFullScreen?: boolean
    ) {
        super();
        let s: ScrollView = this;
        s._instanceType = "ScrollView";
        //视图容器，必须用super.addChildAt,其他都被重写了
        s._content = super.addChildAt(new Container(), 0);
        s.maskObj = super.addChildAt(new Graphics(), 0);
        s.maskObj.alpha = 0;
        //有传过才赋值
        if (typeof sW == "number") s._scrollWidth = sW;
        if (typeof sH == "number") s._scrollHeight = sH;
        //设置第一次
        s.setViewRect(vW, vH, isFullScreen);
        //添加到舞台，需要注意local坐标替换成stage坐标
        s.addEventListener(Event.ADDED_TO_STAGE, function (e: Event) {
            s.stage.addEventListener(MouseEvent.MOUSE_MOVE, s.onMouseEvent, s);
            s.stage.addEventListener(MouseEvent.MOUSE_UP, s.onMouseEvent, s);
        });
        s.addEventListener(Event.REMOVED_FROM_STAGE, function (e: Event) {
            s.stage.removeEventListener(MouseEvent.MOUSE_MOVE, s.onMouseEvent, s);
            s.stage.removeEventListener(MouseEvent.MOUSE_UP, s.onMouseEvent, s);
        });
        s.addEventListener(MouseEvent.MOUSE_DOWN, s.onMouseEvent, s, false);
        // s.addEventListener(MouseEvent.MOUSE_MOVE, s.onMouseEvent, s);
        // s.addEventListener(MouseEvent.MOUSE_UP, s.onMouseEvent, s);
        // s.addEventListener(MouseEvent.MOUSE_OUT, s.onMouseEvent, s);
        s.addEventListener(Event.ENTER_FRAME, s.onEnterFrame, s)
    }
    /**
     * 设置可见区域，可见区域的坐标始终在本地坐标中0,0点位置
     * @method setViewRect
     * @param {number}w 设置可见区域的宽
     * @param {number}h 设置可见区域的高
     * @param {boolean} isVertical 方向
     * @public
     * @since 1.1.1
     */
    public setViewRect(w: number, h: number, isFull?: boolean): void {
        let s = this;
        //不全屏才设置mask
        if (!isFull) {
            //加遮罩
            s._content.mask = s.maskObj;
            //为了能接收鼠标事件设置_isUsedToMask
            s.maskObj._isUsedToMask = false;
            //绘制
            s.maskObj.clear()
                .beginFill(0)
                .drawRect(0, 0, w, h)
                .endFill();
        }
        //移除遮罩
        else {
            s._content.mask = null;
        }
        //视窗尺寸
        s.viewWidth = w;
        s.viewHeight = h;
        //改变
        this.checkPosition(s.bounceTime)
    }
    private onMouseEvent(e: MouseEvent): void {
        let s = this;
        //点的时候
        if (e.type == MouseEvent.MOUSE_DOWN) {
            s.isRunning = false;
            //阻止冒泡
            e.stopPropagation()
            //标记一下状态
            s.mouseStatus = 1;
            s.lastTouchTime = Date.now();
            //记录点击的位置
            s.downX = e.stageX;
            s.downY = e.stageY;
            s.distX = 0;
            s.distY = 0;
        } else if (e.type == MouseEvent.MOUSE_MOVE) {
            //不是down过来的
            if (s.mouseStatus < 1) return;
            //计算间隔
            let deltaX = e.stageX - s.downX,
                deltaY = e.stageY - s.downY,
                timestamp = Date.now();

            s.downX = e.stageX;
            s.downY = e.stageY;

            s.distX += deltaX;
            s.distY += deltaY;
            let absDistX = Math.abs(s.distX);
            let absDistY = Math.abs(s.distY);
            //上一步是开始的，超出10才滑动
            if (s.mouseStatus == 1 &&
                absDistX < 10 &&
                absDistY < 10
            ) {
                s.lastTouchTime = timestamp;
                return
            }
            //这个先不加
            // if (s.isLocked) {
            //     if (absDistX > absDistY + s.lockDis) {
            //         deltaY = 0;
            //     } else if (absDistY >= absDistX + s.lockDis) {
            //         deltaX = 0;
            //     }
            // }
            let oriX = s._content.x,
                oriY = s._content.y;

            deltaX = s.scrollWidth ? deltaX : 0;
            deltaY = s.scrollHeight ? deltaY : 0;

            //计算速度
            let deltaT = timestamp - s.lastTouchTime, sx = 0, sy = 0;

            let newX = oriX + deltaX;
            let newY = oriY + deltaY;

            if (newX > 0 || newX < s.maxScrollX) {
                newX = s.isBounce ? oriX + deltaX / 3 : newX > 0 ? 0 : s.maxScrollX;
            } else {
                sx = deltaX / deltaT
            }
            if (newY > 0 || newY < s.maxScrollY) {
                newY = s.isBounce ? oriY + deltaY / 3 : newY > 0 ? 0 : s.maxScrollY;
            } else {
                sy = deltaY / deltaT
            }
            //记录速度
            s.pushVelocitys(sx, sy)
            s.lastTouchTime = timestamp;

            s._translate(newX, newY);

            //事件处理
            if (s.mouseStatus == 1) {
                s.dispatchEvent(Event.SCROLL_START, {
                    x: -newX,
                    y: -newY,
                    deltaX: oriX - newX,
                    deltaY: oriY - newY
                });
            }
            s.mouseStatus = 2;
        } else {
            //超出边界了
            if (s.checkPosition(s.bounceTime)) {
                s.mouseStatus = 0;
                return;
            }
            //状态不是move过来的，重置状态后return
            if (s.mouseStatus != 2) {
                s.mouseStatus = 0;
                return;
            }
            s.mouseStatus = 0;

            let newX: number = s._content.x,
                newY: number = s._content.y,
                time: number = 0;
            //有缓冲
            if (s.hasMomentum && s.velocitys.length) {
                //计算速度
                let sum = { x: 0, y: 0 }, totalW = 0;
                for (let i = 0; i < s.velocitys.length; i++) {
                    let v = s.velocitys[i];
                    let w = ScrollView.weight[i];
                    sum.x += v[0] * w;
                    sum.y += v[1] * w;
                    totalW += w;
                }
                s.velocitys.length = 0;
                let sx = sum.x / totalW,
                    sy = sum.y / totalW;

                let momentumX = (s.scrollWidth && Math.abs(sx) > 0.02) ? toMomentum(newX, sx, s.maxScrollX, s.damping) : {
                    destination: newX,
                    duration: 0
                };
                let momentumY = (s.scrollHeight && Math.abs(sy) > 0.02) ? toMomentum(newY, sy, s.maxScrollY, s.damping) : {
                    destination: newY,
                    duration: 0
                };
                newX = momentumX.destination;
                newY = momentumY.destination;
                time = Math.max(momentumX.duration, momentumY.duration);
            }
            if (newX != s._content.x || newY != s._content.y) {
                s._scrollTo(newX, newY, time);
                return;
            }
            s.dispatchEvent(Event.SCROLL_STOP, {
                x: -s._content.x,
                y: -s._content.y,
                deltaX: 0,
                deltaY: 0
            });
        }
    }

    private onEnterFrame(e: Event) {
        let s = this;
        let now = Date.now();
        //用户点击交互时需要加
        if (s.mouseStatus && s.lastTouchTime && now - s.lastTouchTime > 100 && now - s.lastTouchTime < 300) {
            s.pushVelocitys(0, 0);
        }
        if (!s.isRunning) return;
        //时间到了
        if (now >= s.destTime) {
            s.isRunning = false;
            const { x, y } = s._content;
            //移动到最终位置
            s._translate(s.destX, s.destY);
            //检查是否合法，
            if (!s.checkPosition(s.bounceTime)) {
                let data = {
                    x: -s.destX,
                    y: -s.destY,
                    deltaX: x - s.destX,
                    deltaY: y - s.destX,
                };
                s.dispatchEvent(Event.SCROLL_STOP, data);
                if (s.destX == 0 && s.destY == 0) {
                    s.dispatchEvent(Event.SCROLL_TO_HEAD, data);
                }
                if (s.destX == s.maxScrollX && s.destY == s.maxScrollY) {
                    s.dispatchEvent(Event.SCROLL_TO_END, data);
                }
            }
        } else {
            now = (now - s.startTime) / s.duration;
            var easing = s.easeFn(now);
            s._translate(
                (s.destX - s.startX) * easing + s.startX,
                (s.destY - s.startY) * easing + s.startY
            );
        }
    }

    private pushVelocitys(vx: number, vy: number) {
        this.velocitys.push([vx, vy]);
        if (this.velocitys.length > ScrollView.weight.length) this.velocitys.shift();
    }
    /**
     * 滚到指定的坐标位置
     * @param {number} x 往左横向滚动距离，0到最大横向滚动距离
     * @param {number} y 往上纵向滚动距离，0到最大纵向滚动距离
     * @param {number} time 滚动需要的时间 默认为0 即没有缓动直接到达指定位置，毫秒计
     * @param {Function} easeFn 缓动函数，不传默认用Ease.circOut，从快到慢
     * @public
     */
    public scrollTo(x: number, y: number, time: number = 0, easeFn?: (t: number) => number): void {
        //xy都是相对左上角正向的，所以真正_content的坐标应该取负
        //判断范围
        const [nx, ny] = this._modifyXY(-x, -y);
        //真实滚动
        this._scrollTo(nx, ny, time, easeFn);
    }
    /**
     * 从当前位置继续滚动
     * @param {number} x 往左滚动距离
     * @param {number} y 往上滚动距离
     * @param {number} time 滚动需要的时间 默认为0 即没有缓动直接到达指定位置，毫秒计
     * @param {Function} easeFn 缓动函数，不传默认用Ease.circOut，从快到慢
     * @public
     */
    public scrollBy(x: number, y: number, time: number = 0, easeFn?: (t: number) => number) {
        let s = this;
        //最终的位置
        x = s._content.x - x;
        y = s._content.y - y;
        //判断范围
        const [nx, ny] = this._modifyXY(x, y);
        s._scrollTo(nx, ny, time, easeFn);
    }
    /**
     * 真实的滚动，xy一般都是负数
     * @param x 
     * @param y 
     * @param time 
     * @param easeFn 缓动函数，不传默认用Ease.circOut
     * @returns 
     */
    private _scrollTo(
        x: number,
        y: number,
        time: number = 0,
        easeFn?: (t: number) => number
    ) {
        let s = this;
        //判断下
        if (isNaN(x) || isNaN(y)) return;
        if (!time) {
            s.isRunning = false;
            s._translate(x, y);
        } else {
            s.startX = s._content.x;
            s.startY = s._content.y;
            s.startTime = Date.now();
            s.destTime = s.startTime + time;
            s.destX = x;
            s.destY = y;
            s.duration = time;
            s.easeFn = easeFn || Ease.quadOut;
            s.isRunning = true;
        }
    }
    /**
     * _content位置的改变都用这个方法
     * @param x 
     * @param y 
     */
    private _translate(x: number, y: number) {
        let s = this;
        let { x: oriX, y: oriY } = s._content;
        s._content.x = x;
        s._content.y = y;
        //事件，位置变了
        if (oriX != x || oriY != y) {
            s.dispatchEvent(Event.SCROLLING, {
                x: -x,
                y: -y,
                deltaX: oriX - x,
                deltaY: oriY - y
            });
        }
    }
    /**
     * 检查当前的位置是否合理，不合理会返回true，且滚动到对应位置
     * @param time 
     * @returns 
     */
    protected checkPosition(time: number = 0): boolean {
        //原先坐标
        const { x, y } = this._content;
        //矫正坐标
        const [nx, ny] = this._modifyXY(x, y);
        // 完全一致，就不处理
        if (x == nx && y == ny) return false;
        //滚动
        this._scrollTo(nx, ny, time);
        return true
    }
    /**
     * 矫正坐标
     * @param x 
     * @param y 
     * @returns 
     */
    private _modifyXY(x: number, y: number) {
        let { scrollWidth, maxScrollX, scrollHeight, maxScrollY } = this;
        if (!scrollWidth || x > 0) {
            x = 0;
        } else if (x < maxScrollX) {
            x = maxScrollX;
        }
        if (!scrollHeight || y > 0) {
            y = 0;
        } else if (y < maxScrollY) {
            y = maxScrollY;
        }
        return [x, y];
    }

    public destroy(): void {
        let s = this;
        //由于操作子级方法被重写了，销毁时移除不了，所以手动操作，用removeChildAt，其他都被重写了
        super.removeChildAt(0)
            .destroy();
        super.removeChildAt(0)
            .destroy();

        s.maskObj = null;
        s._content = null;
        s.easeFn = null;

        super.destroy();
    }
};
//将添加与移除的操作统统重写到_content中，待测试，需要维护,TODO。对于list直接不允许使用下面这些方法
Container._childrenOperationMethods.forEach((v) => {
    Object.defineProperty(ScrollView.prototype, v, {
        value: function (...arg) {
            console.warn("Maybe you want to operate children in its view rather then in it when it is a ScrollView!");
            return Container.prototype[v].call(this, ...arg)
            //先不重写了，不方便扩展，还容易出问题，只加提示吧
            // return this._content[v](...arg);
        },
        writable: true,
        enumerable: true,
    })
})
/**
 * 
 * @param current 当前位置
 * @param speed 速度。move最后位置到松开的位置/滑动到松开的时间
 * @param lowerMargin 
 * @param deceleration 
 * @returns 
 */
function toMomentum(
    current: number,
    speed: number,
    lowerMargin: number,
    deceleration: number = 0.0006
) {
    //衰减到0的时间
    let duration = Math.abs(speed) / deceleration;
    let destination = current + duration * (speed / 2)
    // console.log(current, speed)
    if (destination < lowerMargin) {
        destination = lowerMargin;
        duration = Math.abs((destination - current) / (speed / 2));
    }
    else if (destination > 0) {
        destination = 0;
        duration = Math.abs(current / (speed / 2));
    }
    return {
        destination,
        duration
    };
};

function toMomentum1(
    current: number,
    speed: number,
    lowerMargin: number,
    deceleration: number = 0.0006
) {
    let destination = current + (speed * speed) / (2 * deceleration) * (speed < 0 ? -1 : 1);
    let duration = Math.abs(speed) / deceleration;
    // console.log(current, speed)
    if (destination < lowerMargin) {
        destination = lowerMargin;
        duration = Math.abs((destination - current) / speed);
    }
    else if (destination > 0) {
        destination = 0;
        duration = Math.abs(current / speed);
    }
    return {
        destination,
        duration
    };
};

function toMomentum2(
    curPos: number,
    pixelsPerMS: number,
    maxPos: number,
    damping: number
): { destination: number; duration: number } {
    let absPixelsPerMS: number = Math.abs(pixelsPerMS);
    let extraFricition: number = 0.95;
    let duration: number = 0;
    let friction: number = 0.998;
    let minVelocity: number = 0.02;
    let posTo: number = curPos + pixelsPerMS * 500;

    if (posTo < maxPos) {
        posTo = maxPos;
        duration = Math.abs((posTo - curPos) / pixelsPerMS);
    }
    else if (posTo > 0) {
        posTo = 0;
        duration = Math.abs(curPos / pixelsPerMS);
    }
    else {
        duration = -Math.log(minVelocity / absPixelsPerMS) * 500;
    }

    let result = {
        destination: posTo,
        duration
    };
    return result;
}

export interface IScrollData {
    /**
     * 横向滚动位置，通常为正
     */
    x: number,
    /**
     * 纵向滚动位置，通常为正
     */
    y: number,
    /**
     * 滚动横向距离，为正表示向左滚动
     */
    deltaX?: number,
    /**
     * 滚动纵向距离，为正表示向上滚动
     */
    deltaY?: number
}

export interface ScrollEvent extends Event {
    data: IScrollData
}