<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
    import {
        ComputeRetentionProgress,
        type ComputeParamsProgress,
    } from "@generated/anki/collection_pb";
    import {
        ComputeOptimalRetentionRequest,
        SimulateFsrsReviewRequest,
        type SimulateFsrsReviewResponse,
    } from "@generated/anki/scheduler_pb";
    import {
        computeFsrsParams,
        computeOptimalRetention,
        simulateFsrsReview,
        evaluateParams,
        setWantsAbort,
    } from "@generated/backend";
    import * as tr from "@generated/ftl";
    import { runWithBackendProgress } from "@tslib/progress";

    import SettingTitle from "$lib/components/SettingTitle.svelte";
    import SwitchRow from "$lib/components/SwitchRow.svelte";

    import GlobalLabel from "./GlobalLabel.svelte";
    import { commitEditing, fsrsParams, type DeckOptionsState } from "./lib";
    import SpinBoxFloatRow from "./SpinBoxFloatRow.svelte";
    import SpinBoxRow from "./SpinBoxRow.svelte";
    import Warning from "./Warning.svelte";
    import ParamsInputRow from "./ParamsInputRow.svelte";
    import ParamsSearchRow from "./ParamsSearchRow.svelte";
    import {
        renderSimulationChart,
        SimulateSubgraph,
        type Point,
    } from "../graphs/simulator";
    import Graph from "../graphs/Graph.svelte";
    import HoverColumns from "../graphs/HoverColumns.svelte";
    import CumulativeOverlay from "../graphs/CumulativeOverlay.svelte";
    import AxisTicks from "../graphs/AxisTicks.svelte";
    import NoDataOverlay from "../graphs/NoDataOverlay.svelte";
    import TableData from "../graphs/TableData.svelte";
    import { defaultGraphBounds, type TableDatum } from "../graphs/graph-helpers";
    import InputBox from "../graphs/InputBox.svelte";
    import { UpdateDeckConfigsMode } from "@generated/anki/deck_config_pb";

    export let state: DeckOptionsState;
    export let openHelpModal: (String) => void;

    const presetName = state.currentPresetName;

    const config = state.currentConfig;
    const defaults = state.defaults;
    const fsrsReschedule = state.fsrsReschedule;
    const daysSinceLastOptimization = state.daysSinceLastOptimization;

    $: lastOptimizationWarning =
        $daysSinceLastOptimization > 30 ? tr.deckConfigTimeToOptimize() : "";

    let computeParamsProgress: ComputeParamsProgress | undefined;
    let computingParams = false;
    let checkingParams = false;
    let computingRetention = false;
    let simulating = false;
    let optimalRetention = 0;
    $: if ($presetName) {
        optimalRetention = 0;
    }
    $: computing =
        computingParams || checkingParams || computingRetention || simulating;
    $: defaultparamSearch = `preset:"${state.getCurrentNameForSearch()}" -is:suspended`;
    $: roundedRetention = Number($config.desiredRetention.toFixed(2));
    $: desiredRetentionWarning = getRetentionWarning(roundedRetention);
    $: retentionWarningClass = getRetentionWarningClass(roundedRetention);

    let computeRetentionProgress:
        | ComputeParamsProgress
        | ComputeRetentionProgress
        | undefined;

    let simulateSubgraph: SimulateSubgraph = SimulateSubgraph.count;

    const optimalRetentionRequest = new ComputeOptimalRetentionRequest({
        daysToSimulate: 365,
        lossAversion: 2.5,
    });
    $: if (optimalRetentionRequest.daysToSimulate > 3650) {
        optimalRetentionRequest.daysToSimulate = 3650;
    }

    $: newCardsIgnoreReviewLimit = state.newCardsIgnoreReviewLimit;

    const simulateFsrsRequest = new SimulateFsrsReviewRequest({
        params: fsrsParams($config),
        desiredRetention: $config.desiredRetention,
        deckSize: 0,
        daysToSimulate: 365,
        newLimit: $config.newPerDay,
        reviewLimit: $config.reviewsPerDay,
        maxInterval: $config.maximumReviewInterval,
        search: `preset:"${state.getCurrentNameForSearch()}" -is:suspended`,
        newCardsIgnoreReviewLimit: $newCardsIgnoreReviewLimit,
    });

    function getRetentionWarning(retention: number): string {
        const decay = -0.5;
        const factor = 0.9 ** (1 / decay) - 1;
        const stability = 100;
        const days = Math.round(
            (stability / factor) * (Math.pow(retention, 1 / decay) - 1),
        );
        if (days === 100) {
            return "";
        }
        return tr.deckConfigA100DayInterval({ days });
    }

    function getRetentionWarningClass(retention: number): string {
        if (retention < 0.7 || retention > 0.97) {
            return "alert-danger";
        } else if (retention < 0.8 || retention > 0.95) {
            return "alert-warning";
        } else {
            return "alert-info";
        }
    }

    function getIgnoreRevlogsBeforeMs() {
        return BigInt(
            $config.ignoreRevlogsBeforeDate
                ? new Date($config.ignoreRevlogsBeforeDate).getTime()
                : 0,
        );
    }

    async function computeParams(): Promise<void> {
        if (computingParams) {
            await setWantsAbort({});
            return;
        }
        if (state.presetAssignmentsChanged()) {
            alert(tr.deckConfigPleaseSaveYourChangesFirst());
            return;
        }
        computingParams = true;
        computeParamsProgress = undefined;
        try {
            await runWithBackendProgress(
                async () => {
                    const params = fsrsParams($config);
                    const RelearningSteps = $config.relearnSteps;
                    let numOfRelearningStepsInDay = 0;
                    let accumulatedTime = 0;
                    for (let i = 0; i < RelearningSteps.length; i++) {
                        accumulatedTime += RelearningSteps[i];
                        if (accumulatedTime >= 1440) {
                            break;
                        }
                        numOfRelearningStepsInDay++;
                    }
                    const resp = await computeFsrsParams({
                        search: $config.paramSearch
                            ? $config.paramSearch
                            : defaultparamSearch,
                        ignoreRevlogsBeforeMs: getIgnoreRevlogsBeforeMs(),
                        currentParams: params,
                        numOfRelearningSteps: numOfRelearningStepsInDay,
                    });

                    const already_optimal =
                        (params.length &&
                            params.every(
                                (n, i) => n.toFixed(4) === resp.params[i].toFixed(4),
                            )) ||
                        resp.params.length === 0;

                    if (already_optimal) {
                        const msg = resp.fsrsItems
                            ? tr.deckConfigFsrsParamsOptimal()
                            : tr.deckConfigFsrsParamsNoReviews();
                        setTimeout(() => alert(msg), 200);
                    } else {
                        $config.fsrsParams5 = resp.params;
                    }
                    if (computeParamsProgress) {
                        computeParamsProgress.current = computeParamsProgress.total;
                    }
                },
                (progress) => {
                    if (progress.value.case === "computeParams") {
                        computeParamsProgress = progress.value.value;
                    }
                },
            );
        } finally {
            computingParams = false;
        }
    }

    async function checkParams(): Promise<void> {
        if (checkingParams) {
            await setWantsAbort({});
            return;
        }
        if (state.presetAssignmentsChanged()) {
            alert(tr.deckConfigPleaseSaveYourChangesFirst());
            return;
        }
        checkingParams = true;
        computeParamsProgress = undefined;
        try {
            await runWithBackendProgress(
                async () => {
                    const search = $config.paramSearch
                        ? $config.paramSearch
                        : defaultparamSearch;
                    const resp = await evaluateParams({
                        params: fsrsParams($config),
                        search,
                        ignoreRevlogsBeforeMs: getIgnoreRevlogsBeforeMs(),
                    });
                    if (computeParamsProgress) {
                        computeParamsProgress.current = computeParamsProgress.total;
                    }
                    setTimeout(
                        () =>
                            alert(
                                `Log loss: ${resp.logLoss.toFixed(4)}, RMSE(bins): ${(
                                    resp.rmseBins * 100
                                ).toFixed(2)}%. ${tr.deckConfigSmallerIsBetter()}`,
                            ),
                        200,
                    );
                },
                (progress) => {
                    if (progress.value.case === "computeParams") {
                        computeParamsProgress = progress.value.value;
                    }
                },
            );
        } finally {
            checkingParams = false;
        }
    }

    async function computeRetention(): Promise<void> {
        if (computingRetention) {
            await setWantsAbort({});
            return;
        }
        if (state.presetAssignmentsChanged()) {
            alert(tr.deckConfigPleaseSaveYourChangesFirst());
            return;
        }
        computingRetention = true;
        computeRetentionProgress = undefined;
        try {
            await runWithBackendProgress(
                async () => {
                    optimalRetentionRequest.maxInterval = $config.maximumReviewInterval;
                    optimalRetentionRequest.params = fsrsParams($config);
                    optimalRetentionRequest.search = `preset:"${state.getCurrentNameForSearch()}" -is:suspended`;
                    const resp = await computeOptimalRetention(optimalRetentionRequest);
                    optimalRetention = resp.optimalRetention;
                    computeRetentionProgress = undefined;
                },
                (progress) => {
                    if (progress.value.case === "computeRetention") {
                        computeRetentionProgress = progress.value.value;
                    }
                },
            );
        } finally {
            computingRetention = false;
        }
    }

    $: computeParamsProgressString = renderWeightProgress(computeParamsProgress);
    $: computeRetentionProgressString = renderRetentionProgress(
        computeRetentionProgress,
    );
    $: totalReviews = computeParamsProgress?.reviews ?? undefined;

    function renderWeightProgress(val: ComputeParamsProgress | undefined): String {
        if (!val || !val.total) {
            return "";
        }
        const pct = ((val.current / val.total) * 100).toFixed(1);
        if (val instanceof ComputeRetentionProgress) {
            return `${pct}%`;
        } else {
            return tr.deckConfigPercentOfReviews({ pct, reviews: val.reviews });
        }
    }

    function renderRetentionProgress(
        val: ComputeRetentionProgress | undefined,
    ): String {
        if (!val) {
            return "";
        }
        return tr.deckConfigIterations({ count: val.current });
    }

    function estimatedRetention(retention: number): String {
        if (!retention) {
            return "";
        }
        return tr.deckConfigPredictedOptimalRetention({ num: retention.toFixed(2) });
    }

    async function computeAllParams(): Promise<void> {
        await commitEditing();
        state.save(UpdateDeckConfigsMode.COMPUTE_ALL_PARAMS);
    }

    let tableData: TableDatum[] = [];
    const bounds = defaultGraphBounds();
    bounds.marginLeft += 8;
    let svg: HTMLElement | SVGElement | null = null;
    let simulationNumber = 0;

    let points: Point[] = [];

    function addArrays(arr1: number[], arr2: number[]): number[] {
        return arr1.map((value, index) => value + arr2[index]);
    }

    async function simulateFsrs(): Promise<void> {
        let resp: SimulateFsrsReviewResponse | undefined;
        simulationNumber += 1;
        try {
            await runWithBackendProgress(
                async () => {
                    simulateFsrsRequest.params = fsrsParams($config);
                    simulateFsrsRequest.desiredRetention = $config.desiredRetention;
                    simulateFsrsRequest.search = `preset:"${state.getCurrentNameForSearch()}" -is:suspended`;
                    simulateFsrsRequest.newCardsIgnoreReviewLimit =
                        $newCardsIgnoreReviewLimit;
                    simulating = true;
                    resp = await simulateFsrsReview(simulateFsrsRequest);
                },
                () => {},
            );
        } finally {
            simulating = false;
            if (resp) {
                const dailyTotalCount = addArrays(
                    resp.dailyReviewCount,
                    resp.dailyNewCount,
                );

                const dailyMemorizedCount = resp.accumulatedKnowledgeAcquisition;

                points = points.concat(
                    resp.dailyTimeCost.map((v, i) => ({
                        x: i,
                        timeCost: v,
                        count: dailyTotalCount[i],
                        memorized: dailyMemorizedCount[i],
                        label: simulationNumber,
                    })),
                );

                tableData = renderSimulationChart(
                    svg as SVGElement,
                    bounds,
                    points,
                    simulateSubgraph,
                );
            }
        }
    }

    $: tableData = renderSimulationChart(
        svg as SVGElement,
        bounds,
        points,
        simulateSubgraph,
    );

    function clearSimulation(): void {
        points = points.filter((p) => p.label !== simulationNumber);
        simulationNumber = Math.max(0, simulationNumber - 1);
        tableData = renderSimulationChart(
            svg as SVGElement,
            bounds,
            points,
            simulateSubgraph,
        );
    }
</script>

<SpinBoxFloatRow
    bind:value={$config.desiredRetention}
    defaultValue={defaults.desiredRetention}
    min={0.7}
    max={0.99}
    percentage={true}
>
    <SettingTitle on:click={() => openHelpModal("desiredRetention")}>
        {tr.deckConfigDesiredRetention()}
    </SettingTitle>
</SpinBoxFloatRow>

<Warning warning={desiredRetentionWarning} className={retentionWarningClass} />

<div class="ms-1 me-1">
    <ParamsInputRow
        bind:value={$config.fsrsParams5}
        defaultValue={[]}
        defaults={defaults.fsrsParams5}
    >
        <SettingTitle on:click={() => openHelpModal("modelParams")}>
            {tr.deckConfigWeights()}
        </SettingTitle>
    </ParamsInputRow>

    <ParamsSearchRow
        bind:value={$config.paramSearch}
        placeholder={defaultparamSearch}
    />

    <button
        class="btn {computingParams ? 'btn-warning' : 'btn-primary'}"
        disabled={!computingParams && computing}
        on:click={() => computeParams()}
    >
        {#if computingParams}
            {tr.actionsCancel()}
        {:else}
            {tr.deckConfigOptimizeButton()}
        {/if}
    </button>
    <button
        class="btn {checkingParams ? 'btn-warning' : 'btn-primary'}"
        disabled={!checkingParams && computing}
        on:click={() => checkParams()}
    >
        {#if checkingParams}
            {tr.actionsCancel()}
        {:else}
            {tr.deckConfigEvaluateButton()}
        {/if}
    </button>
    <div>
        {#if computingParams || checkingParams}
            {computeParamsProgressString}
        {:else if totalReviews !== undefined}
            {tr.statisticsReviews({ reviews: totalReviews })}
        {/if}
    </div>
</div>

<div class="m-1">
    <Warning warning={lastOptimizationWarning} className="alert-warning" />

    <button class="btn btn-primary" on:click={() => computeAllParams()}>
        {tr.deckConfigSaveAndOptimize()}
    </button>
</div>

<div class="m-2">
    <SwitchRow bind:value={$fsrsReschedule} defaultValue={false}>
        <SettingTitle on:click={() => openHelpModal("rescheduleCardsOnChange")}>
            <GlobalLabel title={tr.deckConfigRescheduleCardsOnChange()} />
        </SettingTitle>
    </SwitchRow>

    {#if $fsrsReschedule}
        <Warning warning={tr.deckConfigRescheduleCardsWarning()} />
    {/if}
</div>

<div class="m-2">
    <details>
        <summary>{tr.deckConfigComputeOptimalRetention()}</summary>

        <SpinBoxRow
            bind:value={optimalRetentionRequest.daysToSimulate}
            defaultValue={365}
            min={1}
            max={3650}
        >
            <SettingTitle on:click={() => openHelpModal("computeOptimalRetention")}>
                {tr.deckConfigDaysToSimulate()}
            </SettingTitle>
        </SpinBoxRow>

        <button
            class="btn {computingRetention ? 'btn-warning' : 'btn-primary'}"
            disabled={!computingRetention && computing}
            on:click={() => computeRetention()}
        >
            {#if computingRetention}
                {tr.actionsCancel()}
            {:else}
                {tr.deckConfigComputeButton()}
            {/if}
        </button>

        {#if optimalRetention}
            {estimatedRetention(optimalRetention)}
            {#if optimalRetention - $config.desiredRetention >= 0.01}
                <Warning
                    warning={tr.deckConfigDesiredRetentionBelowOptimal()}
                    className="alert-warning"
                />
            {/if}
        {/if}

        {#if computingRetention}
            <div>{computeRetentionProgressString}</div>
        {/if}
    </details>
</div>

<div class="m-2">
    <details>
        <summary>{tr.deckConfigFsrsSimulatorExperimental()}</summary>

        <SpinBoxRow
            bind:value={simulateFsrsRequest.daysToSimulate}
            defaultValue={365}
            min={1}
            max={3650}
        >
            <SettingTitle on:click={() => openHelpModal("simulateFsrsReview")}>
                {tr.deckConfigDaysToSimulate()}
            </SettingTitle>
        </SpinBoxRow>

        <SpinBoxRow
            bind:value={simulateFsrsRequest.deckSize}
            defaultValue={0}
            min={0}
            max={100000}
        >
            <SettingTitle on:click={() => openHelpModal("simulateFsrsReview")}>
                {tr.deckConfigAdditionalNewCardsToSimulate()}
            </SettingTitle>
        </SpinBoxRow>

        <SpinBoxRow
            bind:value={simulateFsrsRequest.newLimit}
            defaultValue={$config.newPerDay}
            min={0}
            max={9999}
        >
            <SettingTitle on:click={() => openHelpModal("simulateFsrsReview")}>
                {tr.schedulingNewCardsday()}
            </SettingTitle>
        </SpinBoxRow>

        <SpinBoxRow
            bind:value={simulateFsrsRequest.reviewLimit}
            defaultValue={$config.reviewsPerDay}
            min={0}
            max={9999}
        >
            <SettingTitle on:click={() => openHelpModal("simulateFsrsReview")}>
                {tr.schedulingMaximumReviewsday()}
            </SettingTitle>
        </SpinBoxRow>

        <SpinBoxRow
            bind:value={simulateFsrsRequest.maxInterval}
            defaultValue={$config.maximumReviewInterval}
            min={1}
            max={36500}
        >
            <SettingTitle on:click={() => openHelpModal("simulateFsrsReview")}>
                {tr.schedulingMaximumInterval()}
            </SettingTitle>
        </SpinBoxRow>

        <button
            class="btn {computing ? 'btn-warning' : 'btn-primary'}"
            disabled={computing}
            on:click={() => simulateFsrs()}
        >
            {tr.deckConfigSimulate()}
        </button>

        <button
            class="btn {computing ? 'btn-warning' : 'btn-primary'}"
            disabled={computing}
            on:click={() => clearSimulation()}
        >
            {tr.deckConfigClearLastSimulate()}
        </button>
        {#if simulating}
            {tr.qtMiscProcessing()}
        {/if}

        <Graph>
            <div class="radio-group">
                <InputBox>
                    <label>
                        <input
                            type="radio"
                            value={SimulateSubgraph.count}
                            bind:group={simulateSubgraph}
                        />
                        {tr.deckConfigFsrsSimulatorRadioCount()}
                    </label>
                    <label>
                        <input
                            type="radio"
                            value={SimulateSubgraph.time}
                            bind:group={simulateSubgraph}
                        />
                        {tr.statisticsReviewsTimeCheckbox()}
                    </label>
                    <label>
                        <input
                            type="radio"
                            value={SimulateSubgraph.memorized}
                            bind:group={simulateSubgraph}
                        />
                        {tr.deckConfigFsrsSimulatorRadioMemorized()}
                    </label>
                </InputBox>
            </div>

            <svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
                <CumulativeOverlay />
                <HoverColumns />
                <AxisTicks {bounds} />
                <NoDataOverlay {bounds} />
            </svg>

            <TableData {tableData} />
        </Graph>
    </details>
</div>

<style>
    div.radio-group {
        margin: 0.5em;
    }
</style>
