<script setup lang="ts">
import { type WorkerRequest, type SolutionMessage, WorkerResponseType, WorkerMessageType, ChallengeStrategy, type Challenge } from '~/types/pow';
import WASMSolverUrl from "~/utils/solver.wasm?url";
import ChallengeWorker from '~/utils/worker?worker';

let shared_atomics = new SharedArrayBuffer(12);

let workers: Worker[] = [];
let workers_initialized = false;
if (import.meta.client) {
    try {
        // const num_workers = 1;
        const num_workers = navigator.hardwareConcurrency;

        for (let i = 0; i < num_workers; i++) {
            workers.push(new ChallengeWorker());
        }
    } catch (error: any) {
        console.error("Failed to create worker:", error);
    }
}

let autoSolve: Ref<boolean> = ref(false);
watch(autoSolve, async () => {
    if (autoSolve.value) {
        while (autoSolve.value) {
            if (solving.value) {
                await new Promise(resolve => setTimeout(resolve, 100));
                continue;
            }

            await getChallenge();
            await solveChallenge();
            if (!autoSolve.value) {
                break;
            }

            await new Promise(resolve => setTimeout(resolve, 100));
        }
    }
});

let total_solved: Ref<number> = ref(0);
let total_solving_for: Ref<number> = ref(0);

function pluralize(value: number, string: string) {
    return value === 1 ? string : `${string}s`;
}

let challenge_loading: Ref<boolean> = ref(false);
let challenge_loading_indicator: Ref<string> = ref('');
let challenge: Ref<Challenge | null> = ref(null);

let nonce: Ref<string | null> = ref(null);
let solved: Ref<boolean> = ref(false);
let solving_for: Ref<string> = ref('0');

const number_formatter = new Intl.NumberFormat('en-US', {
    notation: 'compact',
    compactDisplay: 'short',
    maximumFractionDigits: 2,
});

// the hashrate of all the runs
let hashrate_array: Ref<Array<number>> = ref([]);
let hashrate = computed(() => {
    if (hashrate_array.value.length === 0) {
        return 0;
    }

    return hashrate_array.value.reduce((a, b) => a + b, 0) / hashrate_array.value.length;
});

let solving = ref(false);
let solveTime: Ref<number> = ref(0);

let solveTimeout: Ref<any | null> = ref(null);

let challenge_error: Ref<string | null> = ref(null);

let difficulty: Ref<number> = ref(0);

let { data } = await useFetch('/api/pow/difficulty');

if (data.value) {
    difficulty.value = data.value.difficulty;
}

const MESSAGE_TIMEOUT = 3000;

async function getChallenge() {
    challenge_error.value = null;
    challenge_loading_indicator.value = '';
    challenge.value = null;
    nonce.value = null;
    challenge_loading.value = true;

    const spinners = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
    let spinner_index = 0;
    const loading_interval = setInterval(() => {
        challenge_loading_indicator.value = spinners[spinner_index]!;
        spinner_index = (spinner_index + 1) % spinners.length;
        console.log(spinners[spinner_index]);
    }, 100);

    try {
        const new_challenge = await $fetch('/api/pow/challenge');
        challenge.value = new_challenge.challenge;
    } catch (error: any) {
        console.error("Failed to get challenge:", error);
        challenge_error.value = `Failed to get challenge: ${error.message}`;
    } finally {
        challenge_loading.value = false;
        clearInterval(loading_interval);
    }
}

async function initWorkers() {
    if (workers_initialized) {
        throw createError("Workers already initialized");
    }

    workers_initialized = true;

    const module = await WebAssembly.compileStreaming(fetch(WASMSolverUrl));

    const atomics_view = new Int32Array(shared_atomics);
    Atomics.store(atomics_view, 0, 0);
    Atomics.store(atomics_view, 1, 0);

    console.debug(`Initializing ${workers.length} workers`);

    let worker_promises: Promise<void>[] = [];
    for (let i = 0; i < workers.length; i++) {
        const worker = workers[i]!;

        worker_promises.push(new Promise<void>((resolve, reject) => {
            const message_handler = (event: MessageEvent<SolutionMessage>) => {
                if (event.data.type === WorkerResponseType.Error) {
                    console.error("Worker error:", event.data.error);
                    reject(event.data.error);
                }

                if (event.data.type === WorkerResponseType.Ok) {
                    resolve();
                }

                reject(new Error("Unexpected message from worker"));
            };

            const error_handler = (error: ErrorEvent) => {
                console.error("Worker error:", error);
                reject(error);
            };

            worker.addEventListener('message', message_handler);
            worker.addEventListener('error', error_handler);

            worker.postMessage({
                type: WorkerMessageType.Init,
                module: module,
                sab: shared_atomics,
            } as WorkerRequest);
        }));
    }

    const timeoutMs = 10 * 1000;
    const timeoutPromise = new Promise((_, reject) => {
        setTimeout(() => {
            reject(new Error(`Function timed out after ${timeoutMs}ms`));
        }, timeoutMs);
    });

    await Promise.race([
        Promise.all(worker_promises),
        timeoutPromise,
    ]);

    console.log("All workers initialized");
}

async function getChallengeSolution(worker: Worker, request: { strategy: ChallengeStrategy.LeadingZeroes, target: string, difficulty: number } | { strategy: ChallengeStrategy.TargetNumber, target: string, salt: string }): Promise<SolutionMessage> {
    return new Promise<SolutionMessage>((resolve, reject) => {
        const message_handler = (event: MessageEvent<SolutionMessage>) => {
            worker.removeEventListener('message', message_handler);
            worker.removeEventListener('error', error_handler);

            resolve(event.data);
        };

        const error_handler = (error: ErrorEvent) => {
            worker.removeEventListener('error', error_handler);
            worker.removeEventListener('message', message_handler);
            console.error("Worker error:", error);

            reject(error);
        };

        worker.addEventListener('message', message_handler);
        worker.addEventListener('error', error_handler);

        switch (request.strategy) {
            case ChallengeStrategy.LeadingZeroes:
                worker.postMessage({
                    strategy: ChallengeStrategy.LeadingZeroes,
                    target: request.target,
                    difficulty: request.difficulty,
                } as WorkerRequest);
                break;
            case ChallengeStrategy.TargetNumber:
                worker.postMessage({
                    strategy: ChallengeStrategy.TargetNumber,
                    target: request.target,
                    salt: request.salt,
                } as WorkerRequest);
                break;
        }
    });
}

async function solveChallenge() {
    if (!challenge.value?.target) {
        return;
    }

    if (!workers_initialized) {
        try {
            await initWorkers();
        } catch (error: any) {
            console.error("Failed to initialize workers:", error);
            return;
        }
    }

    const atomics_view = new Int32Array(shared_atomics);
    Atomics.store(atomics_view, 0, 0);
    Atomics.store(atomics_view, 1, -1);

    solved.value = false;
    challenge_error.value = null;
    solving.value = true;
    solveTime.value = 0;
    solving_for.value = '0';

    let startTime = performance.now();

    let solving_for_interval = setInterval(() => {
        solving_for.value = ((performance.now() - startTime) / 1000).toFixed(1);
    }, 100);

    function cleanup() {
        clearTimeout(solveTimeout.value);
        solveTimeout.value = setTimeout(() => {
            solveTime.value = 0;
        }, MESSAGE_TIMEOUT);

        solved.value = false;
        solving.value = false;
        clearInterval(solving_for_interval);
    }

    try {

        let request: { strategy: ChallengeStrategy.LeadingZeroes, target: string, difficulty: number } | { strategy: ChallengeStrategy.TargetNumber, target: string, salt: string };

        switch (challenge.value.strategy) {
            case ChallengeStrategy.LeadingZeroes:
                request = {
                    strategy: ChallengeStrategy.LeadingZeroes,
                    target: challenge.value.target,
                    difficulty: challenge.value.difficulty,
                };
                break;
            case ChallengeStrategy.TargetNumber:
                request = {
                    strategy: ChallengeStrategy.TargetNumber,
                    target: challenge.value.target,
                    salt: challenge.value.salt,
                };
                break;
        }

        let worker_promises: Promise<SolutionMessage>[] = [];
        for (let worker of workers) {
            // dispatch to all workers, func is async so it will not block
            worker_promises.push(getChallengeSolution(worker, request));
        }

        let solution = await Promise.race(worker_promises);

        if (solution.type === WorkerResponseType.Error) {
            throw createError(solution.error);
        }

        if (solution.type === WorkerResponseType.Ok) {
            throw createError("spurious solution");
        }

        console.log(shared_atomics.slice(8, 12));

        nonce.value = Atomics.load(atomics_view, 1).toString();

        solveTime.value = Math.floor(performance.now() - startTime);
        total_solved.value += 1;
        total_solving_for.value += solveTime.value;
        // since nonce is the number of iterations we have completed, we can divide that by how long it took in second
        // to get H/s
        hashrate_array.value.push(+Atomics.load(atomics_view, 1) / (solveTime.value / 1000));
        clearTimeout(solveTimeout.value);

        await $fetch('/api/pow/challenge', {
            method: 'POST',
            body: {
                challenge: challenge.value.target,
                nonce: nonce.value,
            }
        });

        solved.value = true;

        solveTimeout.value = setTimeout(() => {
            solveTime.value = 0;
            solved.value = false;
        }, MESSAGE_TIMEOUT);

        switch (challenge.value.strategy) {
            case ChallengeStrategy.LeadingZeroes:
                console.debug("Solved challenge with difficulty", challenge.value.difficulty, "in " + solveTime.value + "ms");
                break;
            case ChallengeStrategy.TargetNumber:
                console.debug("Solved challenge with salt", challenge.value.salt, "in " + solveTime.value + "ms");
                break;
        }

        solving.value = false;
        clearInterval(solving_for_interval);
    } catch (error: any) {
        challenge_error.value = `Failed to solve challenge: ${error.message}`;
        console.error(error);
        cleanup();
    }
}

async function setDifficulty(difficulty: number) {
    const response = await $fetch('/api/pow/difficulty', {
        method: 'PUT',
        body: {
            difficulty,
        }
    });

    console.log(response);
}

</script>

<template>
    <div class="flex justify-between" v-if="hashrate_array.length !== 0">
        <span>Your average Hashrate: {{ number_formatter.format(hashrate) }} H/s</span>
        <span>You have solved {{ total_solved }} {{ pluralize(total_solved, "challenge") }} in
            {{ number_formatter.format(total_solving_for / 1000) }}s</span>
        <span>Your Hashrate on the last challenge: {{ number_formatter.format(hashrate_array.at(-1)!) }} H/s</span>
    </div>
    <p v-else>You have not solved any challenges yet</p>
    <p>Challenge: <span v-if="challenge_loading">{{ challenge_loading_indicator }}</span>
        <span v-else>{{ challenge }}</span>
    </p>
    <p>Nonce: {{ nonce }}</p>
    <button @click="getChallenge()" :disabled="solving || challenge_loading">
        Get Challenge
    </button>
    <button @click="solveChallenge()" :disabled="solving || challenge === null || nonce !== null">
        <span v-if="!solving">Solve Challenge</span>
        <span v-else>Solving challenge for {{ solving_for }}s...</span>
    </button>
    <div>
        <div v-if="solveTime && !challenge_error" class="min-h-[1rem]">
            <span v-if="solved">Challenge solved in {{ solveTime }}ms!</span>
            <span v-else>Validating solution...</span>
        </div>
        <div v-else-if="challenge_error">{{ challenge_error }}</div>
        <p v-else class="empty-p"><!-- Empty so there is no content shift when there is text or isnt --></p>
    </div>

    <div class="flex flex-row w-fit">
        <label for="autoSolve">Auto solve</label>
        <input class="w-min" type="checkbox" v-model="autoSolve" id="autoSolve"></input>
    </div>

    <div class="flex flex-col w-fit">
        <label for="difficulty">Difficulty</label>
        <input class="w-min" type="number" min="1" max="64" v-model="difficulty" @change="setDifficulty(difficulty)"
            id="difficulty"></input>
    </div>
</template>

<style scoped>
button {
    min-width: 140px;
    padding: 0.25rem 0.375rem;
}

.empty-p {
    margin: 0;

    &::after {
        content: "-";
        visibility: hidden;
    }
}
</style>