ac-predictor

コンテスト中にAtCoderのパフォーマンスを予測します

目前為 2023-07-29 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         ac-predictor
// @namespace    http://ac-predictor.azurewebsites.net/
// @version      2.0.1
// @description  コンテスト中にAtCoderのパフォーマンスを予測します
// @author       keymoon
// @license      MIT
// @match        https://atcoder.jp/*
// @exclude      https://atcoder.jp/*/json
// @grant        none
// ==/UserScript==
var config_header_text$1 = "ac-predictor 設定";
var config_hideDuringContest_label$1 = "コンテスト中に予測を非表示にする";
var config_hideUntilFixed_label$1 = "パフォーマンスが確定するまで非表示にする";
var config_useFinalResultOnVirtual_label$1 = "バーチャル参加時のパフォーマンス表示に最終結果を用いる";
var config_dropdown$1 = "ac-predictor 設定";
var standings_performance_column_label$1 = "perf";
var standings_rate_change_column_label$1 = "レート変化";
var standings_click_to_compute_label$1 = "クリックして計算";
var standings_not_provided_label$1 = "提供不可";
var jaJson = {
	config_header_text: config_header_text$1,
	config_hideDuringContest_label: config_hideDuringContest_label$1,
	config_hideUntilFixed_label: config_hideUntilFixed_label$1,
	config_useFinalResultOnVirtual_label: config_useFinalResultOnVirtual_label$1,
	config_dropdown: config_dropdown$1,
	standings_performance_column_label: standings_performance_column_label$1,
	standings_rate_change_column_label: standings_rate_change_column_label$1,
	standings_click_to_compute_label: standings_click_to_compute_label$1,
	standings_not_provided_label: standings_not_provided_label$1
};

var config_header_text = "ac-predictor settings";
var config_hideDuringContest_label = "hide prediction during contests";
var config_hideUntilFixed_label = "hide prediction until performances are fixed";
var config_useFinalResultOnVirtual_label = "use result at the end as a performance reference during virtual participation";
var config_dropdown = "ac-predictor";
var standings_performance_column_label = "perf";
var standings_rate_change_column_label = "rating delta";
var standings_click_to_compute_label = "click to compute";
var standings_not_provided_label = "not provided";
var enJson = {
	config_header_text: config_header_text,
	config_hideDuringContest_label: config_hideDuringContest_label,
	config_hideUntilFixed_label: config_hideUntilFixed_label,
	config_useFinalResultOnVirtual_label: config_useFinalResultOnVirtual_label,
	config_dropdown: config_dropdown,
	standings_performance_column_label: standings_performance_column_label,
	standings_rate_change_column_label: standings_rate_change_column_label,
	standings_click_to_compute_label: standings_click_to_compute_label,
	standings_not_provided_label: standings_not_provided_label
};

// should not be here
function getCurrentLanguage() {
    const elems = document.querySelectorAll("#navbar-collapse .dropdown > a");
    if (elems.length == 0)
        return "JA";
    for (let i = 0; i < elems.length; i++) {
        if (elems[i].textContent?.includes("English"))
            return "EN";
        if (elems[i].textContent?.includes("日本語"))
            return "JA";
    }
    console.warn("language detection failed. fallback to English");
    return "EN";
}
const language = getCurrentLanguage();
const currentJson = { "EN": enJson, "JA": jaJson }[language];
function getTranslation(label) {
    return currentJson[label];
}
function substitute(input) {
    for (const key in currentJson) {
        // @ts-ignore
        input = input.replaceAll(`{${key}}`, currentJson[key]);
    }
    return input;
}

const configKey = "ac-predictor-config";
const defaultConfig = {
    useResults: true,
    hideDuringContest: false,
    isDebug: false,
    hideUntilFixed: false,
    useFinalResultOnVirtual: false
};
function getConfigObj() {
    const val = localStorage.getItem(configKey) ?? "{}";
    let config;
    try {
        config = JSON.parse(val);
    }
    catch {
        console.warn("invalid config found", val);
        config = {};
    }
    return { ...defaultConfig, ...config };
}
function storeConfigObj(config) {
    localStorage.setItem(configKey, JSON.stringify(config));
}
function getConfig(configKey) {
    return getConfigObj()[configKey];
}
function setConfig(key, value) {
    const config = getConfigObj();
    config[key] = value;
    storeConfigObj(config);
}

function isDebugMode() {
    return location.hash.includes("ac-predictor-debug") || getConfig("isDebug");
}

var modalHTML = "<div id=\"modal-ac-predictor-settings\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n\t<div class=\"modal-dialog\" role=\"document\">\n\t<div class=\"modal-content\">\n\t\t<div class=\"modal-header\">\n\t\t\t<button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">×</span></button>\n\t\t\t<h4 class=\"modal-title\">{config_header_text}</h4>\n\t\t</div>\n\t\t<div class=\"modal-body\">\n\t\t\t<div class=\"container-fluid\">\n\t\t\t\t<div class=\"settings-row\" class=\"row\">\n\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"modal-footer\">\n\t\t\t<button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">close</button>\n\t\t</div>\n\t</div>\n</div>\n</div>";

var newDropdownElem = "<li><a id=\"ac-predictor-settings-dropdown-button\" data-toggle=\"modal\" data-target=\"#modal-ac-predictor-settings\" style=\"cursor : pointer;\"><i class=\"a-icon a-icon-setting\"></i> {config_dropdown}</a></li>\n";

var legacyDropdownElem = "<li><a id=\"ac-predictor-settings-dropdown-button\" data-toggle=\"modal\" data-target=\"#modal-ac-predictor-settings\" style=\"cursor : pointer;\"><span class=\"glyphicon glyphicon-wrench\" aria-hidden=\"true\"></span> {config_dropdown}</a></li>\n";

class ConfigView {
    modalElement;
    constructor(modalElement) {
        this.modalElement = modalElement;
    }
    addCheckbox(label, val, handler) {
        const settingsRow = this.getSettingsRow();
        const div = document.createElement("div");
        div.classList.add("checkbox");
        const labelElem = document.createElement("label");
        const input = document.createElement("input");
        input.type = "checkbox";
        input.checked = val;
        labelElem.append(input);
        labelElem.append(label);
        div.append(labelElem);
        settingsRow.append(div);
        input.addEventListener("change", () => {
            handler(input.checked);
        });
    }
    addHeader(level, content) {
        const settingsRow = this.getSettingsRow();
        const div = document.createElement(`h${level}`);
        div.textContent = content;
        settingsRow.append(div);
    }
    getSettingsRow() {
        return this.modalElement.querySelector(".settings-row");
    }
    static Create() {
        document.querySelector("body")?.insertAdjacentHTML("afterbegin", substitute(modalHTML));
        document.querySelector(".header-mypage_list li:nth-last-child(1)")?.insertAdjacentHTML("beforebegin", substitute(newDropdownElem));
        document.querySelector(".navbar-right .dropdown-menu .divider:nth-last-child(2)")?.insertAdjacentHTML("beforebegin", substitute(legacyDropdownElem));
        const element = document.querySelector("#modal-ac-predictor-settings");
        if (element === null) {
            throw new Error("settings modal not found");
        }
        return new ConfigView(element);
    }
}

class ConfigController {
    register() {
        const configView = ConfigView.Create();
        configView.addCheckbox(getTranslation("config_useFinalResultOnVirtual_label"), getConfig("useFinalResultOnVirtual"), val => setConfig("useFinalResultOnVirtual", val));
        configView.addCheckbox(getTranslation("config_hideDuringContest_label"), getConfig("hideDuringContest"), val => setConfig("hideDuringContest", val));
        configView.addCheckbox(getTranslation("config_hideUntilFixed_label"), getConfig("hideUntilFixed"), val => setConfig("hideUntilFixed", val));
        if (isDebugMode()) {
            configView.addCheckbox("[DEBUG] enable debug mode", getConfig("isDebug"), val => setConfig("isDebug", val));
            configView.addCheckbox("[DEBUG] use results", getConfig("useResults"), val => setConfig("useResults", val));
        }
    }
}

class Cache {
    cacheDuration;
    cacheExpires = new Map();
    cacheData = new Map();
    constructor(cacheDuration) {
        this.cacheDuration = cacheDuration;
    }
    has(key) {
        return this.cacheExpires.has(key) || Date.now() <= this.cacheExpires.get(key);
    }
    set(key, content) {
        const expire = Date.now() + this.cacheDuration;
        this.cacheExpires.set(key, expire);
        this.cacheData.set(key, content);
    }
    get(key) {
        if (!this.has(key)) {
            throw new Error(`invalid key: ${key}`);
        }
        return this.cacheData.get(key);
    }
}

class HistoriesWrapper {
    data;
    constructor(data) {
        this.data = data;
    }
    toPerformances() {
        const results = [];
        for (const history of this.data) {
            if (!history.IsRated)
                continue;
            results.push(history.Performance);
        }
        return results;
    }
    toPerformanceAndTimes() {
        const results = [];
        for (const history of this.data) {
            if (!history.IsRated)
                continue;
            const date = new Date(history.EndTime);
            results.push({ performance: history.Performance, date });
        }
        return results;
    }
}
const HISTORY_CACHE_DURATION = 60 * 60 * 1000;
const cache$3 = new Cache(HISTORY_CACHE_DURATION);
async function getHistory(userScreenName, contestType = "algorithm") {
    const key = `${userScreenName}:${contestType}`;
    if (!cache$3.has(key)) {
        const result = await fetch(`https://atcoder.jp/users/${userScreenName}/history/json?contestType=${contestType}`);
        if (!result.ok) {
            throw new Error(`Failed to fetch history: ${result.status}`);
        }
        cache$3.set(key, await result.json());
    }
    return new HistoriesWrapper(cache$3.get(key));
}

//Copyright © 2017 koba-e964.
//from : https://github.com/koba-e964/atcoder-rating-estimator
const finf = bigf(400);
function bigf(n) {
    let pow1 = 1;
    let pow2 = 1;
    let numerator = 0;
    let denominator = 0;
    for (let i = 0; i < n; ++i) {
        pow1 *= 0.81;
        pow2 *= 0.9;
        numerator += pow1;
        denominator += pow2;
    }
    return Math.sqrt(numerator) / denominator;
}
function f(n) {
    return ((bigf(n) - finf) / (bigf(1) - finf)) * 1200.0;
}
/**
 * calculate unpositivized rating from performance history
 * @param {Number[]} [history] performance history with ascending order
 * @returns {Number} unpositivized rating
 */
function calcAlgRatingFromHistory(history) {
    const n = history.length;
    let pow = 1;
    let numerator = 0.0;
    let denominator = 0.0;
    for (let i = n - 1; i >= 0; i--) {
        pow *= 0.9;
        numerator += Math.pow(2, history[i] / 800.0) * pow;
        denominator += pow;
    }
    return Math.log2(numerator / denominator) * 800.0 - f(n);
}
/**
 * calculate unpositivized rating from last state
 * @param {Number} [last] last unpositivized rating
 * @param {Number} [perf] performance
 * @param {Number} [ratedMatches] count of participated rated contest
 * @returns {number} estimated unpositivized rating
 */
function calcAlgRatingFromLast(last, perf, ratedMatches) {
    if (ratedMatches === 0)
        return perf - 1200;
    last += f(ratedMatches);
    const weight = 9 - 9 * 0.9 ** ratedMatches;
    const numerator = weight * 2 ** (last / 800.0) + 2 ** (perf / 800.0);
    const denominator = 1 + weight;
    return Math.log2(numerator / denominator) * 800.0 - f(ratedMatches + 1);
}
/**
 * calculate the performance required to reach a target rate
 * @param {Number} [targetRating] targeted unpositivized rating
 * @param {Number[]} [history] performance history with ascending order
 * @returns {number} performance
 */
function calcRequiredPerformance(targetRating, history) {
    let valid = 10000.0;
    let invalid = -10000.0;
    for (let i = 0; i < 100; ++i) {
        const mid = (invalid + valid) / 2;
        const rating = Math.round(calcAlgRatingFromHistory(history.concat([mid])));
        if (targetRating <= rating)
            valid = mid;
        else
            invalid = mid;
    }
    return valid;
}
/**
 * calculate unpositivized rating from performance history
 * @param {Number[]} [history] performance histories
 * @returns {Number} unpositivized rating
 */
function calcHeuristicRatingFromHistory(history) {
    const S = 724.4744301;
    const R = 0.8271973364;
    const qs = [];
    for (const perf of history) {
        for (let i = 1; i <= 100; i++) {
            qs.push(perf - S * Math.log(i));
        }
    }
    qs.sort((a, b) => b - a);
    let num = 0.0;
    let den = 0.0;
    for (let i = 99; i >= 0; i--) {
        num = num * R + qs[i];
        den = den * R + 1.0;
    }
    return num / den;
}
/**
 * (-inf, inf) -> (0, inf)
 * @param {Number} [rating] unpositivized rating
 * @returns {number} positivized rating
 */
function positivizeRating(rating) {
    if (rating >= 400.0) {
        return rating;
    }
    return 400.0 * Math.exp((rating - 400.0) / 400.0);
}
/**
 * (0, inf) -> (-inf, inf)
 * @param {Number} [rating] positivized rating
 * @returns {number} unpositivized rating
 */
function unpositivizeRating(rating) {
    if (rating >= 400.0) {
        return rating;
    }
    return 400.0 + 400.0 * Math.log(rating / 400.0);
}
const colorNames = ["unrated", "gray", "brown", "green", "cyan", "blue", "yellow", "orange", "red"];
function getColor(rating) {
    const colorIndex = rating > 0 ? Math.min(Math.floor(rating / 400) + 1, 8) : 0;
    return colorNames[colorIndex];
}

// @ts-nocheck
var dom$1 = "<div id=\"estimator-alert\"></div>\n<div class=\"row\">\n\t<div class=\"input-group\">\n\t\t<span class=\"input-group-addon\" id=\"estimator-input-desc\"></span>\n\t\t<input type=\"number\" class=\"form-control\" id=\"estimator-input\">\n\t</div>\n</div>\n<div class=\"row\">\n\t<div class=\"input-group\">\n\t\t<span class=\"input-group-addon\" id=\"estimator-res-desc\"></span>\n\t\t<input class=\"form-control\" id=\"estimator-res\" disabled=\"disabled\">\n\t\t<span class=\"input-group-btn\">\n\t\t\t<button class=\"btn btn-default\" id=\"estimator-toggle\">入替</button>\n\t\t</span>\n\t</div>\n</div>\n<div class=\"row\" style=\"margin: 10px 0px;\">\n\t<a class=\"btn btn-default col-xs-offset-8 col-xs-4\" rel=\"nofollow\" onclick=\"window.open(encodeURI(decodeURI(this.href)),'twwindow','width=550, height=450, personalbar=0, toolbar=0, scrollbars=1'); return false;\" id=\"estimator-tweet\">ツイート</a>\n</div>";
class EstimatorModel {
    inputDesc;
    resultDesc;
    perfHistory;
    constructor(inputValue, perfHistory) {
        this.inputDesc = "";
        this.resultDesc = "";
        this.perfHistory = perfHistory;
        this.updateInput(inputValue);
    }
    inputValue;
    resultValue;
    updateInput(value) {
        this.inputValue = value;
        this.resultValue = this.calcResult(value);
    }
    toggle() {
        return null;
    }
    calcResult(input) {
        return input;
    }
}
class CalcRatingModel extends EstimatorModel {
    constructor(inputValue, perfHistory) {
        super(inputValue, perfHistory);
        this.inputDesc = "パフォーマンス";
        this.resultDesc = "到達レーティング";
    }
    // @ts-ignore
    toggle() {
        return new CalcPerfModel(this.resultValue, this.perfHistory);
    }
    calcResult(input) {
        return positivizeRating(calcAlgRatingFromHistory(this.perfHistory.concat([input])));
    }
}
class CalcPerfModel extends EstimatorModel {
    constructor(inputValue, perfHistory) {
        super(inputValue, perfHistory);
        this.inputDesc = "目標レーティング";
        this.resultDesc = "必要パフォーマンス";
    }
    // @ts-ignore
    toggle() {
        return new CalcRatingModel(this.resultValue, this.perfHistory);
    }
    calcResult(input) {
        return calcRequiredPerformance(unpositivizeRating(input), this.perfHistory);
    }
}
function GetEmbedTweetLink(content, url) {
    return `https://twitter.com/share?text=${encodeURI(content)}&url=${encodeURI(url)}`;
}
function getLS(key) {
    const val = localStorage.getItem(key);
    return (val ? JSON.parse(val) : val);
}
function setLS(key, val) {
    try {
        localStorage.setItem(key, JSON.stringify(val));
    }
    catch (error) {
        console.log(error);
    }
}
const models = [CalcPerfModel, CalcRatingModel];
function GetModelFromStateCode(state, value, history) {
    let model = models.find((model) => model.name === state);
    if (!model)
        model = CalcPerfModel;
    return new model(value, history);
}
function getPerformanceHistories(history) {
    const onlyRated = history.filter((x) => x.IsRated);
    onlyRated.sort((a, b) => {
        return new Date(a.EndTime).getTime() - new Date(b.EndTime).getTime();
    });
    return onlyRated.map((x) => x.Performance);
}
function roundValue(value, numDigits) {
    return Math.round(value * Math.pow(10, numDigits)) / Math.pow(10, numDigits);
}
class EstimatorElement {
    id;
    title;
    document;
    constructor() {
        this.id = "estimator";
        this.title = "Estimator";
        this.document = dom$1;
    }
    async afterOpen() {
        const estimatorInputSelector = document.getElementById("estimator-input");
        const estimatorResultSelector = document.getElementById("estimator-res");
        let model = GetModelFromStateCode(getLS("sidemenu_estimator_state"), getLS("sidemenu_estimator_value"), getPerformanceHistories((await getHistory(userScreenName)).data));
        updateView();
        document.getElementById("estimator-toggle").addEventListener("click", () => {
            model = model.toggle();
            updateLocalStorage();
            updateView();
        });
        estimatorInputSelector.addEventListener("keyup", () => {
            updateModel();
            updateLocalStorage();
            updateView();
        });
        /** modelをinputの値に応じて更新 */
        function updateModel() {
            const inputNumber = estimatorInputSelector.valueAsNumber;
            if (!isFinite(inputNumber))
                return;
            model.updateInput(inputNumber);
        }
        /** modelの状態をLSに保存 */
        function updateLocalStorage() {
            setLS("sidemenu_estimator_value", model.inputValue);
            setLS("sidemenu_estimator_state", model.constructor.name);
        }
        /** modelを元にviewを更新 */
        function updateView() {
            const roundedInput = roundValue(model.inputValue, 2);
            const roundedResult = roundValue(model.resultValue, 2);
            document.getElementById("estimator-input-desc").innerText = model.inputDesc;
            document.getElementById("estimator-res-desc").innerText = model.resultDesc;
            estimatorInputSelector.value = String(roundedInput);
            estimatorResultSelector.value = String(roundedResult);
            const tweetStr = `AtCoderのハンドルネーム: ${userScreenName}\n${model.inputDesc}: ${roundedInput}\n${model.resultDesc}: ${roundedResult}\n`;
            document.getElementById("estimator-tweet").href = GetEmbedTweetLink(tweetStr, "https://greasyfork.org/ja/scripts/369954-ac-predictor");
        }
    }
    ;
    GetHTML() {
        return `<div class="menu-wrapper">
<div class="menu-header">
    <h4 class="sidemenu-txt">${this.title}<span class="glyphicon glyphicon-menu-up" style="float: right"></span></h4>
</div>
<div class="menu-box"><div class="menu-content" id="${this.id}">${this.document}</div></div>
</div>`;
    }
}
const estimator = new EstimatorElement();
var sidemenuHtml = "<style>\n    #menu-wrap {\n        display: block;\n        position: fixed;\n        top: 0;\n        z-index: 20;\n        width: 400px;\n        right: -350px;\n        transition: all 150ms 0ms ease;\n        margin-top: 50px;\n    }\n\n    #sidemenu {\n        background: #000;\n        opacity: 0.85;\n    }\n    #sidemenu-key {\n        border-radius: 5px 0px 0px 5px;\n        background: #000;\n        opacity: 0.85;\n        color: #FFF;\n        padding: 30px 0;\n        cursor: pointer;\n        margin-top: 100px;\n        text-align: center;\n    }\n\n    #sidemenu {\n        display: inline-block;\n        width: 350px;\n        float: right;\n    }\n\n    #sidemenu-key {\n        display: inline-block;\n        width: 50px;\n        float: right;\n    }\n\n    .sidemenu-active {\n        transform: translateX(-350px);\n    }\n\n    .sidemenu-txt {\n        color: #DDD;\n    }\n\n    .menu-wrapper {\n        border-bottom: 1px solid #FFF;\n    }\n\n    .menu-header {\n        margin: 10px 20px 10px 20px;\n        user-select: none;\n    }\n\n    .menu-box {\n        overflow: hidden;\n        transition: all 300ms 0s ease;\n    }\n    .menu-box-collapse {\n        height: 0px !important;\n    }\n    .menu-box-collapse .menu-content {\n        transform: translateY(-100%);\n    }\n    .menu-content {\n        padding: 10px 20px 10px 20px;\n        transition: all 300ms 0s ease;\n    }\n    .cnvtb-fixed {\n        z-index: 19;\n    }\n</style>\n<div id=\"menu-wrap\">\n    <div id=\"sidemenu\" class=\"container\"></div>\n    <div id=\"sidemenu-key\" class=\"glyphicon glyphicon-menu-left\"></div>\n</div>";
class SideMenu {
    pendingElements;
    constructor() {
        this.pendingElements = [];
        this.Generate();
    }
    Generate() {
        document.getElementById("main-div").insertAdjacentHTML("afterbegin", sidemenuHtml);
        resizeSidemenuHeight();
        const key = document.getElementById("sidemenu-key");
        const wrap = document.getElementById("menu-wrap");
        key.addEventListener("click", () => {
            this.pendingElements.forEach((elem) => {
                elem.afterOpen();
            });
            this.pendingElements.length = 0;
            key.classList.toggle("glyphicon-menu-left");
            key.classList.toggle("glyphicon-menu-right");
            wrap.classList.toggle("sidemenu-active");
        });
        window.addEventListener("onresize", resizeSidemenuHeight);
        document.getElementById("sidemenu").addEventListener("click", (event) => {
            const target = event.target;
            const header = target.closest(".menu-header");
            if (!header)
                return;
            const box = target.closest(".menu-wrapper").querySelector(".menu-box");
            box.classList.toggle("menu-box-collapse");
            const arrow = target.querySelector(".glyphicon");
            arrow.classList.toggle("glyphicon-menu-down");
            arrow.classList.toggle("glyphicon-menu-up");
        });
        function resizeSidemenuHeight() {
            document.getElementById("sidemenu").style.height = `${window.innerHeight}px`;
        }
    }
    addElement(element) {
        const sidemenu = document.getElementById("sidemenu");
        sidemenu.insertAdjacentHTML("afterbegin", element.GetHTML());
        const content = sidemenu.querySelector(".menu-content");
        content.parentElement.style.height = `${content.offsetHeight}px`;
        // element.afterAppend();
        this.pendingElements.push(element);
    }
}
function add() {
    const sidemenu = new SideMenu();
    const elements = [estimator];
    for (let i = elements.length - 1; i >= 0; i--) {
        sidemenu.addElement(elements[i]);
    }
}

async function getAPerfs(contestScreenName) {
    const result = await fetch(`https://data.ac-predictor.com/aperfs/${contestScreenName}.json`);
    if (!result.ok) {
        throw new Error(`Failed to fetch aperfs: ${result.status}`);
    }
    return await result.json();
}

// [start, end]
class Range {
    start;
    end;
    constructor(start, end) {
        this.start = start;
        this.end = end;
    }
    contains(val) {
        return this.start <= val && val <= this.end;
    }
    hasValue() {
        return this.start <= this.end;
    }
}

class ContestDetails {
    contestName;
    contestScreenName;
    contestType;
    startTime;
    duration;
    ratedrange;
    constructor(contestName, contestScreenName, contestType, startTime, duration, ratedRange) {
        this.contestName = contestName;
        this.contestScreenName = contestScreenName;
        this.contestType = contestType;
        this.startTime = startTime;
        this.duration = duration;
        this.ratedrange = ratedRange;
    }
    get endTime() {
        return new Date(this.startTime.getTime() + this.duration * 1000);
    }
    get defaultAPerf() {
        if (this.contestType == "heuristic")
            return 1000;
        if (!this.ratedrange.hasValue()) {
            throw new Error("unrated contest");
        }
        if (this.ratedrange.end == 1999)
            return 800;
        if (this.ratedrange.end == 2799)
            return 1000;
        if (4000 <= this.ratedrange.end)
            return 1200;
        throw new Error("unknown contest type");
    }
    get performanceCap() {
        if (this.contestType == "heuristic")
            return Infinity;
        if (!this.ratedrange.hasValue()) {
            throw new Error("unrated contest");
        }
        if (this.ratedrange.end == 1999)
            return 2400;
        if (this.ratedrange.end == 2799)
            return 3200;
        if (4000 <= this.ratedrange.end)
            return Infinity;
        throw new Error("unknown contest type");
    }
    beforeContest(dateTime) {
        return dateTime < this.startTime;
    }
    duringContest(dateTime) {
        return this.startTime < dateTime && dateTime < this.endTime;
    }
    isOver(dateTime) {
        return this.endTime < dateTime;
    }
}

async function getContestDetails() {
    const result = await fetch(`https://data.ac-predictor.com/contest-details.json`);
    if (!result.ok) {
        throw new Error(`Failed to fetch contest details: ${result.status}`);
    }
    const parsed = await result.json();
    const res = [];
    for (const elem of parsed) {
        if (typeof elem !== "object")
            throw new Error("invalid object returned");
        if (typeof elem.contestName !== "string")
            throw new Error("invalid object returned");
        const contestName = elem.contestName;
        if (typeof elem.contestScreenName !== "string")
            throw new Error("invalid object returned");
        const contestScreenName = elem.contestScreenName;
        if (elem.contestType !== "algorithm" && elem.contestType !== "heuristic")
            throw new Error("invalid object returned");
        const contestType = elem.contestType;
        if (typeof elem.startTime !== "number")
            throw new Error("invalid object returned");
        const startTime = new Date(elem.startTime * 1000);
        if (typeof elem.duration !== "number")
            throw new Error("invalid object returned");
        const duration = elem.duration;
        if (typeof elem.ratedrange !== "object" || typeof elem.ratedrange[0] !== "number" || typeof elem.ratedrange[1] !== "number")
            throw new Error("invalid object returned");
        const ratedRange = new Range(elem.ratedrange[0], elem.ratedrange[1]);
        res.push(new ContestDetails(contestName, contestScreenName, contestType, startTime, duration, ratedRange));
    }
    return res;
}

const handlers = [];
function addHandler(handler) {
    handlers.push(handler);
}
// absurd hack to steal ajax response data for caching
// @ts-ignore
$(document).on("ajaxComplete", (_, xhr, settings) => {
    if (xhr.status == 200) {
        for (const handler of handlers) {
            handler(xhr.responseText, settings.url);
        }
    }
});

class ResultsWrapper {
    data;
    constructor(data) {
        this.data = data;
    }
    toPerformanceMaps() {
        const res = new Map();
        for (const result of this.data) {
            if (!result.IsRated)
                continue;
            res.set(result.UserScreenName, result.Performance);
        }
        return res;
    }
    toIsRatedMaps() {
        const res = new Map();
        for (const result of this.data) {
            res.set(result.UserScreenName, result.IsRated);
        }
        return res;
    }
    toOldRatingMaps() {
        const res = new Map();
        for (const result of this.data) {
            res.set(result.UserScreenName, result.OldRating);
        }
        return res;
    }
    toNewRatingMaps() {
        const res = new Map();
        for (const result of this.data) {
            res.set(result.UserScreenName, result.NewRating);
        }
        return res;
    }
}
const RESULTS_CACHE_DURATION = 10 * 1000;
const cache$2 = new Cache(RESULTS_CACHE_DURATION);
async function getResults(contestScreenName) {
    if (!cache$2.has(contestScreenName)) {
        const result = await fetch(`https://atcoder.jp/contests/${contestScreenName}/results/json`);
        if (!result.ok) {
            throw new Error(`Failed to fetch results: ${result.status}`);
        }
        cache$2.set(contestScreenName, await result.json());
    }
    return new ResultsWrapper(cache$2.get(contestScreenName));
}
addHandler((content, path) => {
    const match = path.match(/^\/contests\/([^/]*)\/results\/json$/);
    if (!match)
        return;
    const contestScreenName = match[1];
    cache$2.set(contestScreenName, JSON.parse(content));
});

let StandingsWrapper$1 = class StandingsWrapper {
    data;
    constructor(data) {
        this.data = data;
    }
    toRanks(onlyRated = false) {
        const res = new Map();
        for (const data of this.data.StandingsData) {
            if (onlyRated && !data.IsRated)
                continue;
            res.set(data.UserScreenName, data.Rank);
        }
        return res;
    }
    toRatedUsers() {
        const res = [];
        for (const data of this.data.StandingsData) {
            if (data.IsRated) {
                res.push(data.UserScreenName);
            }
        }
        return res;
    }
    toIsRatedMaps() {
        const res = new Map();
        for (const data of this.data.StandingsData) {
            res.set(data.UserScreenName, data.IsRated);
        }
        return res;
    }
    toOldRatingMaps(unpositivize = false) {
        const res = new Map();
        for (const data of this.data.StandingsData) {
            const rating = this.data.Fixed ? data.OldRating : data.Rating;
            res.set(data.UserScreenName, unpositivize ? unpositivizeRating(rating) : rating);
        }
        return res;
    }
    toCompetitionMaps() {
        const res = new Map();
        for (const data of this.data.StandingsData) {
            res.set(data.UserScreenName, data.Competitions);
        }
        return res;
    }
    toScores() {
        const res = new Map();
        for (const data of this.data.StandingsData) {
            res.set(data.UserScreenName, { score: data.TotalResult.Score, penalty: data.TotalResult.Elapsed });
        }
        return res;
    }
};
const STANDINGS_CACHE_DURATION$1 = 10 * 1000;
const cache$1 = new Cache(STANDINGS_CACHE_DURATION$1);
async function getStandings(contestScreenName) {
    if (!cache$1.has(contestScreenName)) {
        const result = await fetch(`https://atcoder.jp/contests/${contestScreenName}/standings/json`);
        if (!result.ok) {
            throw new Error(`Failed to fetch standings: ${result.status}`);
        }
        cache$1.set(contestScreenName, await result.json());
    }
    return new StandingsWrapper$1(cache$1.get(contestScreenName));
}
addHandler((content, path) => {
    const match = path.match(/^\/contests\/([^/]*)\/standings\/json$/);
    if (!match)
        return;
    const contestScreenName = match[1];
    cache$1.set(contestScreenName, JSON.parse(content));
});

class EloPerformanceProvider {
    ranks;
    ratings;
    cap;
    rankMemo = new Map();
    constructor(ranks, ratings, cap) {
        this.ranks = ranks;
        this.ratings = ratings;
        this.cap = cap;
    }
    availableFor(userScreenName) {
        return this.ranks.has(userScreenName);
    }
    getPerformance(userScreenName) {
        if (!this.availableFor(userScreenName)) {
            throw new Error(`User ${userScreenName} not found`);
        }
        const rank = this.ranks.get(userScreenName);
        return this.getPerformanceForRank(rank);
    }
    getPerformances() {
        const performances = new Map();
        for (const userScreenName of this.ranks.keys()) {
            performances.set(userScreenName, this.getPerformance(userScreenName));
        }
        return performances;
    }
    getPerformanceForRank(rank) {
        let upper = 6144;
        let lower = -2048;
        while (upper - lower > 0.5) {
            const mid = (upper + lower) / 2;
            if (rank > this.getRankForPerformance(mid))
                upper = mid;
            else
                lower = mid;
        }
        return Math.min(this.cap, Math.round((upper + lower) / 2));
    }
    getRankForPerformance(performance) {
        if (this.rankMemo.has(performance))
            return this.rankMemo.get(performance);
        const res = this.ratings.reduce((val, APerf) => val + 1.0 / (1.0 + Math.pow(6.0, (performance - APerf) / 400.0)), 0.5);
        this.rankMemo.set(performance, res);
        return res;
    }
}

class FixedPerformanceProvider {
    result;
    constructor(result) {
        this.result = result;
    }
    availableFor(userScreenName) {
        return this.result.has(userScreenName);
    }
    getPerformance(userScreenName) {
        if (!this.availableFor(userScreenName)) {
            throw new Error(`User ${userScreenName} not found`);
        }
        return this.result.get(userScreenName);
    }
    getPerformances() {
        return this.result;
    }
}

function getRankToUsers(ranks) {
    const rankToUsers = new Map();
    for (const [userScreenName, rank] of ranks) {
        if (!rankToUsers.has(rank))
            rankToUsers.set(rank, []);
        rankToUsers.get(rank).push(userScreenName);
    }
    return rankToUsers;
}
function getMaxRank(ranks) {
    return Math.max(...ranks.values());
}
class InterpolatePerformanceProvider {
    ranks;
    maxRank;
    rankToUsers;
    baseProvider;
    constructor(ranks, baseProvider) {
        this.ranks = ranks;
        this.maxRank = getMaxRank(ranks);
        this.rankToUsers = getRankToUsers(ranks);
        this.baseProvider = baseProvider;
    }
    availableFor(userScreenName) {
        return this.ranks.has(userScreenName);
    }
    getPerformance(userScreenName) {
        if (!this.availableFor(userScreenName)) {
            throw new Error(`User ${userScreenName} not found`);
        }
        if (this.performanceCache.has(userScreenName))
            return this.performanceCache.get(userScreenName);
        let rank = this.ranks.get(userScreenName);
        while (rank <= this.maxRank) {
            const perf = this.getPerformanceIfAvailable(rank);
            if (perf !== null) {
                return perf;
            }
            rank++;
        }
        this.performanceCache.set(userScreenName, -Infinity);
        return -Infinity;
    }
    performanceCache = new Map();
    getPerformances() {
        let currentPerformance = -Infinity;
        const res = new Map();
        for (let rank = this.maxRank; rank >= 0; rank--) {
            const users = this.rankToUsers.get(rank);
            if (users === undefined)
                continue;
            const perf = this.getPerformanceIfAvailable(rank);
            if (perf !== null)
                currentPerformance = perf;
            for (const userScreenName of users) {
                res.set(userScreenName, currentPerformance);
            }
        }
        this.performanceCache = res;
        return res;
    }
    cacheForRank = new Map();
    getPerformanceIfAvailable(rank) {
        if (!this.rankToUsers.has(rank))
            return null;
        if (this.cacheForRank.has(rank))
            return this.cacheForRank.get(rank);
        for (const userScreenName of this.rankToUsers.get(rank)) {
            if (!this.baseProvider.availableFor(userScreenName))
                continue;
            const perf = this.baseProvider.getPerformance(userScreenName);
            this.cacheForRank.set(rank, perf);
            return perf;
        }
        return null;
    }
}

function normalizeRank(ranks) {
    const rankValues = [...new Set(ranks.values()).values()];
    const rankToUsers = new Map();
    for (const [userScreenName, rank] of ranks) {
        if (!rankToUsers.has(rank))
            rankToUsers.set(rank, []);
        rankToUsers.get(rank).push(userScreenName);
    }
    rankValues.sort((a, b) => a - b);
    const res = new Map();
    let currentRank = 1;
    for (const rank of rankValues) {
        const users = rankToUsers.get(rank);
        const averageRank = currentRank + (users.length - 1) / 2;
        for (const userScreenName of users) {
            res.set(userScreenName, averageRank);
        }
        currentRank += users.length;
    }
    return res;
}

class IncrementalAlgRatingProvider {
    unpositivizedRatingMap;
    competitionsMap;
    constructor(unpositivizedRatingMap, competitionsMap) {
        this.unpositivizedRatingMap = unpositivizedRatingMap;
        this.competitionsMap = competitionsMap;
    }
    availableFor(userScreenName) {
        return this.unpositivizedRatingMap.has(userScreenName);
    }
    async getRating(userScreenName, newPerformance) {
        if (!this.availableFor(userScreenName)) {
            throw new Error(`rating not available for ${userScreenName}`);
        }
        const rating = this.unpositivizedRatingMap.get(userScreenName);
        const competitions = this.competitionsMap.get(userScreenName);
        return Math.round(positivizeRating(calcAlgRatingFromLast(rating, newPerformance, competitions)));
    }
}

class ConstRatingProvider {
    ratings;
    constructor(ratings) {
        this.ratings = ratings;
    }
    availableFor(userScreenName) {
        return this.ratings.has(userScreenName);
    }
    async getRating(userScreenName, newPerformance) {
        if (!this.availableFor(userScreenName)) {
            throw new Error(`rating not available for ${userScreenName}`);
        }
        return this.ratings.get(userScreenName);
    }
}

class FromHistoryHeuristicRatingProvider {
    performancesProvider;
    constructor(performancesProvider) {
        this.performancesProvider = performancesProvider;
    }
    availableFor(userScreenName) {
        return true;
    }
    async getRating(userScreenName, newPerformance) {
        const performances = await this.performancesProvider(userScreenName);
        performances.push(newPerformance);
        return Math.round(positivizeRating(calcHeuristicRatingFromHistory(performances)));
    }
}

const PATH_PREFIX = "/contests/";
function getContestScreenName() {
    const location = document.location.pathname;
    if (!location.startsWith(PATH_PREFIX)) {
        throw Error("not on the contest page");
    }
    return location.substring(PATH_PREFIX.length).split("/")[0];
}

function hasOwnProperty(obj, key) {
    return Object.prototype.hasOwnProperty.call(obj, key);
}

class StandingsLoadingView {
    loaded;
    element;
    hooks;
    constructor(element) {
        this.loaded = false;
        this.element = element;
        this.hooks = [];
        this.initHandler();
    }
    onLoad(hook) {
        this.hooks.push(hook);
    }
    initHandler() {
        new MutationObserver(() => {
            if (!this.loaded) {
                if (document.getElementById("standings-tbody") === null)
                    return;
                this.loaded = true;
                this.hooks.forEach(f => f());
            }
        }).observe(this.element, { attributes: true });
    }
    static Get() {
        const loadingElem = document.querySelector("#vue-standings .loading-show");
        if (loadingElem === null) {
            throw new Error("loadingElem not found");
        }
        return new StandingsLoadingView(loadingElem);
    }
}

function toSignedString (n) {
    return `${n >= 0 ? "+" : "-"}${Math.abs(n)}`;
}

function addStyle(styleSheet) {
    const styleElem = document.createElement("style");
    styleElem.textContent = styleSheet;
    document.getElementsByTagName("head")[0].append(styleElem);
}

function getSpan(innerElements, classList) {
    const span = document.createElement("span");
    span.append(...innerElements);
    span.classList.add(...classList);
    return span;
}

function getRatingSpan(rate) {
    return getSpan([rate.toString()], ["bold", "user-" + getColor(rate)]);
}

var style = "/* Tooltip container */\n.my-tooltip {\n  position: relative;\n  display: inline-block;\n}\n\n/* Tooltip text */\n.my-tooltip .my-tooltiptext {\n  visibility: hidden;\n  width: 120px;\n  background-color: black;\n  color: #fff;\n  text-align: center;\n  padding: 5px 0;\n  border-radius: 6px;\n  /* Position the tooltip text - see examples below! */\n  position: absolute;\n  top: 50%;\n  right: 100%;\n  z-index: 1;\n}\n\n/* Show the tooltip text when you mouse over the tooltip container */\n.my-tooltip:hover .my-tooltiptext {\n  visibility: visible;\n}";

addStyle(style);
function getFadedSpan(innerElements) {
    return getSpan(innerElements, ["grey"]);
}
function getRatedRatingElem(result) {
    const elem = document.createElement("div");
    elem.append(getRatingSpan(result.oldRating), " → ", getRatingSpan(result.newRating), " ", getFadedSpan([`(${toSignedString(result.newRating - result.oldRating)})`]));
    return elem;
}
function getUnratedRatingElem(result) {
    const elem = document.createElement("div");
    elem.append(getRatingSpan(result.oldRating), " ", getFadedSpan(["(unrated)"]));
    return elem;
}
function getDefferedRatingElem(result) {
    const elem = document.createElement("div");
    elem.append(getRatingSpan(result.oldRating), " → ", getSpan(["???"], ["bold"]), document.createElement("br"), getFadedSpan([`(${getTranslation("standings_click_to_compute_label")})`]));
    async function listener() {
        elem.removeEventListener("click", listener);
        elem.replaceChildren(getFadedSpan(["loading..."]));
        let newRating;
        try {
            newRating = await result.newRatingCalculator();
        }
        catch (e) {
            elem.append(getSpan(["error on load"], []), document.createElement("br"), getSpan(["(hover to see details)"], ["grey", "small"]), getSpan([e.toString()], ["my-tooltiptext"]));
            elem.classList.add("my-tooltip");
            return;
        }
        const newElem = getRatedRatingElem({ type: "rated", performance: result.performance, oldRating: result.oldRating, newRating: newRating });
        elem.replaceChildren(newElem);
    }
    elem.addEventListener("click", listener);
    return elem;
}
function getPerfOnlyRatingElem(result) {
    const elem = document.createElement("div");
    elem.append(getFadedSpan([`(${getTranslation("standings_not_provided_label")})`]));
    return elem;
}
function getErrorRatingElem(result) {
    const elem = document.createElement("div");
    elem.append(getSpan(["error on load"], []), document.createElement("br"), getSpan(["(hover to see details)"], ["grey", "small"]), getSpan([result.message], ["my-tooltiptext"]));
    elem.classList.add("my-tooltip");
    return elem;
}
function getRatingElem(result) {
    if (result.type == "rated")
        return getRatedRatingElem(result);
    if (result.type == "unrated")
        return getUnratedRatingElem(result);
    if (result.type == "deffered")
        return getDefferedRatingElem(result);
    if (result.type == "perfonly")
        return getPerfOnlyRatingElem();
    if (result.type == "error")
        return getErrorRatingElem(result);
    throw new Error("unreachable");
}
function getPerfElem(result) {
    if (result.type == "error")
        return getSpan(["-"], []);
    return getRatingSpan(result.performance);
}
const headerHtml = `<th class="ac-predictor-standings-elem" style="width:84px;min-width:84px;">${getTranslation("standings_performance_column_label")}</th><th class="ac-predictor-standings-elem" style="width:168px;min-width:168px;">${getTranslation("standings_rate_change_column_label")}</th>`;
function modifyHeader(header) {
    header.insertAdjacentHTML("beforeend", headerHtml);
}
function isFooter(row) {
    return row.firstElementChild?.classList.contains("colspan");
}
async function modifyStandingsRow(row, results) {
    let userScreenName = row.querySelector(".standings-username .username span")?.textContent ?? null;
    if (userScreenName !== null && row.querySelector(".standings-username .username img[src='//img.atcoder.jp/assets/icon/ghost.svg']")) {
        userScreenName = `ghost:${userScreenName}`;
    }
    const perfCell = document.createElement("td");
    perfCell.classList.add("ac-predictor-standings-elem", "standings-result");
    const ratingCell = document.createElement("td");
    ratingCell.classList.add("ac-predictor-standings-elem", "standings-result");
    if (userScreenName === null) {
        perfCell.append("-");
        ratingCell.append("-");
    }
    else {
        const result = await results(userScreenName);
        perfCell.append(getPerfElem(result));
        ratingCell.append(getRatingElem(result));
    }
    row.insertAdjacentElement("beforeend", perfCell);
    row.insertAdjacentElement("beforeend", ratingCell);
}
function modifyFooter(footer) {
    footer.insertAdjacentHTML("beforeend", '<td class="ac-predictor-standings-elem" colspan="2">-</td>');
}
class StandingsTableView {
    element;
    provider;
    constructor(element, resultDataProvider) {
        this.element = element;
        this.provider = resultDataProvider;
        this.initHandler();
    }
    update() {
        this.removeOldElement();
        const header = this.element.querySelector("thead tr");
        if (!header)
            console.warn("header element not found", this.element);
        else
            modifyHeader(header);
        this.element.querySelectorAll("tbody tr").forEach((row) => {
            if (isFooter(row))
                modifyFooter(row);
            else
                modifyStandingsRow(row, this.provider);
        });
    }
    removeOldElement() {
        this.element.querySelectorAll(".ac-predictor-standings-elem").forEach((elem) => elem.remove());
    }
    initHandler() {
        new MutationObserver(() => this.update()).observe(this.element.tBodies[0], {
            childList: true,
        });
    }
    static Get(resultDataProvider) {
        const tableElem = document.querySelector(".table-responsive table");
        return new StandingsTableView(tableElem, resultDataProvider);
    }
}

class StandingsPageController {
    contestDetails;
    performanceProvider;
    ratingProvider;
    oldRatings = new Map();
    isRatedMaps = new Map();
    standingsTableView;
    async register() {
        const loading = StandingsLoadingView.Get();
        loading.onLoad(() => this.initialize());
    }
    async initialize() {
        const contestScreenName = getContestScreenName();
        const contestDetailsList = await getContestDetails();
        const contestDetails = contestDetailsList.find(details => details.contestScreenName == contestScreenName);
        if (contestDetails === undefined) {
            throw new Error("contest details not found");
        }
        this.contestDetails = contestDetails;
        if (this.contestDetails.beforeContest(new Date()))
            return;
        if (getConfig("hideDuringContest") && this.contestDetails.duringContest(new Date()))
            return;
        const standings = await getStandings(this.contestDetails.contestScreenName);
        if (getConfig("hideUntilFixed") && !standings.data.Fixed)
            return;
        this.standingsTableView = StandingsTableView.Get(async (userScreenName) => {
            if (!this.ratingProvider)
                return { "type": "error", "message": "ratingProvider missing" };
            if (!this.performanceProvider)
                return { "type": "error", "message": "performanceProvider missing" };
            if (!this.isRatedMaps)
                return { "type": "error", "message": "isRatedMapping missing" };
            if (!this.oldRatings)
                return { "type": "error", "message": "oldRatings missing" };
            if (!this.oldRatings.has(userScreenName))
                return { "type": "error", "message": `oldRating not found for ${userScreenName}` };
            const oldRating = this.oldRatings.get(userScreenName);
            if (!this.performanceProvider.availableFor(userScreenName))
                return { "type": "error", "message": `performance not available for ${userScreenName}` };
            const originalPerformance = this.performanceProvider.getPerformance(userScreenName);
            const positivizedPerformance = Math.round(positivizeRating(originalPerformance));
            if (this.isRatedMaps.get(userScreenName)) {
                if (!this.ratingProvider.provider.availableFor(userScreenName))
                    return { "type": "error", "message": `rating not available for ${userScreenName}` };
                if (this.ratingProvider.lazy) {
                    const newRatingCalculator = () => this.ratingProvider.provider.getRating(userScreenName, originalPerformance);
                    return { type: "deffered", oldRating, performance: positivizedPerformance, newRatingCalculator };
                }
                else {
                    const newRating = await this.ratingProvider.provider.getRating(userScreenName, originalPerformance);
                    return { type: "rated", oldRating, performance: positivizedPerformance, newRating };
                }
            }
            else {
                return { type: "unrated", oldRating, performance: positivizedPerformance };
            }
        });
        await this.updateData();
        this.standingsTableView.update();
    }
    async updateData() {
        if (!this.contestDetails)
            throw new Error("contestDetails missing");
        const standings = await getStandings(this.contestDetails.contestScreenName);
        let basePerformanceProvider = undefined;
        if (standings.data.Fixed && getConfig("useResults")) {
            try {
                const results = await getResults(this.contestDetails.contestScreenName);
                if (results.data.length === 0) {
                    throw new Error("results missing");
                }
                basePerformanceProvider = new FixedPerformanceProvider(results.toPerformanceMaps());
                this.isRatedMaps = results.toIsRatedMaps();
                this.oldRatings = results.toOldRatingMaps();
                this.ratingProvider = { provider: new ConstRatingProvider(results.toNewRatingMaps()), lazy: false };
            }
            catch (e) {
                console.warn("getResults failed", e);
            }
        }
        if (basePerformanceProvider === undefined) {
            const aperfsDict = await getAPerfs(this.contestDetails.contestScreenName);
            const defaultAPerf = this.contestDetails.defaultAPerf;
            const normalizedRanks = normalizeRank(standings.toRanks(true));
            const aperfsList = standings.toRatedUsers().map(user => hasOwnProperty(aperfsDict, user) ? aperfsDict[user] : defaultAPerf);
            basePerformanceProvider = new EloPerformanceProvider(normalizedRanks, aperfsList, this.contestDetails.performanceCap);
            this.isRatedMaps = standings.toIsRatedMaps();
            this.oldRatings = standings.toOldRatingMaps();
            if (this.contestDetails.contestType == "algorithm") {
                this.ratingProvider = { provider: new IncrementalAlgRatingProvider(standings.toOldRatingMaps(true), standings.toCompetitionMaps()), lazy: false };
            }
            else {
                this.ratingProvider = {
                    provider: new FromHistoryHeuristicRatingProvider(async (userScreenName) => {
                        const histories = await getHistory(userScreenName, "heuristic");
                        histories.data = histories.data.filter(x => new Date(x.EndTime) < this.contestDetails.endTime);
                        return histories.toPerformances();
                    }),
                    lazy: true
                };
            }
        }
        this.performanceProvider = new InterpolatePerformanceProvider(standings.toRanks(), basePerformanceProvider);
    }
}

class StandingsWrapper {
    data;
    constructor(data) {
        this.data = data;
    }
    toRanks(onlyRated = false) {
        const res = new Map();
        for (const data of this.data.StandingsData) {
            if (onlyRated && (!data.IsRated || data.Additional["standings.virtualElapsed"] !== -2))
                continue;
            const userScreenName = data.Additional["standings.virtualElapsed"] === -2 ? `ghost:${data.UserScreenName}` : data.UserScreenName;
            res.set(userScreenName, data.Rank);
        }
        return res;
    }
    toRatedUsers() {
        const res = [];
        for (const data of this.data.StandingsData) {
            if (data.IsRated && data.Additional["standings.virtualElapsed"] === -2) {
                res.push(data.UserScreenName);
            }
        }
        return res;
    }
    toScores() {
        const res = new Map();
        for (const data of this.data.StandingsData) {
            const userScreenName = data.Additional["standings.virtualElapsed"] === -2 ? `ghost:${data.UserScreenName}` : data.UserScreenName;
            res.set(userScreenName, { score: data.TotalResult.Score, penalty: data.TotalResult.Elapsed });
        }
        return res;
    }
}
function createCacheKey(contestScreenName, showGhost) {
    return `${contestScreenName}:${showGhost}`;
}
const STANDINGS_CACHE_DURATION = 10 * 1000;
const cache = new Cache(STANDINGS_CACHE_DURATION);
async function getVirtualStandings(contestScreenName, showGhost) {
    const cacheKey = createCacheKey(contestScreenName, showGhost);
    if (!cache.has(cacheKey)) {
        const result = await fetch(`https://atcoder.jp/contests/${contestScreenName}/standings/virtual/json${showGhost ? "?showGhost=true" : ""}`);
        if (!result.ok) {
            throw new Error(`Failed to fetch standings: ${result.status}`);
        }
        cache.set(cacheKey, await result.json());
    }
    return new StandingsWrapper(cache.get(cacheKey));
}
addHandler((content, path) => {
    const match = path.match(/^\/contests\/([^/]*)\/standings\/virtual\/json(\?showGhost=true)?$/);
    if (!match)
        return;
    const contestScreenName = match[1];
    const showGhost = match[2] != "";
    cache.set(createCacheKey(contestScreenName, showGhost), JSON.parse(content));
});

function isVirtualStandingsPage() {
    return /^\/contests\/[^/]*\/standings\/virtual\/?$/.test(document.location.pathname);
}

function duringVirtualParticipation() {
    if (!isVirtualStandingsPage()) {
        throw new Error("not available in this page");
    }
    const timerText = document.getElementById("virtual-timer")?.textContent ?? "";
    if (timerText && !timerText.includes("終了") && !timerText.includes("over"))
        return true;
    else
        return false;
}

function forgeCombinedRanks(a, b) {
    const res = new Map();
    const merged = [...a.entries(), ...b.entries()].sort((a, b) => a[1].score !== b[1].score ? b[1].score - a[1].score : a[1].penalty - b[1].penalty);
    let rank = 0;
    let prevScore = NaN;
    let prevPenalty = NaN;
    for (const [userScreenName, { score, penalty }] of merged) {
        if (score !== prevScore || penalty !== prevPenalty) {
            rank++;
            prevScore = score;
            prevPenalty = penalty;
        }
        res.set(userScreenName, rank);
    }
    return res;
}
function remapKey(map, mappingFunction) {
    const newMap = new Map();
    for (const [key, val] of map) {
        newMap.set(mappingFunction(key), val);
    }
    return newMap;
}
class VirtualStandingsPageController {
    contestDetails;
    performanceProvider;
    standingsTableView;
    async register() {
        const loading = StandingsLoadingView.Get();
        loading.onLoad(() => this.initialize());
    }
    async initialize() {
        const contestScreenName = getContestScreenName();
        const contestDetailsList = await getContestDetails();
        const contestDetails = contestDetailsList.find(details => details.contestScreenName == contestScreenName);
        if (contestDetails === undefined) {
            throw new Error("contest details not found");
        }
        this.contestDetails = contestDetails;
        this.standingsTableView = StandingsTableView.Get(async (userScreenName) => {
            if (!this.performanceProvider)
                return { "type": "error", "message": "performanceProvider missing" };
            if (!this.performanceProvider.availableFor(userScreenName))
                return { "type": "error", "message": `performance not available for ${userScreenName}` };
            const originalPerformance = this.performanceProvider.getPerformance(userScreenName);
            const positivizedPerformance = Math.round(positivizeRating(originalPerformance));
            return { type: "perfonly", performance: positivizedPerformance };
        });
        await this.updateData();
        this.standingsTableView.update();
    }
    async updateData() {
        if (!this.contestDetails)
            throw new Error("contestDetails missing");
        const virtualStandings = await getVirtualStandings(this.contestDetails.contestScreenName, true);
        const results = await getResults(this.contestDetails.contestScreenName);
        let ranks;
        let basePerformanceProvider;
        if (!duringVirtualParticipation() || getConfig("useFinalResultOnVirtual")) {
            const standings = await getStandings(this.contestDetails.contestScreenName);
            const referencePerformanceMap = remapKey(results.toPerformanceMaps(), userScreenName => `reference:${userScreenName}`);
            basePerformanceProvider = new FixedPerformanceProvider(referencePerformanceMap);
            ranks = forgeCombinedRanks(remapKey(standings.toScores(), userScreenName => `reference:${userScreenName}`), virtualStandings.toScores());
        }
        else {
            const aperfsObj = await getAPerfs(this.contestDetails.contestScreenName);
            const defaultAPerf = this.contestDetails.defaultAPerf;
            const normalizedRanks = normalizeRank(virtualStandings.toRanks(true));
            const aperfsList = virtualStandings.toRatedUsers().map(userScreenName => hasOwnProperty(aperfsObj, userScreenName) ? aperfsObj[userScreenName] : defaultAPerf);
            basePerformanceProvider = new EloPerformanceProvider(normalizedRanks, aperfsList, this.contestDetails.performanceCap);
            ranks = virtualStandings.toRanks();
        }
        this.performanceProvider = new InterpolatePerformanceProvider(ranks, basePerformanceProvider);
    }
}

function isStandingsPage() {
    return /^\/contests\/[^/]*\/standings\/?$/.test(document.location.pathname);
}

{
    const controller = new ConfigController();
    controller.register();
    add();
}
if (isStandingsPage()) {
    const controller = new StandingsPageController();
    controller.register();
}
if (isVirtualStandingsPage()) {
    const controller = new VirtualStandingsPageController();
    controller.register();
}