<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import DesignStage from '../../../components/DesignStage.vue'
import type Player from '../../../commons/player.ts'
import { $is_run_local } from '@/commons/utils.ts';
import { cssAssetUrl } from '@/commons/assets.ts';

const props = defineProps<{
    rankList: Player[];
}>();

const emit = defineEmits<{
    start: [];
}>();

const HORSE_COUNT = 6;
const HORSE_FRAME_COUNT = 91;
const HORSE_FRAME_SIZE = 150;
const HORSE_SPRITE_COLUMNS = 10;
const HORSE_FRAME_DURATION = 50;
const RACE_TICK_DURATION = 50;
const RACE_DURATION = 60_000;
const RANK_RUNNING_GAP = 45;
const RANK_SPEED_FACTORS = [1.04, 1.025, 1.01, 0.995, 0.98, 0.965];
const FINISH_RANK_OFFSETS = [0, 72, 94, 181, 207, 302];
const TIE_POSITION_OFFSETS = [48, -76, 96, -28, 67, -108];
const RACE_START_X = -225;
const TRACK_MAX_X = 1595;
const BACKGROUND_WIDTH = 2358;
const STAGE_WIDTH = 1920;
const designStage = ref<InstanceType<typeof DesignStage> | null>(null);
const horseFrameIndex = ref(0);
const raceElapsed = ref(0);
const raceStarted = ref(false);
const rankedPlayerKeys = ref<string[]>([]);
const testRankList = ref<Player[]>([]);
let raceAnimationFrame: number | undefined;
let raceStartTimer: ReturnType<typeof setTimeout> | undefined;
let previousAnimationTime = 0;
let horseFrameElapsed = 0;

type Racer = {
    key: string;
    lane: number;
    horseType: number;
    player: Player;
    x: number;
    speed: number;
    paceAmplitude: number;
    paceFrequency: number;
    pacePhase: number;
    tieOffset: number;
};
const START_COUNTDOWN_SECONDS = 3;
const racers = ref<Racer[]>([]);

const imageUrls = {
    bg: cssAssetUrl('game2/bg2.png'),
    logo: cssAssetUrl('game1/logo.png'),
    title: cssAssetUrl('game2/title.png'),
    fireworks: cssAssetUrl('game1/fireworks.png'),

    // fireworks: cssAssetUrl('game1/fireworks.png'),
    // titleRank: cssAssetUrl('game1/title-rank.png'),
    rankNo4: cssAssetUrl('game6/no4.png'),
    rankNo1: cssAssetUrl('game6/no1.png'),
    rankNo2: cssAssetUrl('game6/no2.png'),
    rankNo3: cssAssetUrl('game6/no3.png'),
    // rankIcon: cssAssetUrl('game1/no-icon.png'),
    // rankNo1Icon: cssAssetUrl('game1/no1-icon.png'),
    // rankNo2Icon: cssAssetUrl('game1/no2-icon.png'),
    // rankNo3Icon: cssAssetUrl('game1/no3-icon.png'),
    avatar: cssAssetUrl('avatar-yellow.png'),
    avatarAnimation: cssAssetUrl('avatar-yellow_animation.png'),
    mole2: cssAssetUrl('game6/mole2.png'),
    rankContainer: cssAssetUrl('game6/rank-container.png'),
    land: cssAssetUrl('game6/land.png'),
    moleShow: cssAssetUrl('game6/mole_show.png'),
    moleHide: cssAssetUrl('game6/mole_hide.png'),
    rabbitShow: cssAssetUrl('game6/rabbit_show.png'),
    rabbitHide: cssAssetUrl('game6/rabbit_hide.png'),
};

const horseSprites = [
    cssAssetUrl('game2/horse1.png'),
    cssAssetUrl('game2/horse2.png'),
    cssAssetUrl('game2/horse3.png'),
];

const preloadHorseSprites = () => {
    horseSprites.forEach((sprite) => {
        const image = new Image();
        image.src = sprite.slice(5, -2);
    });
};

const playerKey = (player: Player, index = 0) => (
    String(player.userid || player.wechat_id || player.telephone || `${player.nickname}-${index}`)
);

const hashKey = (key: string) => Array.from(key).reduce(
    (hash, character) => ((hash << 5) - hash + character.charCodeAt(0)) | 0,
    0,
);

const createPaceProfile = (key: string) => {
    const hash = Math.abs(hashKey(key));

    return {
        paceAmplitude: 14 + hash % 18,
        paceFrequency: 1.5 + (hash % 5) * 0.2,
        pacePhase: (hash % 360) * Math.PI / 180,
        tieOffset: TIE_POSITION_OFFSETS[hash % TIE_POSITION_OFFSETS.length]!,
    };
};

const isRealPlayer = (player: Player) => (
    Boolean(player)
    && !String(player.wechat_id || '').startsWith('empty-')
    && player.nickname !== '-'
);

const scoreOf = (player: Player) => Number(player.score || 0);

const renderRaceBackground = () => {
    const progress = Math.min(raceElapsed.value / RACE_DURATION, 1);
    const backgroundX = -(BACKGROUND_WIDTH - STAGE_WIDTH) * progress;

    return {
        backgroundImage: imageUrls.bg,
        backgroundPosition: `${backgroundX}px center`,
    };
};

const targetXForRacer = (racer: Racer, rankIndex: number) => {
    const safeRankIndex = Math.max(0, Math.min(rankIndex, HORSE_COUNT - 1));
    const progress = Math.min(raceElapsed.value / RACE_DURATION, 1);
    const runningGap = RANK_RUNNING_GAP * Math.sin(Math.PI * progress);
    const finishOffset = FINISH_RANK_OFFSETS[safeRankIndex]! * progress;
    const paceFade = Math.sin(Math.PI * progress);
    const paceOffset = Math.sin(
        racer.pacePhase + progress * Math.PI * 2 * racer.paceFrequency,
    ) * racer.paceAmplitude * paceFade;
    const tiedCount = racers.value.filter(
        ({ player }) => scoreOf(player) === scoreOf(racer.player),
    ).length;
    const tieOffset = tiedCount > 1 ? racer.tieOffset * progress : 0;

    return RACE_START_X
        + (TRACK_MAX_X - RACE_START_X) * progress
        - safeRankIndex * runningGap
        - finishOffset
        + paceOffset
        + tieOffset;
};

const createRandomLanesByRank = () => {
    // 每项表示从顶部到底部的名次索引，均为明显的前后交替排列。
    const ranksByLaneTemplates = [
        [5, 0, 4, 1, 3, 2],
        [4, 1, 5, 0, 3, 2],
        [3, 0, 5, 1, 4, 2],
        [5, 1, 3, 0, 4, 2],
        [4, 0, 3, 1, 5, 2],
        [2, 5, 0, 4, 1, 3],
    ];
    const ranksByLane = ranksByLaneTemplates[
        Math.floor(Math.random() * ranksByLaneTemplates.length)
    ]!;

    return Array.from(
        { length: HORSE_COUNT },
        (_, rankIndex) => ranksByLane.indexOf(rankIndex),
    );
};

let lanesByRank = createRandomLanesByRank();

const syncRacers = (rankList: Player[]) => {
    const topPlayers = rankList.filter(isRealPlayer).slice(0, HORSE_COUNT);
    const rankedEntries = topPlayers.map((player, index) => ({
        key: playerKey(player, index),
        player,
    }));
    const topKeys = rankedEntries.map(({ key }) => key);
    const topPlayerMap = new Map(
        rankedEntries.map(({ key, player }) => [key, player]),
    );
    const previousRacersByLane = new Map(
        racers.value.map((racer) => [racer.lane, racer]),
    );

    const retained = racers.value
        .filter((racer) => topPlayerMap.has(racer.key))
        .map((racer) => ({ ...racer, player: topPlayerMap.get(racer.key)! }));
    const retainedKeys = new Set(retained.map((racer) => racer.key));
    const availableLanes = Array.from(
        { length: HORSE_COUNT },
        (_, lane) => lane,
    ).filter((lane) => !retained.some((racer) => racer.lane === lane));
    const freeLanes = racers.value.length === 0 ? lanesByRank : availableLanes;
    const newcomers = rankedEntries
        .filter(({ key }) => !retainedKeys.has(key))
        .map(({ key, player }, index) => {
            const previousRacer = previousRacersByLane.get(freeLanes[index]!);

            return {
                key,
                player,
                lane: freeLanes[index]!,
                horseType: previousRacer?.horseType ?? freeLanes[index]! % horseSprites.length,
                x: previousRacer?.x ?? RACE_START_X,
                speed: previousRacer?.speed ?? 0,
                ...createPaceProfile(key),
            };
        });

    racers.value = [...retained, ...newcomers]
        .sort((a, b) => a.lane - b.lane);
    rankedPlayerKeys.value = topKeys;
};

const updateRacePositions = (tickScale = 1) => {
    if (raceElapsed.value >= RACE_DURATION) {
        racers.value.forEach((racer) => {
            racer.speed = 0;
        });
        return;
    }

    racers.value.forEach((racer) => {
        const rankIndex = rankedPlayerKeys.value.indexOf(racer.key);
        const racerScore = scoreOf(racer.player);
        const sameScoreRank = rankedPlayerKeys.value.findIndex((key) => {
            const rankedRacer = racers.value.find((item) => item.key === key);
            return rankedRacer && scoreOf(rankedRacer.player) === racerScore;
        });
        const effectiveRank = sameScoreRank >= 0
            ? sameScoreRank
            : rankIndex >= 0 ? rankIndex : HORSE_COUNT - 1;
        const targetX = targetXForRacer(racer, effectiveRank);
        const speedFactor = RANK_SPEED_FACTORS[effectiveRank] ?? RANK_SPEED_FACTORS.at(-1)!;
        const desiredSpeed = Math.max((targetX - racer.x) * 0.18 * speedFactor, 0);
        const speedBlend = 1 - Math.pow(1 - 0.24, tickScale);

        racer.speed += (desiredSpeed - racer.speed) * speedBlend;
        racer.x = Math.min(racer.x + Math.max(racer.speed, 0) * tickScale, TRACK_MAX_X);
    });
};

const animateRace = (time: number) => {
    if (!previousAnimationTime) {
        previousAnimationTime = time;
    }

    const delta = Math.min(time - previousAnimationTime, 100);
    previousAnimationTime = time;
    horseFrameElapsed += delta;

    if (horseFrameElapsed >= HORSE_FRAME_DURATION) {
        const elapsedFrames = Math.floor(horseFrameElapsed / HORSE_FRAME_DURATION);
        horseFrameIndex.value = (horseFrameIndex.value + elapsedFrames) % HORSE_FRAME_COUNT;
        horseFrameElapsed %= HORSE_FRAME_DURATION;
    }

    if (raceStarted.value) {
        raceElapsed.value = Math.min(raceElapsed.value + delta, RACE_DURATION);
        updateRacePositions(delta / RACE_TICK_DURATION);
    }

    raceAnimationFrame = window.requestAnimationFrame(animateRace);
};

const resetRace = (startImmediately = true) => {
    raceElapsed.value = 0;
    raceStarted.value = startImmediately;
    lanesByRank = createRandomLanesByRank();

    racers.value.forEach((racer) => {
        const rankIndex = rankedPlayerKeys.value.indexOf(racer.key);
        racer.lane = lanesByRank[Math.max(rankIndex, 0)]!;
        racer.x = RACE_START_X;
        racer.speed = 0;
    });
    racers.value.sort((a, b) => a.lane - b.lane);
};

const renderHorseFrame = (racer: Racer) => {
    const column = horseFrameIndex.value % HORSE_SPRITE_COLUMNS;
    const row = Math.floor(horseFrameIndex.value / HORSE_SPRITE_COLUMNS);

    return {
        backgroundImage: horseSprites[racer.horseType],
        backgroundPosition: `${-column * HORSE_FRAME_SIZE}px ${-row * HORSE_FRAME_SIZE}px`,
        transform: `translate3d(${racer.x}px, 0, 0) scale(1.5)`,
    };
};

const startGame = () => {
    if ($is_run_local()) {
        emit('start');
    }
};

const createTestPlayers = () => Array.from({ length: 7 }, (_, index): Player => ({
    userid: `test-player-${index + 1}`,
    wechat_id: `test-player-${index + 1}`,
    nickname: `测试玩家${index + 1}`,
    avatar: '',
    score: 700 - index * 100,
}));

const updateTestRank = () => {
    if (!$is_run_local()) return;

    if (testRankList.value.length === 0) {
        testRankList.value = createTestPlayers();
        syncRacers(testRankList.value);
        resetRace();
        return;
    }

    const player = testRankList.value[Math.floor(Math.random() * testRankList.value.length)];
    if (player) {
        player.score = Number(player.score || 0) + 80 + Math.floor(Math.random() * 180);
    }

    testRankList.value = [...testRankList.value].sort(
        (a, b) => Number(b.score || 0) - Number(a.score || 0),
    );
    syncRacers(testRankList.value);
};

onMounted(() => {
    preloadHorseSprites();
    raceAnimationFrame = window.requestAnimationFrame(animateRace);
});

onBeforeUnmount(() => {
    if (raceAnimationFrame !== undefined) {
        window.cancelAnimationFrame(raceAnimationFrame);
    }
    if (raceStartTimer) {
        clearTimeout(raceStartTimer);
    }
});

const startIntro = async () => {
    resetRace(false);
    const countdown = designStage.value?.startCountdown(START_COUNTDOWN_SECONDS, 60);

    raceStartTimer = setTimeout(() => {
        raceStarted.value = true;
        raceStartTimer = undefined;
    }, START_COUNTDOWN_SECONDS * 1000);

    await countdown;
};

defineExpose({
    startIntro,
});

const renderAvatar = (item: any | null) => {
    if (item && item.avatar) {
        return `background: url("${String(item.avatar).replace(/"/g, '\\"')}") center / cover no-repeat;`
    }
    return `background: ${imageUrls.avatar} center / cover no-repeat;`
}

watch(
    () => props.rankList,
    (rankList) => syncRacers(rankList),
    { immediate: true, deep: true },
);

</script>

<template>
    <DesignStage ref="designStage">
        <div class="bg" :style="renderRaceBackground()">
            <div v-if="$is_run_local()" class="test-click-layer"></div>
            <div class="img-logo"></div>
            <div class="img-title"></div>

            <div class="horse-container">
                <div v-for="racer in racers" :key="racer.lane" class="horse-lane">
                    <div class="horse" :style="renderHorseFrame(racer)">
                        <div class="name-container">
                            <div class="nickname">{{ racer.player.nickname }}</div>
                            <div class="img-avatar" :style="renderAvatar(racer.player)"></div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </DesignStage>
</template>

<style scoped>
.bg {
    width: 1920px;
    height: 1080px;
    background-repeat: no-repeat;
    background-size: 2358px 1080px;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    position: relative;
}

.test-click-layer {
    position: absolute;
    inset: 0;
    z-index: 1;
}

.img-logo {
    background: v-bind('imageUrls.logo');
    background-size: cover;
    width: 428px;
    height: 77px;
    position: absolute;
    left: 37px;
    top: 82px;
    z-index: 2;
}

.img-title {
    background: v-bind('imageUrls.title') center / cover no-repeat;
    width: 831px;
    height: 325px;
    margin: 0 auto;
    margin-top: 37px;
    z-index: 2;
}

.horse-container {
    position: absolute;
    bottom: 0px;
    left: 0;
    display: flex;
    flex-direction: column;
    width: 1920px;
    height: 500px;
    z-index: 2;
    pointer-events: none;
}

.horse-lane {
    position: relative;
    flex: 1;
    min-height: 0;
}

.horse {
    position: absolute;
    left: 0;
    bottom: 0;
    width: 150px;
    height: 150px;
    background-repeat: no-repeat;
    background-size: 1500px 1500px;
    transform-origin: left bottom;
    backface-visibility: hidden;
    will-change: transform, background-position;
    transform-style: preserve-3d;

    .name-container {
        display: flex;
        align-items: center;
        gap: 8px;
        width: 150px;
        height: 42px;
        position: absolute;
        left: -70px;
        top: 35px;

        .nickname {
            max-width: 150px;
            padding: 4px 8px;
            color: #fff;
            background: rgb(0 0 0 / 55%);
            border-radius: 12px;
            text-overflow: ellipsis;
            white-space: nowrap;
            font-size: 20px;
        }

        .img-avatar {
            flex: 0 0 42px;
            width: 42px;
            height: 42px;
            border: 2px solid #fff;
            border-radius: 50%;
        }
    }
}
</style>
