ORAC Extension

Combined ORAC Leaderboard Tracker, Personal Tab, custom tags, hidden problems, difficulty approximation, searching upgrade, custom styling & ordering, editorials. More information (and issue tracking/new features, orac solutions and lots more) at https://github.com/aperson31415/informatics

Terá de instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

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

Terá de instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Terá de instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Terá de instalar uma extensão como Tampermonkey para instalar este script.

Terá de instalar uma extensão de gestão de scripts de utilizador para instalar este script.

(Já tenho um gestor de scripts de utilizador, deixe-me instalá-lo!)

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

(Já tenho um gestor de estilos de utilizador, deixe-me instalá-lo!)

// ==UserScript==
// @name         ORAC Extension
// @namespace    http://tampermonkey.net/
// @version      5.5.3
// @description  Combined ORAC Leaderboard Tracker, Personal Tab, custom tags, hidden problems, difficulty approximation, searching upgrade, custom styling & ordering, editorials. More information (and issue tracking/new features, orac solutions and lots more) at https://github.com/aperson31415/informatics
// @author       a_person31415
// @match        https://orac2.info/hub/*
// @match        https://orac2.info/hub/personal/*
// @match        https://orac.amt.edu.au/hub/personal/*
// @match        https://orac2.info/problem/*
// @match        https://orac.amt.edu.au/problem/*
// @match        https://orac2.info/*
// @match        https://orac.amt.edu.au/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=orac2.info
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_listValues
// @grant        GM_deleteValue
// @grant        unsafeWindow
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(async function() {
    'use strict';

    async function requireScript(url) {
        const scriptText = await new Promise((resolve, reject) => {
            GM.xmlHttpRequest({
                method: "GET",
                url: url,
                onload: (res) => resolve(res.responseText),
                onerror: (err) => reject(err)
            });
        });

        const script = document.createElement('script');
        script.textContent = scriptText;
        document.head.appendChild(script);
    }

    await requireScript('https://cdn.jsdelivr.net/npm/chart.js');

    /* ============================================================
       FIRST SCRIPT
       ORAC Leaderboard Tracker & Personal Tab
       ============================================================ */

    const currentYear = new Date().getFullYear();
    const currentMonth = new Date().getMonth();
    const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

    function getISOWeek(date) {
        const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
        d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7));
        const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
        const weekNo = Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
        return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`;
    }

    function getMonthYearKey(date) {
        return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
    }

    function formatMonthYearLabel(yearMonthStr) {
        const parts = yearMonthStr.split('-');
        const year = parseInt(parts[0], 10);
        const month = parseInt(parts[1], 10) - 1;
        return `${monthNames[month]} ${year}`;
    }

    function getEmptyStats() {
        return {
            allTime: new Set(),
            thisYear: new Set(),
            thisMonth: new Set(),
            thisWeek: new Set(),
            solvedPerMonth: {},
            monthlySolves: {}
        };
    }

    function getUsername() {
        const userEl = document.querySelector(".username-field.nav-link");
        if (!userEl) return "";

        // Cache the real username before updateProfileElo modifies it.
        if (!userEl.dataset.oracOriginalUsername) {
            const strong = userEl.querySelector("strong");

            userEl.dataset.oracOriginalUsername =
                (strong || userEl).textContent
                .trim()
                .replace(/^@/, '')
                .replace(/\s*\([^)]*\)\s*$/, '');
        }

        return userEl.dataset.oracOriginalUsername;
    }


    function injectGridStyleFix() {
        if (document.getElementById('orac-grid-fix-style')) return;
        const style = document.createElement('style');
        style.id = 'orac-grid-fix-style';
        style.textContent = `
            .leaderboard-grid {
                padding: 0;
                margin: 0;
                width: 100%;
                display: grid;
                list-style: none;
                grid-auto-flow: column;
                grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
                grid-gap: 15px;
                grid-template-rows: repeat(11, 1fr) !important;
            }
        `;
        document.head.appendChild(style);
    }

    class LeaderboardScraper {
        constructor() {
            this.stats = getEmptyStats();
            this.now = new Date();
            this.currentIsoWeek = getISOWeek(this.now);
            this.page = 1;
            this.firstPageHtml = null;
        }

        async scrape(onUpdate) {
            while (this.page < 200) {
                try {
                    const response = await fetch(`/hub/allsubs/${this.page}`);
                    if (!response.ok) break;

                    const text = await response.text();
                    if (this.page > 1 && text === this.firstPageHtml) break;
                    if (this.page === 1) this.firstPageHtml = text;

                    const parser = new DOMParser();
                    const doc = parser.parseFromString(text, 'text/html');
                    const rows = doc.querySelectorAll('table.table tbody tr');
                    if (rows.length === 0) break;

                    rows.forEach(row => this.processRow(row));

                    this.rebuildMonthlySolves();

                    if (onUpdate) {
                        onUpdate({
                            allTime: this.stats.allTime.size,
                            thisYear: this.stats.thisYear.size,
                            thisMonth: this.stats.thisMonth.size,
                            thisWeek: this.stats.thisWeek.size
                        });
                    }

                    this.page++;
                    await new Promise(r => setTimeout(r, 100));
                } catch (err) {
                    console.error("ORAC Tracker Error:", err);
                    break;
                }
            }

            return this.stats;
        }

        processRow(row) {
            const probLink = row.querySelector('td a[href^="/problem/"]');
            const timeEl = row.querySelector('time');
            const scoreSpan = row.querySelector('span.badge');

            if (!probLink || !timeEl || !scoreSpan) return;

            const problemSlug = probLink.getAttribute('href');
            const score = parseInt(scoreSpan.textContent.trim(), 10);
            const timestamp = new Date(timeEl.getAttribute('datetime'));

            if (score !== 100) return;

            if (!this.stats.allTime.has(problemSlug)) {
                this.stats.allTime.add(problemSlug);

                if (timestamp.getFullYear() === currentYear) {
                    this.stats.thisYear.add(problemSlug);
                    if (timestamp.getMonth() === currentMonth)
                        this.stats.thisMonth.add(problemSlug);
                }

                if (getISOWeek(timestamp) === this.currentIsoWeek)
                    this.stats.thisWeek.add(problemSlug);
            }

            if (!this.stats.solvedPerMonth[problemSlug] ||
                timestamp < new Date(this.stats.solvedPerMonth[problemSlug])) {
                this.stats.solvedPerMonth[problemSlug] = timestamp.toISOString();
            }
        }

        rebuildMonthlySolves() {
            this.stats.monthlySolves = {};

            for (const [slug, dateStr] of Object.entries(this.stats.solvedPerMonth)) {
                const mKey = getMonthYearKey(new Date(dateStr));
                this.stats.monthlySolves[mKey] =
                    (this.stats.monthlySolves[mKey] || 0) + 1;
            }
        }

        static percentile(values, percentile) {
            if (!values.length) return 0;

            const index = (values.length - 1) * percentile;
            const lower = Math.floor(index);
            const upper = Math.ceil(index);

            if (lower === upper) return values[lower];

            return values[lower] +
                (values[upper] - values[lower]) * (index - lower);
        }

        async calculateOverallElo() {
            const ratings = [];

            for (const slug of this.stats.allTime) {
                try {
                    const url = new URL(slug, location.origin).href;
                    const solves = await cachedSolveCount(url);
                    const value = Number(solves);

                    if (!Number.isFinite(value)) continue;

                    const elo = Math.max(
                        2800 * (1 - Math.pow(Math.max(value, 0) / 1737, 0.28)),
                        0
                    );

                    if (Number.isFinite(elo))
                        ratings.push(elo);
                } catch {}
            }

            ratings.sort((a, b) => a - b);

            if (!ratings.length) return null;

            const q1 = LeaderboardScraper.percentile(ratings, 0.25);
            const q3 = LeaderboardScraper.percentile(ratings, 0.75);
            const iqr = q3 - q1;
            const finalElo = q3;

            return {
                elo: finalElo,
                q1,
                q3,
                iqr,
                count: ratings.length
            };
        }
    }

    function createEloDisplay(elo) {
        const value = Math.round(elo).toString();
        const wrapper = document.createElement("span");

        if (elo > 3000) {
            const first = document.createElement("span");
            first.style.color = "#000";
            first.textContent = value.charAt(0);

            const rest = document.createElement("span");
            rest.style.color = "#f00";
            rest.textContent = value.slice(1);

            wrapper.append(first, rest);
        } else {
            wrapper.style.color = getEloColor(elo);
            wrapper.textContent = value;
        }

        return wrapper;
    }

    function updateProfileElo(result) {
        const userEl = document.querySelector(".username-field.nav-link");
        if (!userEl || !result) return;

        const strong = userEl.querySelector("strong");
        if (!strong) return;

        if (strong.dataset.oracEloUpdated === "true") return;
        strong.dataset.oracEloUpdated = "true";

        const username = strong.textContent.trim().replace(/\s*$/, "");
        strong.textContent = "";

        strong.append(
            document.createTextNode(username + " (")
        );

        const elo = createEloDisplay(result.elo);
        strong.append(elo);
        strong.append(")")
    }

    async function init() {
        if (window.oracTrackerInitialized) return;
        window.oracTrackerInitialized = true;

        injectGridStyleFix();

        let statsData = sessionStorage.getItem('orac_tracker_data_v2');
        let parsedData = statsData ? JSON.parse(statsData) : null;

        let stats;
        let isCached = !!parsedData;

        if (isCached) {
            stats = {
                allTime: new Set(parsedData.allTime),
                thisYear: new Set(parsedData.thisYear),
                thisMonth: new Set(parsedData.thisMonth),
                thisWeek: new Set(parsedData.thisWeek),
                solvedPerMonth: parsedData.solvedPerMonth || {},
                monthlySolves: parsedData.monthlySolves || {}
            };
        } else {
            stats = getEmptyStats();
        }

        if (window.location.pathname.includes('/hub/leaderboards'))
            setupLeaderboardInjections(stats);

        setupCustomTab(stats, !isCached);

        if (!isCached) {
            const scraper = new LeaderboardScraper();

            const freshStats = await scraper.scrape((counts) => {
                updateLiveUI(counts);
            });

            sessionStorage.setItem('orac_tracker_data_v2', JSON.stringify({
                allTime: Array.from(freshStats.allTime),
                thisYear: Array.from(freshStats.thisYear),
                thisMonth: Array.from(freshStats.thisMonth),
                thisWeek: Array.from(freshStats.thisWeek),
                solvedPerMonth: freshStats.solvedPerMonth,
                monthlySolves: freshStats.monthlySolves
            }));

            updateLiveUI({
                allTime: freshStats.allTime.size,
                thisYear: freshStats.thisYear.size,
                thisMonth: freshStats.thisMonth.size,
                thisWeek: freshStats.thisWeek.size
            });

            window.oracFreshStats = freshStats;

            const overallElo = await scraper.calculateOverallElo();
            if (overallElo) {
                window.oracOverallElo = overallElo;
                updateProfileElo(overallElo);
            }
        } else {
            window.oracFreshStats = stats;

            const scraper = new LeaderboardScraper();
            scraper.stats = stats;

            const overallElo = await scraper.calculateOverallElo();
            if (overallElo) {
                window.oracOverallElo = overallElo;
                updateProfileElo(overallElo);
            }
        }
    }

    function updateLiveUI(counts) {
        const elAll = document.getElementById('orac-stat-all');
        const elYear = document.getElementById('orac-stat-year');
        const elMonth = document.getElementById('orac-stat-month');
        const elWeek = document.getElementById('orac-stat-week');

        if (elAll) elAll.textContent = counts.allTime;
        if (elYear) elYear.textContent = counts.thisYear;
        if (elMonth) elMonth.textContent = counts.thisMonth;
        if (elWeek) elWeek.textContent = counts.thisWeek;
    }

    function setupLeaderboardInjections(stats) {
        const tryAppend = () => {
            const currentStats = window.oracFreshStats || stats;
            appendUserToLeaderboards(currentStats);
        };

        tryAppend();
        setTimeout(tryAppend, 500);
        setTimeout(tryAppend, 1500);

        const observer = new MutationObserver((mutations) => {
            if (!mutations.some(m => m.addedNodes.length > 0)) return;

            observer.disconnect();

            try {
                tryAppend();
            } finally {
                observer.observe(targetContainer, {
                    childList: true,
                    subtree: true
                });
            }
        });

        const targetContainer =
            document.querySelector('#leaderboard-tab-content') || document.body;

        observer.observe(targetContainer, { childList: true, subtree: true });

        const navTabs = document.querySelector('#leaderboard-tabs');
        if (navTabs) {
            navTabs.addEventListener('click', () => {
                setTimeout(tryAppend, 200);
            });
        }
    }

    function appendUserToLeaderboards(stats) {
        const username = getUsername();
        if (!username) return;

        const gridLists = document.querySelectorAll('ul.leaderboard-grid');
        if (gridLists.length === 0) return;

        gridLists.forEach(gridList => {
            const parentPane = gridList.closest('.tab-pane');
            const isOverallTab = parentPane && parentPane.id === 'overall';

            // ------------------------------------------------------------
            // Overall leaderboard
            // ------------------------------------------------------------
            if (isOverallTab) {
                // Check BOTH the site's existing username and our injected one.
                const alreadyExists =
                      [...gridList.querySelectorAll('.username-field')]
                .some(el => el.textContent.trim() === username) ||
                      gridList.querySelector(
                          `li[data-injected-user="${CSS.escape(username)}"]`
                      );

                if (alreadyExists) return;

                const newLi = document.createElement('li');

                newLi.dataset.injectedUser = username;
                newLi.style.backgroundColor = 'rgba(0, 123, 255, 0.1)';

                newLi.innerHTML = `
                <span class="place" title="Rank">?</span>
                <span class="username-field">${username}</span>
                <span class="solvecount" title="Solved problems">
                    ${stats.allTime.size}
                </span>
            `;

                gridList.appendChild(newLi);
                return;
            }

            // ------------------------------------------------------------
            // Week / Month / Year leaderboards
            // ------------------------------------------------------------
            const headers = gridList.querySelectorAll('h3');

            headers.forEach(h3 => {
                const title = h3.textContent.trim().toLowerCase();

                let count;

                if (title.includes('week')) {
                    count = stats.thisWeek.size;
                } else if (title.includes('month')) {
                    count = stats.thisMonth.size;
                } else if (title.includes('year')) {
                    count = stats.thisYear.size;
                } else if (
                    title.includes('overall') ||
                    title.includes('all-time')
                ) {
                    count = stats.allTime.size;
                } else {
                    return;
                }

                // Find the actual section belonging to this heading.
                let currentNode = h3.nextElementSibling;
                const sectionNodes = [];

                while (
                    currentNode &&
                    currentNode.tagName !== 'H3'
                ) {
                    sectionNodes.push(currentNode);
                    currentNode = currentNode.nextElementSibling;
                }

                // IMPORTANT:
                // Look for an injected user anywhere in this section,
                // not just on the last <li>.
                const alreadyInjected = sectionNodes.some(node =>
                                                          node.querySelector?.(
                    `li[data-injected-user="${CSS.escape(username)}"]`
                )
                                                         );

                const alreadyNative = sectionNodes.some(node =>
                                                        [...(node.querySelectorAll?.('.username-field') || [])]
                                                        .some(el => el.textContent.trim() === username)
                                                       );

                if (alreadyInjected || alreadyNative) return;

                // Find the last <li> in this section.
                let lastLi = null;

                sectionNodes.forEach(node => {
                    if (node.tagName === 'LI') {
                        lastLi = node;
                    }

                    node.querySelectorAll?.('li').forEach(li => {
                        lastLi = li;
                    });
                });

                const newLi = document.createElement('li');

                newLi.dataset.injectedUser = username;
                newLi.style.backgroundColor = 'rgba(0, 123, 255, 0.1)';

                newLi.innerHTML = `
                <span class="place" title="Rank">?</span>
                <span class="username-field">${username}</span>
                <span class="solvecount" title="Solved problems">
                    ${count}
                </span>
            `;

                if (lastLi) {
                    lastLi.after(newLi);
                } else {
                    h3.after(newLi);
                }
            });
        });
    }


    function setupCustomTab(stats, isLoading) {
        const navTabs = document.querySelector('#leaderboard-tabs');
        const tabContentContainer =
            document.querySelector('#leaderboard-tab-content');

        if (!navTabs || !tabContentContainer) return;
        if (document.getElementById('orac-me-tab')) return;

        const tabLi = document.createElement('li');
        tabLi.id = 'orac-me-tab';
        tabLi.className = 'nav-item';

        const tabLink = document.createElement('a');
        tabLink.className = 'nav-link';
        tabLink.id = 'me-tab';
        tabLink.href = '#me';
        tabLink.textContent = 'My Activity';

        tabLi.appendChild(tabLink);
        navTabs.appendChild(tabLi);

        const contentPane = document.createElement('div');
        contentPane.className = 'tab-pane fade';
        contentPane.id = 'me';

        contentPane.innerHTML = `
            <br>
            <div class="card mt-2 mb-4">
                <div class="card-header py-2"><strong>Solves</strong></div>
                <div class="card-body py-2">
                    <ul class="leaderboard-grid" style="list-style:none;padding:0;margin:0;display:flex;gap:15px;justify-content:space-around;">
                        <li><span class="username-field">All-Time:</span> <span class="solvecount" id="orac-stat-all">${isLoading ? '...' : stats.allTime.size}</span></li>
                        <li><span class="username-field">This Year:</span> <span class="solvecount" id="orac-stat-year">${isLoading ? '...' : stats.thisYear.size}</span></li>
                        <li><span class="username-field">This Month:</span> <span class="solvecount" id="orac-stat-month">${isLoading ? '...' : stats.thisMonth.size}</span></li>
                        <li><span class="username-field">This Week:</span> <span class="solvecount" id="orac-stat-week">${isLoading ? '...' : stats.thisWeek.size}</span></li>
                    </ul>
                </div>
            </div>
            <div class="card p-3">
                <div style="width:100%;max-width:700px;margin:0 auto;position:relative;">
                    <canvas id="meChart"></canvas>
                </div>
            </div>
        `;

        tabContentContainer.appendChild(contentPane);

        let meChartInstance = null;

        function renderChart() {
            const canvas = document.getElementById('meChart');
            if (!canvas || typeof Chart === 'undefined') return;

            const activeStats = window.oracFreshStats || stats;
            const monthlySolves = activeStats.monthlySolves || {};
            const keys = Object.keys(monthlySolves).sort();

            if (keys.length === 0) return;

            const startKey = keys[0];
            const endKey = keys[keys.length - 1];

            const allMonths = [];
            let [currY, currM] =
                startKey.split('-').map(x => parseInt(x, 10));

            const [endY, endM] =
                endKey.split('-').map(x => parseInt(x, 10));

            while (currY < endY || (currY === endY && currM <= endM)) {
                allMonths.push(`${currY}-${String(currM).padStart(2, '0')}`);
                currM++;

                if (currM > 12) {
                    currM = 1;
                    currY++;
                }
            }

            const labelsFormatted =
                allMonths.map(m => formatMonthYearLabel(m));

            const dataValues =
                allMonths.map(m => monthlySolves[m] || 0);

            if (meChartInstance)
                meChartInstance.destroy();

            meChartInstance = new Chart(canvas.getContext('2d'), {
                type: 'bar',
                data: {
                    labels: labelsFormatted,
                    datasets: [{
                        label: 'Solves per Month',
                        data: dataValues,
                        backgroundColor: '#28a745',
                        borderRadius: 3
                    }]
                },
                options: {
                    responsive: true,
                    scales: {
                        x: {
                            ticks: { font: { size: 10 } },
                            grid: { display: false }
                        },
                        y: {
                            ticks: { font: { size: 10 }, precision: 0 }
                        }
                    },
                    plugins: {
                        legend: { display: false },
                        title: {
                            display: false,
                            text: 'solves per month',
                            font: { size: 14, weight: 'bold' },
                            padding: { bottom: 15 }
                        }
                    }
                }
            });
        }

        tabLink.addEventListener('click', e => {
            e.preventDefault();

            Array.from(navTabs.querySelectorAll('.nav-link'))
                .forEach(l => l.classList.remove('active'));

            Array.from(tabContentContainer.querySelectorAll('.tab-pane'))
                .forEach(p => {
                    p.classList.remove('show', 'active');
                    p.style.display = 'none';
                });

            tabLink.classList.add('active');
            contentPane.classList.add('show', 'active');
            contentPane.style.display = 'block';

            const activeStats = window.oracFreshStats || stats;

            if (document.getElementById('orac-stat-all')) {
                document.getElementById('orac-stat-all').textContent =
                    activeStats.allTime.size;
                document.getElementById('orac-stat-year').textContent =
                    activeStats.thisYear.size;
                document.getElementById('orac-stat-month').textContent =
                    activeStats.thisMonth.size;
                document.getElementById('orac-stat-week').textContent =
                    activeStats.thisWeek.size;
            }

            setTimeout(renderChart, 50);
        });

        Array.from(navTabs.querySelectorAll('.nav-link:not(#me-tab)'))
            .forEach(nativeLink => {
                nativeLink.addEventListener('click', () => {
                    Array.from(navTabs.querySelectorAll('.nav-link'))
                        .forEach(l => l.classList.remove('active'));

                    nativeLink.classList.add('active');

                    Array.from(tabContentContainer.querySelectorAll('.tab-pane'))
                        .forEach(p => {
                            p.classList.remove('show', 'active');
                            p.style.display = 'none';
                        });

                    const targetPane =
                        document.querySelector(nativeLink.getAttribute('href'));

                    if (targetPane) {
                        targetPane.classList.add('show', 'active');
                        targetPane.style.display = 'block';
                    }

                    setTimeout(() =>
                        appendUserToLeaderboards(
                            window.oracFreshStats || stats
                        ), 100);
                });
            });
    }


    /* ============================================================
       SECOND SCRIPT
       ORAC Userscript Personal - bf5
       ============================================================ */

    const $ = (s, p = document) => {
        const els = p.querySelectorAll(s);
        return els.length === 1 ? els[0] : els;
    };

    const TAG_ORDER = [
        "starter", "training", "aic", "aio", "acio", "aiio", "alpha",
        "fario", "precamp", "camp", "seln", "apio", "ioi",
        "cpp-practice", "testing", "custom-tags"
    ];

    const NEW_TAGS = [
        "starter", "training", "aic", "acio", "cpp-practice", "custom-tags"
    ];

    const OLD_PROBLEMS = `
        aic00p1 aic00p2 aic00p3 aic00p4 aic01p1 aic01p2 aic01p3 aic01p4
        aic02p1 aic02p2 aic03p2 aic03p3 aic03p4 aic04p1 aic04p2
        aic04p3 aic04p4 aic04p5 aic98p1 aic98p2 aic98p3 aic99p1 aic99p2
        aic99p3 dscannons dsfenwickxor dsinversioncounting dslazyupdate
        dsrangetreeupdates dssupplies monthly05p1 monthly05p2 monthly05p3
        precamppublic2
    `.trim().split(/\s+/);

    const STARTER_SETS = [
        "starter", "starterset1", "starterset1challenge", "starterset2",
        "starterset2challenge", "starterset3", "starterset3challenge"
    ];

    const ACIO_SETS = ["acio25"];

    const ELO_TIERS = [
        [0.00, "#000000"],
        [800.00, "#808080"],
        [1000.00, "#A52A2A"],
        [1200.00, "#008000"],
        [1400.00, "#03A89E"],
        [1600.00, "#0000FF"],
        [1800.00, "#a0a"],
        [2000.00, "#bb0"],
        [2200.00, "#ffa200"],
        [2478.00, "#ff7b00"],
        [2600.00, "#ff0000"]
    ];

    const ALIASES = {
        py: "python", Py: "python", python: "python", Python: "python",
        "C++": "cpp", "c++": "cpp", txt: "plaintext", Txt: "plaintext",
        text: "plaintext", Text: "plaintext", Java: "java", java: "java"
    };

    GM_addStyle(`
        .badge-tag{margin-right:4px;cursor:pointer;background:#f8f9fa;color:#333;user-select:none}
        .badge-tag.selected{background:#d9534f!important;color:#fff!important;border-color:#d43f3a!important}
        .badge-tag:hover,.koolbutton:hover{opacity:.8}
        .progress-column,.difficulty-column{bottom-border:1px solid #dee2e6}
        .difficulty-column{width:30%}.progress-column{width:20%}
        .kooltable{border-spacing:0 10px;border-collapse:separate;margin-bottom:0!important}
        .koolbutton{border:none;border-radius:.5rem;outline:none;cursor:pointer;user-select:none}
        .koolbutton:focus{outline:none}
        .slider-container{display:flex;align-items:center;width:350px;height:40px}
        #rangevalue{font-size:14px;font-weight:bold;color:#4CAF50;white-space:nowrap;min-width:80px}
        .slider-track,.slider-range{position:absolute;height:5px;top:50%;transform:translateY(-50%);border-radius:5px}
        .slider-track{width:100%;background:#ddd;z-index:1}
        .slider-range{background:#4CAF50;z-index:2}
        input[type=range]{position:absolute;width:100%;height:30px;top:50%;left:0;margin:0;transform:translateY(-50%);pointer-events:none;-webkit-appearance:none;appearance:none;background:transparent;z-index:3}
        input[type=range]::-webkit-slider-runnable-track{height:5px;background:transparent;border:0}
        input[type=range]::-moz-range-track{height:5px;background:transparent;border:0}
        input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;pointer-events:auto;width:16px;height:16px;margin-top:-5px;border-radius:50%;background:#4CAF50;cursor:pointer;border:2px solid #fff;box-shadow:0 1px 3px #0004}
        input[type=range]::-moz-range-thumb{pointer-events:auto;width:16px;height:16px;border-radius:50%;background:#4CAF50;cursor:pointer;border:2px solid #fff;box-shadow:0 1px 3px #0004}
        input[type=range]:focus{outline:none}
        .custom-search{border-radius:.5rem;border:1px solid gray;margin-left:.25rem;height:25px}
        .custom-search:focus,.custom-search:hover{outline:1px solid #343a40}
        .problemset-display[data-visible=false]{height:0;padding:0;margin:0;overflow:hidden;border:none}
        tr[data-visible=false]{height:0!important;padding:0!important;margin:0!important;overflow:hidden!important;border:none;visibility:hidden}
        tr[data-visible=false] td{padding:0!important;margin:0!important;height:0!important;font-size:0!important;line-height:0!important;border:none!important}
    `);

    const waitFor = selector => new Promise(resolve => {
        const found = document.querySelector(selector);
        if (found) return resolve(found);

        const observer = new MutationObserver(() => {
            const el = document.querySelector(selector);

            if (el) {
                observer.disconnect();
                resolve(el);
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });
    });

    const tag = text => {
        const el = document.createElement("span");
        el.className = "badge badge-tag";
        el.innerText = text;
        return el;
    };

    const styleDetails = (name, content, id) => {
        const p = document.createElement("p");

        const button = Object.assign(document.createElement("a"), {
            className: "btn btn-primary collapsed",
            innerText: name
        });

        button.dataset.toggle = "collapse";
        button.href = "#" + id;
        button.role = "button";
        button.setAttribute("aria-expanded", "false");
        p.appendChild(button);

        const container = Object.assign(document.createElement("div"), {
            className: "collapse",
            id
        });

        const body = Object.assign(document.createElement("div"), {
            className: "card card-body",
            innerHTML: content
        });

        container.appendChild(body);

        const wrapper = document.createElement("span");
        wrapper.append(p, container);
        return wrapper;
    };

    const problemId = url => url.pathname.split("/").filter(Boolean).pop();

    async function fetchProblemData(id) {
        const res = await fetch(
            "https://raw.githubusercontent.com/aperson31415/informatics/refs/heads/main/ORAC%20Userscript/data.json"
        );

        const doc =
            new DOMParser().parseFromString(await res.text(), "text/html");

        const data = JSON.parse(doc.body.innerHTML);
        return data[id] ?? data[String(id)] ?? null;
    }

    const getEloColor = elo => {
        const value = Number(elo);
        if (!Number.isFinite(value)) return ELO_TIERS[0][1];

        for (const [threshold, color] of ELO_TIERS)
            if (value < threshold) return color;

        return ELO_TIERS[ELO_TIERS.length - 1][1];
    };

    const rating = solves => {
        const x = Number(solves);
        const safeSolves =
            Number.isFinite(x) && x >= 0 ? x : 0;

        const elo = Math.max(
            2800 * (1 - Math.pow(safeSolves / 1737, 0.28)),
            0
        );

        const elem = document.createElement("b");
        elem.style.color = getEloColor(elo);
        elem.innerText =
            `${Math.round(elo)} (${safeSolves} ${safeSolves === 1 ? "solve" : "solves"})`;

        return { elem, elo };
    };

    async function solveCount(url) {
        try {
            const res = await fetch(url + "/hof");
            const doc =
                new DOMParser().parseFromString(await res.text(), "text/html");

            if (doc.querySelector("#solversList"))
                return doc.querySelector("b")?.innerText || "0";

            return String([...doc.querySelectorAll(".solvecount")]
                .filter(e => e.innerText.trim() === "100").length);
        } catch {
            return "0";
        }
    }

    async function cachedSolveCount(url) {
        let value = GM_getValue("solves_" + url, -1);

        if (value == -1) {
            value = await solveCount(url);
            GM_setValue("solves_" + url, value);
        }

        return value;
    }

    class ProblemPage {
        constructor() {
            this.url = location.href;
            this.id = problemId(new URL(this.url));
        }

        init() {
            this.saveProblemMetadata();

            if ($("tbody")[1]) this.addStats();

            this.addNotes();

            if (location.href.includes("submissions"))
                this.addHints();
        }

        saveProblemMetadata() {
            const badge = $(".badge.hub-badge");
            if (!badge) return;

            GM_setValue("score_" + this.url, {
                score: badge.innerText.trim(),
                style: badge.className
            });

            const name = $("h1.mb-0")?.innerText.trim();
            if (name) GM_setValue("name_" + this.url, name);
        }

        addStats() {
            window.stats_bar = document.createElement("tr");
            window.bookmark_section = document.createElement("td");

            const diff = document.createElement("td");
            diff.style.textAlign = "right";

            $(".mb-3").classList.add("kooltable");

            this.statsRow = diff;

            window.stats_bar.append(window.bookmark_section, diff);
            $("tbody")[1].appendChild(window.stats_bar);

            cachedSolveCount(this.url).then(n => {
                const { elem } = rating(n);
                elem.classList.add("text-nowrap");
                diff.appendChild(elem);
            });

            this.renderTags();
        }

        renderTags() {
            if (!window.bookmark_section) return;

            const section = window.bookmark_section;
            section.innerText = "Custom Tags: ";

            const tags = GM_getValue("rev_tags", {})[this.url] || [];
            tags.forEach(t => section.appendChild(tag(t)));

            const plus = tag("+");
            const minus = tag("-");

            plus.classList.add("plus");
            minus.classList.add("minus");

            section.append(plus, minus);
        }

        addNotes() {
            const parts = location.pathname.split("/").filter(Boolean);
            if (parts.length !== 2) return;

            waitFor(".container-xl").then(() => {
                const containers = document.querySelectorAll(".container-xl");

                containers[2].insertAdjacentHTML("beforeend", `
                    <div id="notes"><h2 class="mt-5">Notes</h2>
                    Small: <input id="smallnote" type="text" placeholder="Nothing yet!" style="width:80%"><br><br>
                    Large:<br><textarea id="bignote" style="width:100%;height:301px" placeholder="Nothing yet!"></textarea></div>
                `);

                const small = $("#smallnote");
                const big = $("#bignote");

                small.value = GM_getValue("smallnote" + this.id, "");
                big.value = GM_getValue("bignote" + this.id, "");

                small.addEventListener("input", () =>
                    GM_setValue("smallnote" + this.id, small.value)
                );

                big.addEventListener("input", () =>
                    GM_setValue("bignote" + this.id, big.value)
                );
            });
        }

        async addHints() {
            const parent = document.querySelectorAll(".container-xl")[2];

            parent.appendChild(Object.assign(document.createElement("h2"), {
                className: "mt-5",
                innerText: "Hints"
            }));

            if (!document.querySelector(".table")) {
                parent.appendChild(Object.assign(document.createElement("p"), {
                    innerText: "Please attempt this problem before looking at hints"
                }));

                return;
            }

            const data = await fetchProblemData(parseInt(this.id));

            if (!data) {
                parent.appendChild(Object.assign(document.createElement("p"), {
                    innerText: "Sorry, no hints or solutions are available for this problem as of now. Try coming back to this problem later."
                }));

                return;
            }

            if (data.hints)
                Object.keys(data.hints).forEach(key => {
                    const content =
                        data.hints[key].map(p => `<p>${p}</p>`).join("");

                    const node = styleDetails(
                        key,
                        content,
                        "hint" + key.replace(" ", "koolspace")
                    );

                    node.appendChild(document.createElement("br"));
                    parent.appendChild(node);
                });

            if (data.solutions)
                Object.keys(data.solutions).forEach(lang => {
                    const id =
                        "customsol" + lang.replace(" ", "koolspace");

                    const code =
                        `<pre><code class="language-${ALIASES[lang]} hljs ${ALIASES[lang]}" id="${id}">${data.solutions[lang].join("\n")}</code></pre>`;

                    parent.appendChild(styleDetails(
                        lang + " solution",
                        code,
                        "sol" + lang.replace(" ", "koolspace")
                    ));

                    const el = document.getElementById(id);

                    hljs.highlightBlock(el);
                    hljs.lineNumbersBlock(el, {
                        singleLine: false,
                        startFrom: 1
                    });
                });

            document.querySelectorAll(".inline-code").forEach(el => {
                hljs.highlightBlock(el);
                hljs.lineNumbersBlock(el, {
                    singleLine: false,
                    startFrom: 1
                });
            });

            parent.appendChild(Object.assign(document.createElement("p"), {
                innerText: "Last updated on " + data.updated
            }));
        }
    }

    class TagManager {
        constructor() {
            this.url = location.href;
        }

        render() {
            if (!window.bookmark_section) return;

            const section = window.bookmark_section;
            section.innerText = "Custom Tags: ";

            (GM_getValue("rev_tags", {})[this.url] || [])
                .forEach(t => section.appendChild(tag(t)));

            const plus = tag("+");
            const minus = tag("-");

            plus.classList.add("plus");
            minus.classList.add("minus");

            section.append(plus, minus);
        }

        add() {
            const name = prompt("Name of tag to add to problem");
            if (!name) return;

            const tags = GM_getValue("tags", {});
            const reverse = GM_getValue("rev_tags", {});

            if (tags[name])
                tags[name].push(this.url);
            else if (confirm(`Create new tag '${name}'?`))
                tags[name] = [this.url];
            else
                return;

            (reverse[this.url] ||= []).push(name);

            GM_setValue("tags", tags);
            GM_setValue("rev_tags", reverse);

            this.render();
        }

        remove() {
            const name = prompt("Name of tag to delete from problem");
            if (!name) return;

            const tags = GM_getValue("tags", {});
            const reverse = GM_getValue("rev_tags", {});

            if (!tags[name]?.includes(this.url)) return;

            tags[name] =
                tags[name].filter(url => url !== this.url);

            if (!tags[name].length && confirm(`Fully remove tag '${name}'?`))
                delete tags[name];

            reverse[this.url] =
                reverse[this.url].filter(t => t !== name);

            GM_setValue("tags", tags);
            GM_setValue("rev_tags", reverse);

            this.render();
        }
    }

    class Restorer {
        static async run() {
            if (!location.href.includes("problem") ||
                location.href.includes("hof") ||
                location.href.includes("submission"))
                return;

            const id = problemId(new URL(location.href));
            const h1 = $("h1");
            const heading = h1?.innerText.toLowerCase().trim() || "";

            const error = [
                "access denied",
                "page requested does not exist",
                "page not found"
            ].some(x => heading.includes(x));

            if (!OLD_PROBLEMS.includes(id) || !error) return;

            document.body.innerHTML = `
                <div style="text-align:center;margin-top:100px;font-family:sans-serif">
                    <h2>Restoring ${id}...</h2>
                </div>`;

            const base =
                `https://cdn.jsdelivr.net/gh/aperson31415/informatics@main/wayback_raw/${id}`;

            const nav = `
                <div style="background:#1a1a1a;color:white;padding:12px 20px;font-family:sans-serif;display:flex;justify-content:space-between;align-items:center;border-bottom:2px solid #3498db;position:relative;z-index:9999">
                    <span>Restored Problem: <strong style="color:#3498db">${id}</strong></span>
                    <a href="https://orac2.info/hub/personal" style="color:white;text-decoration:none;background:#3498db;padding:5px 12px;border-radius:4px;font-weight:bold">← Hub</a>
                </div>`;

            const css = [
                "https://github.com/aperson31415/informatics/blob/main/ORAC%20Userscript/ben-aioc.css",
                "https://github.com/aperson31415/informatics/blob/main/ORAC%20Userscript/bootstrap_min.css",
                "https://github.com/aperson31415/informatics/blob/main/ORAC%20Userscript/bootstrap_responsive.css"
            ];

            try {
                const html = await fetch(`${base}.html`);

                if (html.ok) {
                    const text = await html.text();

                    if (!text.includes("404: Not Found")) {
                        document.open();
                        document.write(nav + text);
                        document.close();

                        Restorer.styles(css);
                        return;
                    }
                }

                const pdf = await fetch(`${base}.pdf`);

                if (pdf.ok && (await pdf.blob()).size > 500) {
                    document.body.innerHTML = `
                        <style>
                            html,body{margin:0;padding:0;height:100%;overflow:hidden;display:flex;flex-direction:column}
                        </style>
                        ${nav}
                        <embed src="${base}.pdf" type="application/pdf" style="flex-grow:1;width:100%">
                    `;

                    Restorer.styles(css);
                    return;
                }

                document.body.innerHTML =
                    nav +
                    `<h1 style="text-align:center;margin-top:50px">Backup not found.</h1>`;
            } catch {
                document.body.innerHTML =
                    nav +
                    `<h1 style="text-align:center;margin-top:50px">Fetch error.</h1>`;
            }
        }

        static styles(urls) {
            urls.forEach(href => {
                const link = Object.assign(document.createElement("link"), {
                    rel: "stylesheet",
                    type: "text/css",
                    href
                });

                document.head.appendChild(link);
            });
        }
    }

    class PersonalPage {
        constructor() {
            this.library = this.getBaseData();
        }

        async getBadge(url) {
            let entry = GM_getValue("score_" + url, null);

            if (entry && typeof entry === "object")
                return entry;

            try {
                const res = await fetch(url);

                const doc = new DOMParser().parseFromString(
                    await res.text(),
                    "text/html"
                );

                const el = doc.querySelector(".badge.hub-badge");

                entry = el ? {
                    score: el.innerText.trim(),
                    style: el.className
                } : {
                    score: "0",
                    style: "badge hub-badge badge-no-submission"
                };

                GM_setValue("score_" + url, entry);
            } catch {
                entry = {
                    score: "0",
                    style: "badge hub-badge badge-no-submission"
                };
            }

            return entry;
        }

        getBaseData() {
            const p = (name, link, old = false) =>
                ({ problem_name: name, problem_link: link, old });

            const s = (tag, set_name, set_id, problems, old = false) =>
                ({ tag, set_name, set_id, old, problems });

            return {
                sets: [
                    s("training", "Implementation Problems", "impl", [
                        p("Bernard's Magic Needles", "https://orac2.info/problem/1106/"),
                        p("A Not So Simple Sort", "https://orac2.info/problem/1105/"),
                        p("Pie a la Mode", "https://orac2.info/problem/1111/"),
                        p("Stacks", "https://orac2.info/problem/745/"),
                        p("Queues", "https://orac2.info/problem/752/"),
                        p("Bracket Matching", "https://orac2.info/problem/1107/"),
                        p("Twin Primes", "https://orac2.info/problem/872/"),
                        p("Pairs", "https://orac2.info/problem/947/"),
                        p("Swapsies", "https://orac2.info/problem/1104/")
                    ]),

                    s("training", "Graph Problems", "graph", [
                        p("Adjacency Lists", "https://orac2.info/problem/1108"),
                        p("Quicksort", "https://orac2.info/problem/659/"),
                        p("Gossip Chains", "https://orac2.info/problem/738/"),
                        p("King Arthur", "https://orac2.info/problem/978/")
                    ]),

                    s("training", "Bitmask Problems", "bitmask", [
                        p("Tiling", "https://orac2.info/problem/935/"),
                        p("Odd Jobs", "https://orac2.info/problem/951/"),
                        p("King Arthur", "https://orac2.info/problem/978/")
                    ]),

                    s("cpp-practice", "C++ Starter Problems", "cpp-starter", [
                        p("Addition (C++ Only)", "https://orac2.info/problem/1309/"),
                        p("Counting to Infinity (C++ Only)", "https://orac2.info/problem/1307/"),
                        p("Triple Hunting (C++ Only)", "https://orac2.info/problem/1306/"),
                        p("Sitting or Standing (C++ Only)", "https://orac2.info/problem/1308/"),
                        p("A Dish Best served Cold (C++ Only)", "https://orac2.info/problem/1310/")
                    ]),

                    s("aic", "AIC 1998", "aic98", [
                        p("Shopping Malls", "https://orac2.info/problem/aic98p1", true),
                        p("Anagram Solver", "https://orac2.info/problem/aic98p2", true),
                        p("Building Integers", "https://orac2.info/problem/aic98p3", true)
                    ], true),

                    s("aic", "AIC 1999", "aic99", [
                        p("Hailstone Sequences", "https://orac2.info/problem/aic99p1", true),
                        p("Hunt-a-Word", "https://orac2.info/problem/aic99p2", true),
                        p("Cartography", "https://orac2.info/problem/aic99p3", true)
                    ], true),

                    s("aic", "AIC 2000", "aic00", [
                        p("Academic Espionage", "https://orac2.info/problem/aic00p1", true),
                        p("Keeping Secret", "https://orac2.info/problem/aic00p2", true),
                        p("Analysing Bach", "https://orac2.info/problem/aic00p3", true),
                        p("Displaying Paintings", "https://orac2.info/problem/aic00p4", true)
                    ], true),

                    s("aic", "AIC 2001", "aic01", [
                        p("Flowers", "https://orac2.info/problem/aic01p1", true),
                        p("Cartography III", "https://orac2.info/problem/aic01p2", true),
                        p("Spies", "https://orac2.info/problem/aic01p3", true),
                        p("Mobiles", "https://orac2.info/problem/aic01p4", true)
                    ], true),

                    s("aic", "AIC 2002", "aic02", [
                        p("Halloween", "https://orac2.info/problem/aic02p1", true),
                        p("Cartography II", "https://orac2.info/problem/aic02p2", true),
                        p("Bureaucratic Bungling", "https://orac2.info/problem/aic02p3", true)
                    ], true),

                    s("aic", "AIC 2003", "aic03", [
                        p("Word Wrap", "https://orac2.info/problem/aic03p2", true),
                        p("Stacking Numbers", "https://orac2.info/problem/aic03p3", true),
                        p("Handwriting Recognition", "https://orac2.info/problem/aic03p4", true)
                    ], true),

                    s("aic", "AIC 2004", "aic04", [
                        p("Bugs", "https://orac2.info/problem/aic04p1", true),
                        p("AFL", "https://orac2.info/problem/aic04p2", true),
                        p("Atlantis", "https://orac2.info/problem/aic04p3", true),
                        p("Zig-Zag Cipher", "https://orac2.info/problem/aic04p4", true),
                        p("Bouncy Ball", "https://orac2.info/problem/aic04p5", true)
                    ], true),

                    s("training", "Data Structure Excercises", "trainingds", [
                        p("Cannons", "https://orac2.info/problem/dscannons", true),
                        p("XOR Queries", "https://orac2.info/problem/dsfenwickxor", true),
                        p("Inversion Counting", "https://orac2.info/problem/dsinversioncounting", true),
                        p("Lazy Updates", "https://orac2.info/problem/dslazyupdate", true),
                        p("Min Tree with Updates", "https://orac2.info/problem/dsrangetreeupdates", true),
                        p("Supplies", "https://orac2.info/problem/dssupplies", true)
                    ], true),

                    s("training", "2005 April Monthly Problems", "monthly05", [
                        p("Anagrammatic Primes", "https://orac2.info/problem/monthly05p1", true),
                        p("Packing Pentominoes", "https://orac2.info/problem/monthly05p2", true),
                        p("Nine Clocks", "https://orac2.info/problem/monthly05p3", true),
                        p("Reverse Polish Notation", "https://orac2.info/problem/precamppublic2", true)
                    ], true),

                    s("aiio", "2020 AIIO", "aiio20", [
                        p("L-Bot", "https://orac2.info/problem/aiio20lbot", true),
                        p("Metromole", "https://orac2.info/problem/aiio20metro", true),
                        p("Purview", "https://orac2.info/problem/aiio20purview", true)
                    ], true),

                    s("training", "Various Problems", "misc_wayback", [
                        p("AIIO 2016 Greece", "https://orac2.info/problem/aiio16greece", true),
                        p("Selection Exam 2012 Career", "https://orac2.info/problem/seln12career", true),
                        p("Selection Camp Problems", "https://orac2.info/problem/selnprac", true),
                        p("2013 December Camp Beta Trial Exam I", "https://orac2.info/problem/trial1beta2013dec", true)
                    ], true)
                ]
            };
        }

        async init() {
            const custom = GM_getValue("tags", {});

            for (const [name, urls] of Object.entries(custom)) {
                this.library.sets.push({
                    tag: "custom-tags",
                    set_name: "Custom Tag: " + name,
                    set_id: "c-" + name.replace(/\s+/g, "-"),
                    problems: (urls || []).map(url => ({
                        problem_name: GM_getValue("name_" + url, "Unknown"),
                        problem_link: url
                    })),
                    tooltip_text: "Tags: " + name
                });
            }

            const container = $(".tags-container");
            if (!container) return;

            const original = [...$(".badge", container)].map(e => e.innerText);

            container.innerHTML =
                '<span class="tags-description">Tags:</span>';

            TAG_ORDER.forEach(name => {
                if (!NEW_TAGS.includes(name) && !original.includes(name))
                    return;

                const badge = tag(name);
                badge.setAttribute("onclick", "toggleSetTagSelected(this);");
                container.appendChild(badge);
            });

            for (const set of this.library.sets)
                await this.renderSet(set);

            if (typeof window.updateSetDisplay === "function")
                window.updateSetDisplay();

            if (typeof querySelector !== "undefined")
                $('[data-toggle="tooltip"]').tooltip();
        }

        async renderSet(set) {
            const div = document.createElement("div");

            div.id = "custom_set-" + set.set_id;
            div.className =
                `problemset-display set-table set-tag-${set.tag}`;

            const tooltip =
                set.tooltip_text || "Tags: " + set.tag;

            div.innerHTML = `
                <table class="table table-sm mt-0 mb-0 pointer" data-toggle="collapse" data-target="#collapse-${set.set_id}">
                    <thead class="thead-dark"><tr>
                        <th scope="col"><span class="set-title mr-auto">${set.set_name}</span></th>
                        <th scope="col" class="progress-column"><div class="d-flex align-items-center">
                            <span class="badge badge-secondary mr-auto score-display">${set.old ? "N/A" : "Fetching scores..."}</span>
                            <span class="fas fa-lg fa-tag" data-toggle="tooltip" data-original-title="${tooltip}"></span>
                        </div></th>
                    </tr></thead>
                </table>
                <div id="collapse-${set.set_id}" class="collapse show set-problems">
                    <table class="table table-sm mt-0 mb-0"><tbody id="table-${set.set_id}"></tbody></table>
                </div>`;

            $("#show-sets").appendChild(div);

            $(".progress-column").forEach(el => {
                if (el.innerHTML.includes("Viewed"))
                    el.innerHTML = "Not attempted";
            });

            const oldMap = [...STARTER_SETS, ...ACIO_SETS];

            oldMap.forEach(name => {
                const parent =
                    $(`[data-target="#problem-set-${name}"]`);

                if (!parent) return;

                parent.parentElement?.classList.add(
                    ACIO_SETS.includes(name)
                        ? "set-tag-acio"
                        : "set-tag-starter"
                );

                const tooltipEl =
                    parent.querySelector('[data-original-title]');

                if (tooltipEl) {
                    tooltipEl.setAttribute(
                        "data-original-title",
                        `Tags: ${ACIO_SETS.includes(name) ? "acio" : "starter"}`
                    );
                }
            });

            const results = await Promise.all(
                (set.problems || []).map(p => this.renderProblem(set, p))
            );

            const valid = results.filter(Boolean);

            const total = valid.reduce(
                (sum, r) => sum + (parseInt(r.score) || 0),
                0
            );

            const attempted =
                valid.some(r => !r.style.includes("badge-no-submission"));

            const max = valid.length * 100;
            const score = div.querySelector(".score-display");

            score.innerText = `${total} / ${max}`;

            const cls =
                !attempted
                    ? "badge-no-submission"
                    : total === max
                        ? "badge-cmsgreen"
                        : total > 0
                            ? "badge-cmsyellow"
                            : "badge-cmsred";

            score.className = "badge mr-auto " + cls;

            if (
                valid.length &&
                valid.length ===
                    (set.problems || []).filter(p => !p.old).length &&
                valid.every(r => parseInt(r.score) === 100)
            )
                div.classList.add("solved-set");
        }

        async renderProblem(set, p) {
            const tbody = $("#table-" + set.set_id);
            if (!tbody || !p) return null;

            const tr = document.createElement("tr");

            tr.innerHTML =
                `<td><a href="${p.problem_link}">${p.problem_name}</a></td><td class="progress-column"></td>`;

            tbody.appendChild(tr);

            if (p.old) {
                tr.setAttribute("old", true);

                tr.children[1].appendChild(
                    Object.assign(document.createElement("span"), {
                        className: "badge badge-secondary",
                        innerText: "N/A"
                    })
                );

                return null;
            }

            const badge = Object.assign(
                document.createElement("span"),
                {
                    className: "badge badge-secondary",
                    innerText: "Fetching score..."
                }
            );

            tr.children[1].appendChild(badge);

            const result = await this.getBadge(p.problem_link);

            badge.innerText = result.score;
            badge.className = result.style;

            if (parseInt(result.score) === 100)
                tr.classList.add("solved-problem");

            return result;
        }
    }

    class DifficultyFilter {
        init() {
            this.loadSolves();
            this.addReloadButton();
            this.addSlider();
            this.addSearch();
        }

        async loadSolves() {
            for (const set of document.querySelectorAll(
                ".collapse.show.set-problems"
            )) {
                const table = set.querySelector("table");
                if (!table) continue;

                const header =
                    set.parentElement?.children?.[0]?.querySelector("tr");

                if (header && !header.querySelector(".th-diff")) {
                    const th = document.createElement("th");
                    th.className = "difficulty-column th-diff";
                    header.children[0]?.after(th);
                }

                const queue = [];

                for (const row of table.querySelectorAll("tbody tr")) {
                    if (
                        row.querySelector("th") ||
                        row.classList.contains("thead-dark") ||
                        row.querySelector(".difficulty-column")
                    )
                        continue;

                    const cell = document.createElement("td");
                    cell.className = "difficulty-column";

                    row.children[0]?.after(cell);

                    if (row.hasAttribute("old")) {
                        cell.innerHTML = "N/A (Old Problem)";
                        continue;
                    }

                    const url = row.querySelector("a")?.href;

                    if (url)
                        queue.push({ row, cell, url });
                }

                const diff =
                    set.parentElement?.querySelector(
                        "table .th-diff"
                    );

                const ratings = [];

                for (const item of queue) {
                    const result =
                        rating(await cachedSolveCount(item.url));

                    item.cell.appendChild(result.elem);
                    item.row.dataset.elo = String(result.elo);

                    ratings.push({
                        row: item.row,
                        elo: result.elo
                    });
                }

                if (diff && ratings.length) {
                    const values =
                        ratings.map(x => x.elo).sort((a, b) => a - b);

                    const min = values[0];
                    const max = values[values.length - 1];

                    diff.dataset.minElo = String(min);
                    diff.dataset.maxElo = String(max);
                    diff.innerHTML = "";

                    if (Math.round(min) === Math.round(max)) {
                        const elem = document.createElement("b");

                        elem.style.color = getEloColor(min);
                        elem.innerText = Math.round(min);

                        diff.appendChild(elem);
                    } else {
                        const low = document.createElement("b");
                        low.style.color = getEloColor(min);
                        low.innerText = Math.round(min);

                        const high = document.createElement("b");
                        high.style.color = getEloColor(max);
                        high.innerText = Math.round(max);

                        diff.append(
                            low,
                            document.createTextNode(" to "),
                            high
                        );
                    }
                }
            }

            this.filter();
        }

        addReloadButton() {
            const target =
                $(".custom-control.custom-switch.mb-sm-1")[1];

            if (!target) return;

            const button = Object.assign(
                document.createElement("button"),
                {
                    className: "koolbutton",
                    innerHTML: "&nbsp;&nbsp;&nbsp;&nbsp;"
                }
            );

            const label = Object.assign(
                document.createElement("label"),
                {
                    innerHTML: "Reload solve rates"
                }
            );

            label.style.marginLeft = ".45rem";

            button.onclick = async () => {
                for (const key of await GM_listValues())
                    if (key.includes("solves_"))
                        await GM_deleteValue(key);

                location.href = location.href;
                this.loadSolves();
            };

            target.after(label);
            target.after(button);
        }

        addSlider() {
            const old =
                document.querySelector(".slider-container");

            if (old) old.remove();

            const container = document.createElement("div");
            container.className = "slider-container";

            const display = Object.assign(
                document.createElement("span"),
                { id: "rangevalue" }
            );

            const wrapper = Object.assign(
                document.createElement("div"),
                {
                    style:
                        "position:relative;width:200px;height:30px;margin-left:10px;"
                }
            );

            const track = Object.assign(
                document.createElement("div"),
                { className: "slider-track" }
            );

            const range = Object.assign(
                document.createElement("div"),
                {
                    className: "slider-range",
                    id: "slider-range"
                }
            );

            const min = this.range("minRange", 0);
            const max = this.range("maxRange", 2800);

            min.style.zIndex = "4";
            max.style.zIndex = "5";

            wrapper.append(track, range, min, max);
            container.append(display, wrapper);

            const tags = $(".tags-container");

            if (!tags?.parentNode) return;

            tags.after(container);

            this.min = min;
            this.max = max;
            this.sliderDisplay = display;
            this.sliderRange = range;
            this.activeHandle = null;

            const activate = e => {
                this.activeHandle = e.target;
            };

            min.addEventListener("pointerdown", activate);
            max.addEventListener("pointerdown", activate);
            min.addEventListener("mousedown", activate);
            max.addEventListener("mousedown", activate);
            min.addEventListener("touchstart", activate, { passive: true });
            max.addEventListener("touchstart", activate, { passive: true });

            const update = e => {
                const target = e?.target;
                let lo = Number(this.min.value);
                let hi = Number(this.max.value);

                if (lo === hi && target) {
                    this.activeHandle = target;
                }

                if (lo > hi) {
                    if (target === this.min)
                        this.max.value = String(lo);
                    else
                        this.min.value = String(hi);
                }

                this.updateSlider();
                this.filter();
            };

            min.addEventListener("input", update);
            max.addEventListener("input", update);
            min.addEventListener("change", update);
            max.addEventListener("change", update);

            this.updateSlider();
        }

        range(id, value) {
            return Object.assign(
                document.createElement("input"),
                {
                    id,
                    type: "range",
                    min: "0",
                    max: "2800",
                    step: "1",
                    value: String(value)
                }
            );
        }

        updateSlider() {
            let min = Number(this.min.value);
            let max = Number(this.max.value);

            min = Math.max(0, Math.min(2800, min));
            max = Math.max(0, Math.min(2800, max));

            if (min > max)
                [min, max] = [max, min];

            this.min.value = String(min);
            this.max.value = String(max);

            this.sliderDisplay.innerHTML = `
                <span style="color:#212529;font-weight:400;font-size:1rem">
                    Difficulty:
                </span>`;

            const minLabel = document.createElement("span");
            minLabel.style.color = getEloColor(min);
            minLabel.style.fontWeight = "700";
            minLabel.textContent = min;

            const maxLabel = document.createElement("span");
            maxLabel.style.color = getEloColor(max);
            maxLabel.style.fontWeight = "700";
            maxLabel.textContent = max;

            this.sliderDisplay.firstElementChild.append(
                document.createTextNode(" "),
                minLabel,
                document.createTextNode(" - "),
                maxLabel
            );

            const p1 = min / 2800 * 100;
            const p2 = max / 2800 * 100;

            this.sliderRange.style.left = `${p1}%`;
            this.sliderRange.style.width =
                `${Math.max(0, p2 - p1)}%`;
        }

        filter() {
            if (!this.min || !this.max) return;

            let min = Number(this.min.value);
            let max = Number(this.max.value);

            if (min > max)
                [min, max] = [max, min];

            document
                .querySelectorAll(".problemset-display.set-table")
                .forEach(set => {
                    const rows =
                        [...set.querySelectorAll(
                            ".set-problems tbody tr"
                        )];

                    const rated =
                        rows.filter(
                            row => row.dataset.elo !== undefined
                        );

                    if (!rated.length) {
                        set.style.display = "";
                        return;
                    }

                    let visible = 0;

                    rated.forEach(row => {
                        const elo = Number(row.dataset.elo);

                        const show =
                            Number.isFinite(elo) &&
                            elo >= min &&
                            elo <= max;

                        row.hidden = !show;
                        row.setAttribute(
                            "data-visible",
                            String(show)
                        );

                        if (show) visible++;
                    });

                    set.style.display =
                        visible ? "block" : "none";
                });
        }

        addSearch() {
            const container = document.createElement("div");
            container.className = "search-container";

            const label = Object.assign(
                document.createElement("span"),
                { innerText: "Includes: " }
            );

            const input = Object.assign(
                document.createElement("input"),
                {
                    className: "custom-search",
                    placeholder: "Type a phrase"
                }
            );

            container.append(label, input);

            $(".slider-container").after(container);

            input.addEventListener(
                "input",
                () => this.search(input.value)
            );
        }

        search(value) {
            const phrase = value.toLowerCase();

            document
                .querySelectorAll(".problemset-display.set-table")
                .forEach(set => {
                    let match = false;

                    const title =
                        set.querySelector('[scope="col"]')
                            ?.innerText.toLowerCase() || "";

                    const titleMatch =
                        title.includes(phrase);

                    const collapse =
                        set.querySelector(".collapse");

                    if (!collapse) return;

                    collapse
                        .querySelectorAll("tr")
                        .forEach(row => {
                            if (
                                row.querySelector("th") ||
                                row.closest("thead")
                            )
                                return;

                            const link =
                                row.querySelector("a");

                            if (!link) return;

                            const problemMatch =
                                link.innerText
                                    .toLowerCase()
                                    .includes(phrase);

                            const visible =
                                !phrase ||
                                titleMatch ||
                                problemMatch;

                            row.setAttribute(
                                "data-visible",
                                String(visible)
                            );

                            match ||= visible;
                        });

                    set.setAttribute(
                        "data-visible",
                        String(
                            !phrase ||
                            titleMatch ||
                            match
                        )
                    );
                });
        }
    }

    function setupNavigation() {
        waitFor(".collapse.navbar-collapse").then(el => {
            el.children[0].innerHTML += `
                <a class="nav-item nav-link user-links" href="/hub/notes/">Notes</a>
                <a class="nav-item nav-link user-links" href="https://aperson31415.github.io/informatics/">Github</a>`;
        });

        if (
            location.href === "https://orac2.info/hub/notes/" ||
            location.href === "https://orac.amt.edu.au/hub/notes"
        ) {
            waitFor("p").then(() => {
                document.title = "Informatics Notes - ORAC";

                const pdf =
                    `https://cdn.jsdelivr.net/gh/aperson31415/informatics@main/kactl.pdf?time=${Date.now()}`;

                document.querySelectorAll(
                    "div.container-xl"
                )[1].innerHTML =
                    `<embed class="embed-responsive-item" src="${pdf}" type="application/pdf" style="flex-grow:1;width:100%;height:80vh">`;
            });
        }
    }

    function setupPersonal() {
        unsafeWindow.toggleSetTagSelected = elem => {
            if (elem.classList.contains("selected-tag")) {
                elem.classList.remove("selected-tag");
                localStorage.removeItem("selected-tag");
            } else {
                elem.classList.add("selected-tag");
                localStorage.setItem(
                    "selected-tag",
                    elem.textContent.trim()
                );
            }

            reloadHidden();
        };

        new PersonalPage().init().then(() => {
            new DifficultyFilter().init();
        });
    }

    function setupHof() {
        unsafeWindow.toggleLanguageFilter = (elem, language) => {
            elem.classList.toggle("selected-tag");

            if (!elem.classList.contains("selected-tag"))
                language = null;

            const solvers =
                document
                    .getElementById("solversList")
                    .getElementsByClassName("solver");

            for (let i = 0; i < solvers.length; i++)
                solvers[i].hidden =
                    !!language &&
                    !solvers[i].classList.contains(language);

            hideLastComma();
        };
    }

    function setupProblem() {
        const valid =
            location.href.includes("problem") &&
            !location.href.includes("hof") &&
            !location.href.includes("submission");

        if (valid)
            new ProblemPage().init();
    }

    document.addEventListener("click", e => {
        if (e.target.classList.contains("plus")) {
            e.preventDefault();
            e.stopPropagation();
            new TagManager().add();
        } else if (e.target.classList.contains("minus")) {
            e.preventDefault();
            e.stopPropagation();
            new TagManager().remove();
        }
    }, true);

    const boot = () => {
        if (location.href.includes("problem")) {
            Restorer.run();
            setupProblem();
        }

        if (location.href.includes("personal"))
            setupPersonal();
        else if (location.href.includes("hof"))
            setupHof();

        setupNavigation();
    };

    const interval = setInterval(() => {
        if (document.querySelector("h1")) {
            clearInterval(interval);
            boot();
        }
    }, 100);


    /* ============================================================
       START FIRST SCRIPT
       ============================================================ */

    if (
        document.readyState === 'complete' ||
        document.readyState === 'interactive'
    ) {
        setTimeout(init, 300);
    } else {
        window.addEventListener('DOMContentLoaded', init);
    }

})();