import { RenderAdapter } from '@motajs/render-core';
import { HeroRenderer } from './hero';
import { ILayerGroupRenderExtends, LayerGroup } from './layer';
import { LayerGroupFloorBinder } from './layer';
import { hyper, TimingFn } from 'mutate-animate';
import {
    MAP_BLOCK_HEIGHT,
    MAP_BLOCK_WIDTH,
    MAP_HEIGHT,
    MAP_WIDTH
} from '../shared';

export class FloorViewport implements ILayerGroupRenderExtends {
    id: string = 'viewport';

    group!: LayerGroup;
    hero?: HeroRenderer;
    binder?: LayerGroupFloorBinder;

    /** 是否启用视角控制拓展 */
    enabled: boolean = true;
    /** 是否自动限定视角范围至地图范围 */
    boundX: boolean = true;
    boundY: boolean = true;

    /** 渐变的速率曲线 */
    transitionFn: TimingFn = hyper('sin', 'out');
    /** 瞬移的速率曲线 */
    mutateFn: TimingFn = hyper('sin', 'out');

    /** 突变时的渐变时长 */
    transitionTime: number = 600;

    /** 当前视角位置 */
    private nx: number = 0;
    private ny: number = 0;
    /** 移动时的偏移位置 */
    private ox: number = 0;
    private oy: number = 0;
    /** 移动时的偏移最大值 */
    private maxOffset: number = 1;

    /** 委托ticker */
    private delegation: number = -1;
    /** 渐变委托ticker */
    private transition: number = -1;
    /** 移动委托ticker */
    private moving: number = -1;
    /** 是否在渐变过程中 */
    private inTransition: boolean = false;
    /** 是否在移动过程中 */
    private inMoving: boolean = false;

    /** 移动的监听函数 */
    private movingFramer?: () => void;

    /**
     * 禁用自动视角控制
     */
    disable() {
        this.enabled = false;
    }

    /**
     * 启用自动视角控制
     */
    enable() {
        this.enabled = true;
        const { x, y } = core.status.hero.loc;
        const { x: nx, y: ny } = this.group.camera;
        const halfWidth = MAP_WIDTH / 2;
        const halfHeight = MAP_HEIGHT / 2;
        const cell = this.group.cellSize;
        const half = cell / 2;
        this.applyPosition(
            -(nx - halfWidth + half) / this.group.cellSize,
            -(ny - halfHeight + half) / this.group.cellSize
        );
        this.mutateTo(x, y);
    }

    /**
     * 设置是否自动限定视角范围至地图范围
     * @param boundX 是否自动限定水平视角范围
     * @param boundY 是否自动限定竖直视角范围
     */
    setAutoBound(boundX: boolean = this.boundX, boundY: boolean = this.boundY) {
        this.boundX = boundX;
        this.boundY = boundY;
        this.group.requestBeforeFrame(() => {
            this.setPosition(this.nx, this.ny);
        });
    }

    /**
     * 传入视角的目标位置，将其限定在地图范围内后返回
     * @param x 图格横坐标
     * @param y 图格纵坐标
     */
    getBoundedPosition(x: number, y: number) {
        if (!this.checkDependency()) return { x, y };
        if (!this.boundX && !this.boundY) return { x, y };
        const width = MAP_BLOCK_WIDTH;
        const height = MAP_BLOCK_HEIGHT;
        const minX = (width - 1) / 2;
        const minY = (height - 1) / 2;
        const floor = core.status.maps[this.binder!.getFloor()];
        const maxX = floor.width - minX - 1;
        const maxY = floor.height - minY - 1;

        return {
            x: this.boundX ? core.clamp(x, minX, maxX) : x,
            y: this.boundY ? core.clamp(y, minY, maxY) : y
        };
    }

    /**
     * 设置视角位置
     * @param x 目标图格横坐标
     * @param y 目标图格纵坐标
     */
    setPosition(x: number, y: number) {
        if (!this.enabled) return;
        const { x: nx, y: ny } = this.getBoundedPosition(x, y);
        this.group.removeTicker(this.transition, false);
        this.applyPosition(nx, ny);
    }

    /**
     * 开始移动
     */
    startMove() {
        if (this.inMoving) return;
        this.inMoving = true;
        this.createMoveTransition();
    }

    /**
     * 结束移动
     */
    endMove() {
        this.inMoving = false;
    }

    /**
     * 当勇士通过移动改变至指定位置时移动视角
     * @param x 目标图格横坐标
     * @param y 目标图格纵坐标
     */
    moveTo(x: number, y: number, time: number = 200) {
        if (!this.enabled) return;
        const { x: nx, y: ny } = this.getBoundedPosition(x, y);
        if (this.inTransition) {
            const distance = Math.hypot(this.nx - nx, this.ny - ny);
            const t = core.clamp(distance * time, time, time * 3);
            this.createTransition(nx, ny, t, this.transitionFn);
        }
    }

    private createMoveTransition() {
        if (!this.checkDependency()) return;
        let xTarget: number = 0;
        let yTarget: number = 0;
        let xStart: number = this.ox;
        let yStart: number = this.oy;
        let xStartTime: number = Date.now();
        let yStartTime: number = Date.now();
        let ending: boolean = false;
        // 这个数等于 sinh(2)，用这个数的话，可以正好在刚开始移动的时候达到1的斜率，效果会比较好
        const transitionTime = this.hero!.speed * 3.626860407847019;

        const setTargetX = (x: number, time: number) => {
            if (x === xTarget) return;
            xTarget = x;
            xStartTime = time;
            xStart = this.ox;
        };
        const setTargetY = (y: number, time: number) => {
            if (y === yTarget) return;
            yTarget = y;
            yStart = this.oy;
            yStartTime = time;
        };

        if (this.movingFramer) {
            this.hero!.off('moveTick', this.movingFramer);
        }
        this.movingFramer = () => {
            if (this.inTransition) return;
            const now = Date.now();
            if (!this.inMoving && !ending) {
                setTargetX(0, now);
                setTargetY(0, now);
                ending = true;
            }
            if (!ending) {
                const dir = this.hero!.stepDir;
                const { x, y } = core.utils.scan2[dir];
                setTargetX(-x * this.maxOffset, now);
                setTargetY(-y * this.maxOffset, now);
            }

            if (!this.hero!.renderable) return;

            const { x, y } = this.hero!.renderable;
            const { x: nx, y: ny } = this.getBoundedPosition(x, y);
            this.applyPosition(nx, ny);

            if (ending) {
                if (this.ox === xTarget && this.oy === yTarget) {
                    this.hero!.off('moveTick', this.movingFramer);
                    return;
                }
            }
            // todo: 效果太差了，需要优化
            return;
            if (this.ox !== xTarget) {
                const time = transitionTime * Math.abs(xStart - xTarget);
                const progress = (now - xStartTime) / time;
                if (progress > 1) {
                    this.ox = xTarget;
                } else {
                    const p = this.transitionFn(progress);
                    this.ox = (xTarget - xStart) * p + xStart;
                }
            }
            if (this.oy !== yTarget) {
                const time = transitionTime * Math.abs(yStart - yTarget);
                const progress = (now - yStartTime) / time;
                if (progress > 1) {
                    this.oy = yTarget;
                } else {
                    const p = this.transitionFn(progress);
                    this.oy = (yTarget - yStart) * p + yStart;
                }
            }
        };
        this.hero!.on('moveTick', this.movingFramer);
    }

    /**
     * 当勇士位置突变至指定位置时移动视角
     * @param x 目标图格横坐标
     * @param y 目标图格纵坐标
     */
    mutateTo(x: number, y: number, time: number = this.transitionTime) {
        if (!this.enabled) return;
        const { x: nx, y: ny } = this.getBoundedPosition(x, y);
        this.createTransition(nx, ny, time, this.mutateFn);
    }

    private createTransition(x: number, y: number, time: number, fn: TimingFn) {
        const start = Date.now();
        const end = start + time;
        const sx = this.nx;
        const sy = this.ny;
        const dx = x - sx;
        const dy = y - sy;

        this.inTransition = true;
        this.group.removeTicker(this.transition, false);
        this.transition = this.group.delegateTicker(
            () => {
                const now = Date.now();
                if (now >= end) {
                    this.group.removeTicker(this.transition, true);
                    return;
                }
                const progress = fn((now - start) / time);
                const tx = dx * progress;
                const ty = dy * progress;
                this.applyPosition(tx + sx, ty + sy);
            },
            time,
            () => {
                this.applyPosition(x, y);
                this.inTransition = false;
            }
        );
    }

    private applyPosition(x: number, y: number) {
        if (!this.enabled) return;
        if (x === this.nx && y === this.ny) return;
        const halfWidth = MAP_WIDTH / 2;
        const halfHeight = MAP_HEIGHT / 2;
        const cell = this.group.cellSize;
        const half = cell / 2;
        this.nx = x;
        this.ny = y;
        const { x: bx, y: by } = this.getBoundedPosition(x, y);
        const rx = bx * cell - halfWidth + half;
        const ry = by * cell - halfHeight + half;
        core.bigmap.offsetX = rx;
        core.bigmap.offsetY = ry;
        this.group.camera.setTranslate(-rx, -ry);
        this.group.update(this.group);
    }

    private checkDependency() {
        if (this.hero && this.binder) return true;
        const group = this.group;
        const ex1 = group.getLayer('event')?.getExtends('floor-hero');
        const ex2 = group.getExtends('floor-binder');
        if (
            ex1 instanceof HeroRenderer &&
            ex2 instanceof LayerGroupFloorBinder
        ) {
            this.hero = ex1;
            this.binder = ex2;
            return true;
        }
        return false;
    }

    awake(group: LayerGroup): void {
        this.group = group;
        adapter.add(this);
    }

    onDestroy(group: LayerGroup): void {
        group.removeTicker(this.delegation);
        group.removeTicker(this.transition);
        group.removeTicker(this.moving);
        adapter.remove(this);
    }
}

const adapter = new RenderAdapter<FloorViewport>('viewport');
adapter.receive('mutateTo', (item, x, y, time) => {
    item.mutateTo(x, y, time);
    return Promise.resolve();
});
adapter.receive('moveTo', (item, x, y, time) => {
    item.moveTo(x, y, time);
    return Promise.resolve();
});
adapter.receive('setPosition', (item, x, y) => {
    item.setPosition(x, y);
    return Promise.resolve();
});
adapter.receiveSync('disable', item => {
    item.disable();
});
adapter.receiveSync('enable', item => {
    item.enable();
});
adapter.receiveSync('startMove', item => {
    item.startMove();
});
adapter.receiveSync('endMove', item => {
    item.endMove();
});

export function createViewport() {
    const { hook } = Mota.require('@user/data-base');
    hook.on('changingFloor', (_, loc) => {
        adapter.all('setPosition', loc.x, loc.y);
    });
}
