AtCoder Problems Custom Add

Custom add, deduplicate, sort, shuffle, remove, and optionally set points on the AtCoder Problems contest creation page.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         AtCoder Problems Custom Add
// @namespace    https://kenkoooo.com/atcoder/
// @version      1.5.0
// @description  Custom add, deduplicate, sort, shuffle, remove, and optionally set points on the AtCoder Problems contest creation page.
// @author       generated
// @match        https://kenkoooo.com/atcoder/*
// @grant        none
// @license MIT 
// @run-at       document-idle
// ==/UserScript==

(() => {
  "use strict";

  const APP_TITLE = "Custom Add";
  const VERSION = "1.5.0";
  const PANEL_ID = "apca-panel";
  const LEGACY_PANEL_IDS = ["apcb-panel"];
  const STATUS_KEY = "apca:last-status";
  const FORM_KEY = "apca:contest-form";
  const POINTS_KEY = "apca:pending-points";
  const POINT_CACHE_KEY = "apca:point-cache";
  const PROBLEMS_URL = "https://kenkoooo.com/atcoder/resources/problems.json";
  const SEP = "~";
  const DEFAULT_MIN_NO = 1;
  const DEFAULT_MAX_NO = 9999;
  const SERIES = ["abc", "arc", "agc", "awc", "adt", "ahc", "custom"];
  const SERIES_PRIORITY = ["abc", "arc", "agc", "awc", "adt", "ahc"];
  const INDEXES = [
    "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
    "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "EX",
  ];

  let problemsPromise = null;
  let restoreStarted = false;
  let pointsApplyStarted = false;
  let applyingPoints = false;
  let originalDocumentTitle = null;

  const $ = (selector, base = document) => base.querySelector(selector);
  const $$ = (selector, base = document) => Array.from(base.querySelectorAll(selector));
  const isCreatePage = () => /^#\/contest\/create(?:\?|$)/.test(location.hash || "");
  const root = () => document.getElementById(PANEL_ID);
  const normIndex = (s) => String(s || "").trim().toUpperCase();
  const esc = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  const sleep = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms));

  function uniq(items) {
    const seen = new Set();
    const out = [];
    for (const item of items) {
      if (!item || seen.has(item)) continue;
      seen.add(item);
      out.push(item);
    }
    return out;
  }

  function updateDocumentTitle() {
    if (isCreatePage()) {
      if (originalDocumentTitle === null) originalDocumentTitle = document.title;
      if (document.title !== APP_TITLE) document.title = APP_TITLE;
    } else if (originalDocumentTitle !== null && document.title === APP_TITLE) {
      document.title = originalDocumentTitle;
    }
  }

  function show(message, type = "info") {
    const el = $("[data-apca-status]", root());
    if (!el) return;
    el.textContent = message || "";
    el.style.display = message ? "block" : "none";
    el.className = ({
      info: "alert alert-info py-2 my-2",
      success: "alert alert-success py-2 my-2",
      warning: "alert alert-warning py-2 my-2",
      danger: "alert alert-danger py-2 my-2",
    }[type] || "alert alert-info py-2 my-2");
  }

  function setBusy(busy) {
    const r = root();
    if (!r) return;
    r.dataset.busy = busy ? "1" : "0";

    const series = $("[data-apca-series]", r)?.value || "abc";
    const mode = $("[data-apca-mode]", r)?.value || "random";
    const pointMode = $("[data-apca-point-mode]", r)?.value || "unset";

    $$("button,input,select", r).forEach((el) => { el.disabled = busy; });

    const custom = $("[data-apca-custom]", r);
    const count = $("[data-apca-count]", r);
    const point = $("[data-apca-point]", r);
    if (custom) custom.disabled = busy || series !== "custom";
    if (count) count.disabled = busy || mode !== "random";
    if (point) point.disabled = busy || pointMode !== "set";
  }

  async function getProblems() {
    if (!problemsPromise) {
      problemsPromise = fetch(PROBLEMS_URL, { credentials: "same-origin" })
        .then((res) => {
          if (!res.ok) throw new Error(`problems.json: HTTP ${res.status}`);
          return res.json();
        })
        .then((json) => {
          if (!Array.isArray(json)) throw new Error("Invalid problems.json");
          return json.filter((p) => p && typeof p.id === "string" && typeof p.contest_id === "string");
        });
    }
    return problemsPromise;
  }

  function normalizeContestId(input) {
    const raw = String(input || "").trim();
    if (!raw) return "";
    try {
      const url = new URL(raw);
      const m = url.pathname.match(/\/contests\/([^/?#]+)/i);
      if (m) return m[1].toLowerCase();
    } catch (_) {}
    const m = raw.match(/(?:contests\/)?([a-zA-Z0-9_-]+)/);
    return m ? m[1].toLowerCase() : "";
  }

  function contestAliases(id) {
    const s = String(id || "").toLowerCase();
    const out = new Set([s]);
    const m = s.match(/^([a-z]+)0*(\d+)$/);
    if (m) {
      const prefix = m[1];
      const n = Number(m[2]);
      if (Number.isFinite(n)) {
        out.add(`${prefix}${n}`);
        out.add(`${prefix}${String(n).padStart(3, "0")}`);
        out.add(`${prefix}${String(n).padStart(4, "0")}`);
      }
    }
    return out;
  }

  function parseOptionalInt(value, name) {
    const text = String(value || "").trim();
    if (text === "") return null;
    const n = Number(text);
    if (!Number.isInteger(n) || n < 0) throw new Error(`${name}: integer >= 0`);
    return n;
  }

  function readPointPolicy(required = false) {
    const r = root();
    const mode = $("[data-apca-point-mode]", r)?.value || "unset";
    if (mode === "unset") return { mode: "unset", point: null };
    const text = String($("[data-apca-point]", r)?.value || "").trim();
    if (text === "") {
      if (required) throw new Error("Point: integer >= 0");
      return { mode: "unset", point: null };
    }
    const n = Number(text);
    if (!Number.isInteger(n) || n < 0) throw new Error("Point: integer >= 0");
    return { mode: "set", point: n };
  }

  function selectedIndexes() {
    const r = root();
    if (!r || $("[data-apca-index-all]", r)?.checked) return [];
    return $$("[data-apca-index]", r)
      .filter((input) => input.checked)
      .map((input) => normIndex(input.value))
      .filter(Boolean);
  }

  function onIndexChange(event) {
    const r = root();
    if (!r) return;
    const all = $("[data-apca-index-all]", r);
    const indexes = $$("[data-apca-index]", r);
    if (event.target?.matches?.("[data-apca-index-all]")) {
      if (all.checked) indexes.forEach((input) => { input.checked = false; });
      else if (!indexes.some((input) => input.checked)) all.checked = true;
      return;
    }
    if (event.target?.matches?.("[data-apca-index]")) {
      all.checked = !indexes.some((input) => input.checked);
    }
  }

  function firstNumber(s) {
    const m = String(s || "").match(/(\d+)/);
    return m ? Number(m[1]) : null;
  }

  function matchPrefix(contestId, prefix) {
    const id = String(contestId || "").toLowerCase();
    const p = String(prefix || "").toLowerCase();
    if (!p) return null;
    if (p === "adt") return id.startsWith("adt_") || id === "adt" ? { number: firstNumber(id) ?? 0 } : null;

    const numbered = id.match(new RegExp(`^${esc(p)}0*(\\d+)$`, "i"));
    if (numbered) return { number: Number(numbered[1]) };
    if (id === p || id.startsWith(`${p}_`) || id.startsWith(`${p}-`)) {
      return { number: firstNumber(id) ?? 0 };
    }
    return null;
  }

  function readCondition() {
    const r = root();
    const series = $("[data-apca-series]", r)?.value || "abc";
    const customPrefixes = String($("[data-apca-custom]", r)?.value || "")
      .split(/[\s,、]+/)
      .map((s) => s.trim().toLowerCase())
      .filter(Boolean);
    const minNo = parseOptionalInt($("[data-apca-min]", r)?.value, "No. min");
    const maxNo = parseOptionalInt($("[data-apca-max]", r)?.value, "No. max");
    const mode = $("[data-apca-mode]", r)?.value || "random";
    const count = parseOptionalInt($("[data-apca-count]", r)?.value, "Count");

    if (!SERIES.includes(series)) throw new Error("Series");
    if (series === "custom" && customPrefixes.length === 0) throw new Error("Custom prefix");
    if (minNo !== null && maxNo !== null && minNo > maxNo) throw new Error("No. min > No. max");
    if (mode === "random" && (!Number.isInteger(count) || count <= 0)) throw new Error("Count: integer >= 1");

    return { series, customPrefixes, indexes: selectedIndexes(), minNo, maxNo, mode, count };
  }

  function isDefaultRange(cond) {
    return cond.minNo === DEFAULT_MIN_NO && cond.maxNo === DEFAULT_MAX_NO;
  }

  function isRangeActive(cond) {
    return !isDefaultRange(cond) && (cond.minNo !== null || cond.maxNo !== null);
  }

  function filterProblems(problems, cond) {
    const prefixes = cond.series === "custom" ? cond.customPrefixes : [cond.series];
    const indexSet = new Set(cond.indexes);
    const rangeActive = isRangeActive(cond);
    return problems.filter((problem) => {
      let matched = null;
      for (const prefix of prefixes) {
        matched = matchPrefix(problem.contest_id, prefix);
        if (matched) break;
      }
      if (!matched) return false;
      if (rangeActive && cond.minNo !== null && matched.number < cond.minNo) return false;
      if (rangeActive && cond.maxNo !== null && matched.number > cond.maxNo) return false;
      if (indexSet.size && !indexSet.has(normIndex(problem.problem_index))) return false;
      return true;
    });
  }

  function seriesOfContest(contestId) {
    const id = String(contestId || "").toLowerCase();
    if (id.startsWith("adt_") || id === "adt") return "adt";
    const m = id.match(/^([a-z]+)\d+/);
    if (m) return m[1];
    return id.split(/[_-]/)[0] || "";
  }

  function originalContestFromProblemId(problemId) {
    const id = String(problemId || "").toLowerCase();
    const m = id.match(/^([a-z]+)(\d+)[_-]/);
    if (!m) return "";
    return `${m[1]}${Number(m[2])}`;
  }

  function indexKey(index) {
    const s = normIndex(index);
    if (/^[A-Z]$/.test(s)) return [s.charCodeAt(0) - 65, s];
    if (s === "EX") return [26, s];
    const first = s.match(/[A-Z]/)?.[0];
    return [first ? first.charCodeAt(0) - 65 : 999, s];
  }

  function priorityOfProblem(problem) {
    const contestId = String(problem?.contest_id || "").toLowerCase();
    const original = originalContestFromProblemId(problem?.id);
    const originalMatch = original && contestAliases(original).has(contestId) ? 0 : 1;
    const series = seriesOfContest(contestId);
    const seriesRank = SERIES_PRIORITY.includes(series) ? SERIES_PRIORITY.indexOf(series) : 50;
    const contestRank = contestId || "~";
    const indexRank = indexKey(problem?.problem_index || "");
    return [originalMatch, seriesRank, contestRank, indexRank[0], indexRank[1]];
  }

  function comparePriority(a, b) {
    const ak = priorityOfProblem(a);
    const bk = priorityOfProblem(b);
    for (let i = 0; i < Math.min(ak.length, bk.length); i += 1) {
      if (typeof ak[i] === "number" && typeof bk[i] === "number") {
        if (ak[i] !== bk[i]) return ak[i] - bk[i];
      } else {
        const c = String(ak[i]).localeCompare(String(bk[i]), undefined, { numeric: true, sensitivity: "base" });
        if (c) return c;
      }
    }
    return 0;
  }

  function bestProblemMap(problems) {
    const byId = new Map();
    for (const problem of problems) {
      if (!problem?.id) continue;
      const current = byId.get(problem.id);
      if (!current || comparePriority(problem, current) < 0) byId.set(problem.id, problem);
    }
    return byId;
  }

  function dedupeProblems(problems, byId = null) {
    const seen = new Set();
    const out = [];
    for (const problem of problems) {
      const id = problem?.id;
      if (!id || seen.has(id)) continue;
      seen.add(id);
      out.push(byId?.get(id) || problem);
    }
    return out;
  }

  function compareProblem(a, b) {
    const c = String(a.contest_id).localeCompare(String(b.contest_id), undefined, { numeric: true, sensitivity: "base" });
    if (c) return c;
    const ak = indexKey(a.problem_index);
    const bk = indexKey(b.problem_index);
    if (ak[0] !== bk[0]) return ak[0] - bk[0];
    return String(ak[1]).localeCompare(String(bk[1]), undefined, { numeric: true, sensitivity: "base" });
  }

  function randomFloat() {
    const cryptoObj = window.crypto || window.msCrypto;
    if (cryptoObj?.getRandomValues) {
      const buf = new Uint32Array(1);
      cryptoObj.getRandomValues(buf);
      return buf[0] / (0xffffffff + 1);
    }
    return Math.random();
  }

  function shuffle(items) {
    const a = items.slice();
    for (let i = a.length - 1; i > 0; i -= 1) {
      const j = Math.floor(randomFloat() * (i + 1));
      [a[i], a[j]] = [a[j], a[i]];
    }
    return a;
  }

  function readHashIds() {
    const i = location.hash.indexOf("?");
    if (i < 0) return [];
    const ids = new URLSearchParams(location.hash.slice(i + 1)).get("problemIds");
    return ids ? ids.split(SEP).map((id) => decodeURIComponent(id)).filter(Boolean) : [];
  }

  function problemRows() {
    const rows = $$(`tr[data-rbd-draggable-id]`).filter((el) => !el.closest(`#${PANEL_ID}`));
    if (rows.length) return rows;

    const out = [];
    $$(`[data-rbd-draggable-id]`).forEach((el) => {
      if (el.closest(`#${PANEL_ID}`)) return;
      const row = el.closest("tr") || el;
      if (!row || row.closest(`#${PANEL_ID}`) || out.includes(row)) return;
      out.push(row);
    });
    return out;
  }

  function readDomIds() {
    const draggableIds = problemRows()
      .map((el) => el.getAttribute("data-rbd-draggable-id") || el.querySelector("[data-rbd-draggable-id]")?.getAttribute("data-rbd-draggable-id"))
      .filter(Boolean);
    if (draggableIds.length) return draggableIds;

    const ids = [];
    $$("a[href*='/tasks/']").forEach((a) => {
      if (a.closest(`#${PANEL_ID}`)) return;
      try {
        const m = new URL(a.href).pathname.match(/\/tasks\/([^/?#]+)/);
        if (m) ids.push(m[1]);
      } catch (_) {}
    });
    return ids.filter(Boolean);
  }

  function rawCurrentIds() {
    const dom = readDomIds();
    return dom.length ? dom : readHashIds();
  }

  function currentIds() {
    return uniq(rawCurrentIds());
  }

  function pointColumnIndexForRow(row) {
    const table = row.closest("table");
    if (!table) return 3;
    const headers = Array.from(table.querySelectorAll("thead th"));
    const idx = headers.findIndex((th) => String(th.textContent || "").trim().toLowerCase() === "point");
    return idx >= 0 ? idx : 3;
  }

  function pointCellOfRow(row) {
    const cells = Array.from(row.children).filter((el) => el.tagName === "TD");
    const idx = pointColumnIndexForRow(row);
    return cells[idx] || cells[3] || null;
  }

  function parsePointCellValue(cell) {
    if (!cell) return undefined;
    const input = cell.querySelector('input[type="number"]');
    const text = String(input ? input.value : cell.textContent || "").trim();
    if (text === "" || text === "-") return null;
    if (/^\d+$/.test(text)) return Number(text);
    return undefined;
  }

  function readCurrentPointMap(includeNull = true) {
    const map = new Map();
    for (const row of problemRows()) {
      const id = row.getAttribute("data-rbd-draggable-id") || row.querySelector("[data-rbd-draggable-id]")?.getAttribute("data-rbd-draggable-id");
      const value = parsePointCellValue(pointCellOfRow(row));
      if (!id || value === undefined) continue;
      if (value !== null || includeNull) map.set(id, value);
    }
    return map;
  }

  function mapToPlainObject(map) {
    const obj = {};
    for (const [id, point] of map || new Map()) {
      if (!id) continue;
      if (point === null || (Number.isInteger(point) && point >= 0)) obj[id] = point;
    }
    return obj;
  }

  function plainObjectToPointMap(obj) {
    const map = new Map();
    Object.entries(obj || {}).forEach(([id, point]) => {
      if (!id) return;
      if (point === null || (Number.isInteger(point) && point >= 0)) map.set(id, point);
    });
    return map;
  }

  function savePointCache(ids = currentIds(), pointMap = readCurrentPointMap(true)) {
    if (!ids.length || !pointMap.size) {
      sessionStorage.removeItem(POINT_CACHE_KEY);
      return;
    }
    sessionStorage.setItem(POINT_CACHE_KEY, JSON.stringify({
      ids,
      points: mapToPlainObject(pointMap),
      createdAt: Date.now(),
    }));
  }

  function readPointCache() {
    try {
      const raw = sessionStorage.getItem(POINT_CACHE_KEY);
      if (!raw) return new Map();
      const data = JSON.parse(raw);
      return plainObjectToPointMap(data.points || {});
    } catch (_) {
      sessionStorage.removeItem(POINT_CACHE_KEY);
      return new Map();
    }
  }

  function pointMapForNavigation(ids, policy = { mode: "preserve", point: null }) {
    const existingIds = currentIds();
    const domMap = readCurrentPointMap(true);
    const cacheMap = readPointCache();
    if (domMap.size) savePointCache(existingIds, domMap);

    const out = new Map();
    for (const id of ids) {
      if (!id) continue;
      if (policy.mode === "set") {
        out.set(id, policy.point);
      } else if (policy.mode === "clear") {
        out.set(id, null);
      } else {
        if (domMap.has(id)) {
          const value = domMap.get(id);
          if (value !== null) out.set(id, value);
        } else if (existingIds.length && !domMap.size && cacheMap.has(id)) {
          const value = cacheMap.get(id);
          if (value !== null) out.set(id, value);
        }
      }
    }
    return out;
  }

  function schedulePoints(pointMap) {
    const points = mapToPlainObject(pointMap);
    if (Object.keys(points).length === 0) {
      sessionStorage.removeItem(POINTS_KEY);
      return;
    }
    sessionStorage.setItem(POINTS_KEY, JSON.stringify({ points, createdAt: Date.now() }));
  }

  function outside(selector) {
    return $$(selector).filter((el) => !el.closest(`#${PANEL_ID}`));
  }

  function saveForm() {
    const selectors = [
      'input[placeholder="Contest Title"]',
      'textarea[placeholder="Description"]',
      'input[placeholder^="Penalty"]',
      'input[placeholder="AtCoder ID list separated by space"]',
      'input[type="date"]',
      'input[type="time"]',
      'input[type="datetime-local"]',
      "select",
    ];
    const data = {};
    selectors.forEach((selector) => { data[selector] = outside(selector).map((el) => el.value ?? ""); });
    sessionStorage.setItem(FORM_KEY, JSON.stringify(data));
  }

  function setValue(el, value) {
    if (!el) return;
    const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el), "value")?.set;
    if (setter) setter.call(el, value);
    else el.value = value;
    el.dispatchEvent(new Event("input", { bubbles: true }));
    el.dispatchEvent(new Event("change", { bubbles: true }));
  }

  function restoreFormOnce() {
    const raw = sessionStorage.getItem(FORM_KEY);
    if (!raw) return true;
    if (!outside('input[placeholder="Contest Title"]')[0]) return false;
    try {
      const data = JSON.parse(raw);
      Object.entries(data).forEach(([selector, values]) => {
        const elements = outside(selector);
        (values || []).forEach((value, i) => setValue(elements[i], value));
      });
    } finally {
      sessionStorage.removeItem(FORM_KEY);
    }
    return true;
  }

  function startRestore() {
    if (restoreStarted) return;
    restoreStarted = true;
    let tries = 0;
    const timer = setInterval(() => {
      tries += 1;
      if (restoreFormOnce() || tries >= 40) clearInterval(timer);
    }, 250);
  }

  function navigate(ids, message, pointMap = null, options = {}) {
    const nextIds = uniq(ids).filter(Boolean);
    const capturePoints = options.capturePoints !== false;
    if (capturePoints) savePointCache(currentIds(), readCurrentPointMap(true));
    saveForm();
    if (message) sessionStorage.setItem(STATUS_KEY, message);
    if (pointMap) schedulePoints(pointMap);
    else sessionStorage.removeItem(POINTS_KEY);

    if (!nextIds.length) {
      location.href = `${location.origin}${location.pathname}${location.search}#/contest/create`;
    } else {
      const encoded = nextIds.map((id) => encodeURIComponent(id)).join(SEP);
      location.href = `${location.origin}${location.pathname}${location.search}#/contest/create?problemIds=${encoded}`;
    }
    setTimeout(() => location.reload(), 50);
  }

  async function setPointInRow(row, point) {
    const cell = pointCellOfRow(row);
    if (!cell) return false;

    const currentValue = parsePointCellValue(cell);
    if (currentValue === point) return true;

    cell.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
    await sleep(20);

    let input = cell.querySelector('input[type="number"]');
    if (!input) {
      await sleep(80);
      input = cell.querySelector('input[type="number"]');
    }
    if (!input) return false;

    setValue(input, point === null ? "" : String(point));
    input.dispatchEvent(new KeyboardEvent("keypress", { key: "Enter", code: "Enter", bubbles: true }));
    input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", bubbles: true }));
    input.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", code: "Enter", bubbles: true }));
    input.blur();
    await sleep(10);
    return true;
  }

  async function applyPointMapToVisibleRows(pointMap) {
    const rows = problemRows();
    if (!rows.length) return { done: false, applied: 0, missing: pointMap.size };

    const rowsById = new Map();
    rows.forEach((row) => {
      const id = row.getAttribute("data-rbd-draggable-id") || row.querySelector("[data-rbd-draggable-id]")?.getAttribute("data-rbd-draggable-id");
      if (id) rowsById.set(id, row);
    });

    const missing = Array.from(pointMap.keys()).filter((id) => !rowsById.has(id));
    if (missing.length) return { done: false, applied: 0, missing: missing.length };

    let applied = 0;
    for (const [id, point] of pointMap.entries()) {
      const row = rowsById.get(id);
      if (!row) continue;
      if (await setPointInRow(row, point)) applied += 1;
    }
    savePointCache(currentIds(), readCurrentPointMap(true));
    return { done: true, applied, missing: 0 };
  }

  async function applyPendingPointsOnce() {
    const raw = sessionStorage.getItem(POINTS_KEY);
    if (!raw || applyingPoints) return true;
    let data;
    try {
      data = JSON.parse(raw);
    } catch (_) {
      sessionStorage.removeItem(POINTS_KEY);
      return true;
    }

    const pointMap = plainObjectToPointMap(data.points || {});
    if (!pointMap.size) {
      sessionStorage.removeItem(POINTS_KEY);
      return true;
    }

    applyingPoints = true;
    try {
      const result = await applyPointMapToVisibleRows(pointMap);
      if (result.done) {
        sessionStorage.removeItem(POINTS_KEY);
        show(`Points applied: ${result.applied}`, "success");
        return true;
      }
      return false;
    } finally {
      applyingPoints = false;
    }
  }

  function startPointApply() {
    if (pointsApplyStarted) return;
    if (!sessionStorage.getItem(POINTS_KEY)) return;
    pointsApplyStarted = true;
    let tries = 0;
    const timer = setInterval(async () => {
      tries += 1;
      const done = await applyPendingPointsOnce();
      if (done || tries >= 100) {
        clearInterval(timer);
        pointsApplyStarted = false;
        if (!done) show("Point setting failed", "warning");
      }
    }, 250);
  }

  function syncPointCache() {
    if (!isCreatePage()) return;
    const ids = currentIds();
    const map = readCurrentPointMap(true);
    if (ids.length && map.size) savePointCache(ids, map);
  }

  async function appendProblems(problems, label, allProblems = null) {
    const policy = readPointPolicy(false);
    const source = allProblems || await getProblems();
    const byId = bestProblemMap(source);
    const selected = dedupeProblems(problems, byId)
      .sort(compareProblem)
      .map((p) => p.id)
      .filter(Boolean);
    if (!selected.length) {
      show("No problem to add", "warning");
      return;
    }
    const cur = currentIds();
    const next = uniq([...cur, ...selected]);
    const added = next.length - cur.length;
    if (added <= 0) {
      show("Already added", "warning");
      return;
    }
    navigate(next, `${label}: ${added} added`, pointMapForNavigation(next, policy.mode === "set" ? policy : { mode: "preserve", point: null }));
  }

  async function addContest() {
    const id = normalizeContestId($("[data-apca-contest]", root())?.value);
    if (!id) {
      show("Contest ID required", "warning");
      return;
    }
    setBusy(true);
    try {
      const problems = await getProblems();
      const aliases = contestAliases(id);
      const byId = bestProblemMap(problems);
      const selected = dedupeProblems(
        problems.filter((p) => aliases.has(String(p.contest_id).toLowerCase())),
        byId,
      ).sort(compareProblem);
      await appendProblems(selected, id.toUpperCase(), problems);
    } catch (e) {
      console.error(e);
      show(String(e.message || e), "danger");
    } finally {
      setBusy(false);
    }
  }

  function rangeLabel(cond) {
    if (!isRangeActive(cond)) return "";
    const left = cond.minNo !== null ? String(cond.minNo) : "";
    const right = cond.maxNo !== null ? String(cond.maxNo) : "";
    return ` No.${left}-${right}`;
  }

  function labelOf(cond) {
    const series = cond.series === "custom" ? cond.customPrefixes.join(",").toUpperCase() : cond.series.toUpperCase();
    const index = cond.indexes.length ? cond.indexes.join(",") : "ALL";
    return `${series}-${index}${rangeLabel(cond)}`;
  }

  async function countCandidates() {
    setBusy(true);
    try {
      const cond = readCondition();
      const problems = await getProblems();
      const candidates = dedupeProblems(filterProblems(problems, cond), bestProblemMap(problems));
      show(`${labelOf(cond)}: ${candidates.length} candidates`, candidates.length ? "success" : "warning");
    } catch (e) {
      console.error(e);
      show(String(e.message || e), "danger");
    } finally {
      setBusy(false);
    }
  }

  async function addByCondition() {
    setBusy(true);
    try {
      const cond = readCondition();
      const problems = await getProblems();
      const byId = bestProblemMap(problems);
      const candidates = dedupeProblems(filterProblems(problems, cond), byId).sort(compareProblem);
      const selected = cond.mode === "random"
        ? shuffle(candidates).slice(0, Math.min(cond.count, candidates.length)).sort(compareProblem)
        : candidates;
      await appendProblems(selected, labelOf(cond), problems);
    } catch (e) {
      console.error(e);
      show(String(e.message || e), "danger");
    } finally {
      setBusy(false);
    }
  }

  function removeDuplicatesCurrent() {
    const raw = rawCurrentIds();
    if (!raw.length) {
      show("No problem", "warning");
      return;
    }
    const ids = uniq(raw);
    const removed = raw.length - ids.length;
    if (removed <= 0) {
      show("No duplicate", "success");
      return;
    }
    navigate(ids, `Duplicates removed: ${removed}`, pointMapForNavigation(ids, { mode: "preserve", point: null }));
  }

  function removeAllCurrent() {
    const ids = currentIds();
    if (!ids.length) {
      show("No problem", "warning");
      return;
    }
    if (!window.confirm(`Remove all ${ids.length} problems?`)) return;
    sessionStorage.removeItem(POINT_CACHE_KEY);
    sessionStorage.removeItem(POINTS_KEY);
    navigate([], `All removed: ${ids.length}`, null, { capturePoints: false });
  }

  async function sortCurrent() {
    const ids = currentIds();
    if (!ids.length) {
      show("No problem", "warning");
      return;
    }
    setBusy(true);
    try {
      const problems = await getProblems();
      const byId = bestProblemMap(problems);
      const pos = new Map(ids.map((id, i) => [id, i]));
      const sorted = ids.slice().sort((a, b) => {
        const pa = byId.get(a);
        const pb = byId.get(b);
        if (pa && pb) return compareProblem(pa, pb);
        if (pa) return -1;
        if (pb) return 1;
        return (pos.get(a) ?? 0) - (pos.get(b) ?? 0);
      });
      navigate(sorted, `Sorted: ${sorted.length}`, pointMapForNavigation(sorted, { mode: "preserve", point: null }));
    } catch (e) {
      console.error(e);
      show(String(e.message || e), "danger");
    } finally {
      setBusy(false);
    }
  }

  function shuffleCurrent() {
    const ids = currentIds();
    if (!ids.length) {
      show("No problem", "warning");
      return;
    }
    const shuffled = shuffle(ids);
    navigate(shuffled, `Shuffled: ${ids.length}`, pointMapForNavigation(shuffled, { mode: "preserve", point: null }));
  }

  async function setAllPointsCurrent() {
    const ids = currentIds();
    if (!ids.length) {
      show("No problem", "warning");
      return;
    }
    setBusy(true);
    try {
      const policy = readPointPolicy(true);
      const point = policy.mode === "set" ? policy.point : null;
      const pointMap = new Map(ids.map((id) => [id, point]));
      const result = await applyPointMapToVisibleRows(pointMap);
      if (!result.done) {
        show("Point setting failed", "warning");
        return;
      }
      show(`Points applied: ${result.applied}`, "success");
    } catch (e) {
      console.error(e);
      show(String(e.message || e), "danger");
    } finally {
      setBusy(false);
    }
  }

  function indexCheckbox(value, checked = false) {
    const attr = value === "__all" ? "data-apca-index-all" : "data-apca-index";
    const text = value === "__all" ? "All" : value;
    return `<label class="apca-index-item"><input type="checkbox" ${attr} value="${value}" ${checked ? "checked" : ""}>${text}</label>`;
  }

  function createPanel() {
    const div = document.createElement("div");
    div.id = PANEL_ID;
    div.dataset.apcaVersion = VERSION;
    div.className = "card my-3";
    div.innerHTML = `
      <style>
        #${PANEL_ID} .apca-index-list{display:flex;flex-wrap:wrap;gap:6px 10px;padding:8px;border:1px solid #ced4da;border-radius:.25rem;min-height:38px}
        #${PANEL_ID} .apca-index-item{margin:0;white-space:nowrap;font-weight:normal}
        #${PANEL_ID} .apca-index-item input{margin-right:3px}
      </style>
      <div class="card-header"><strong>Custom Add</strong></div>
      <div class="card-body">
        <div class="form-row align-items-end">
          <div class="form-group col-md-7">
            <label class="mb-1">Contest ID / URL</label>
            <input data-apca-contest class="form-control">
          </div>
          <div class="form-group col-md-5">
            <button data-apca-add-contest class="btn btn-primary btn-block" type="button">Add Contest</button>
          </div>
        </div>
        <hr>
        <div class="form-row">
          <div class="form-group col-md-2">
            <label class="mb-1">Series</label>
            <select data-apca-series class="form-control">
              <option value="abc" selected>ABC</option>
              <option value="arc">ARC</option>
              <option value="agc">AGC</option>
              <option value="awc">AWC</option>
              <option value="adt">ADT</option>
              <option value="ahc">AHC</option>
              <option value="custom">custom</option>
            </select>
          </div>
          <div class="form-group col-md-2">
            <label class="mb-1">custom prefix</label>
            <input data-apca-custom class="form-control" disabled>
          </div>
          <div class="form-group col-md-1">
            <label class="mb-1">No. min</label>
            <input data-apca-min class="form-control" type="number" min="0" value="${DEFAULT_MIN_NO}">
          </div>
          <div class="form-group col-md-1">
            <label class="mb-1">No. max</label>
            <input data-apca-max class="form-control" type="number" min="0" value="${DEFAULT_MAX_NO}">
          </div>
          <div class="form-group col-md-2">
            <label class="mb-1">Selection</label>
            <select data-apca-mode class="form-control">
              <option value="random" selected>Random</option>
              <option value="all">All</option>
            </select>
          </div>
          <div class="form-group col-md-2">
            <label class="mb-1">Count</label>
            <input data-apca-count class="form-control" type="number" min="1" value="50">
          </div>
          <div class="form-group col-md-2">
            <label class="mb-1">Action</label>
            <button data-apca-add-condition class="btn btn-success btn-block" type="button">Add by Condition</button>
          </div>
        </div>
        <div class="form-group">
          <label class="mb-1">Problem index</label>
          <div data-apca-indexes class="apca-index-list">
            ${indexCheckbox("__all", true)}${INDEXES.map((v) => indexCheckbox(v)).join("")}
          </div>
        </div>
        <div class="form-row align-items-end">
          <div class="form-group col-md-2">
            <label class="mb-1">Point mode</label>
            <select data-apca-point-mode class="form-control">
              <option value="unset" selected>Unset</option>
              <option value="set">Set</option>
            </select>
          </div>
          <div class="form-group col-md-2">
            <label class="mb-1">Point</label>
            <input data-apca-point class="form-control" type="number" min="0" disabled>
          </div>
          <div class="form-group col-md-2">
            <button data-apca-set-points class="btn btn-outline-primary btn-block" type="button">Set All Points</button>
          </div>
          <div class="form-group col-md-2">
            <button data-apca-preview class="btn btn-outline-secondary btn-block" type="button">Count Candidates</button>
          </div>
          <div class="form-group col-md-2">
            <button data-apca-dedupe class="btn btn-outline-primary btn-block" type="button">Remove Duplicates</button>
          </div>
          <div class="form-group col-md-2">
            <button data-apca-remove-all class="btn btn-outline-danger btn-block" type="button">Remove All</button>
          </div>
        </div>
        <div class="form-row align-items-end">
          <div class="form-group col-md-6">
            <button data-apca-sort class="btn btn-outline-primary btn-block" type="button">Sort Problems</button>
          </div>
          <div class="form-group col-md-6">
            <button data-apca-shuffle class="btn btn-outline-primary btn-block" type="button">Shuffle Problems</button>
          </div>
        </div>
        <div data-apca-status style="display:none"></div>
      </div>`;
    return div;
  }

  function bind() {
    const r = root();
    if (!r || r.dataset.bound === VERSION) return;
    r.dataset.bound = VERSION;
    $("[data-apca-series]", r)?.addEventListener("change", () => setBusy(false));
    $("[data-apca-mode]", r)?.addEventListener("change", () => setBusy(false));
    $("[data-apca-point-mode]", r)?.addEventListener("change", () => setBusy(false));
    $("[data-apca-indexes]", r)?.addEventListener("change", onIndexChange);
    $("[data-apca-add-contest]", r)?.addEventListener("click", addContest);
    $("[data-apca-preview]", r)?.addEventListener("click", countCandidates);
    $("[data-apca-add-condition]", r)?.addEventListener("click", addByCondition);
    $("[data-apca-set-points]", r)?.addEventListener("click", setAllPointsCurrent);
    $("[data-apca-dedupe]", r)?.addEventListener("click", removeDuplicatesCurrent);
    $("[data-apca-remove-all]", r)?.addEventListener("click", removeAllCurrent);
    $("[data-apca-sort]", r)?.addEventListener("click", sortCurrent);
    $("[data-apca-shuffle]", r)?.addEventListener("click", shuffleCurrent);
    const last = sessionStorage.getItem(STATUS_KEY);
    if (last) {
      show(last, "success");
      sessionStorage.removeItem(STATUS_KEY);
    } else {
      show("");
    }
    setBusy(false);
  }

  function insert() {
    updateDocumentTitle();
    LEGACY_PANEL_IDS.forEach((id) => document.getElementById(id)?.remove());
    if (!isCreatePage()) {
      root()?.remove();
      return;
    }

    const currentPanel = root();
    if (currentPanel && currentPanel.dataset.apcaVersion !== VERSION) currentPanel.remove();

    if (!root()) {
      const div = createPanel();
      const heading = $$("h1,h2").find((el) => /Create Contest|Contest Create/i.test(el.textContent || ""));
      if (heading) (heading.closest(".row") || heading).insertAdjacentElement("afterend", div);
      else ($(".container") || $("main") || document.body).prepend(div);
    }
    bind();
    startRestore();
    startPointApply();
  }

  function main() {
    insert();
    new MutationObserver(insert).observe(document.documentElement, { childList: true, subtree: true });
    addEventListener("hashchange", () => {
      restoreStarted = false;
      pointsApplyStarted = false;
      setTimeout(insert, 50);
    });
    setInterval(insert, 1000);
    setInterval(syncPointCache, 1000);
  }

  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", main, { once: true });
  else main();
})();