<commit_before>// Copyright (c) ppy Pty Ltd <<EMAIL>>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

import AchievementJson from 'interfaces/achievement-json';
import BeatmapPlaycountJson from 'interfaces/beatmap-playcount-json';
import BeatmapsetExtendedJson from 'interfaces/beatmapset-extended-json';
import CurrentUserJson from 'interfaces/current-user-json';
import EventJson from 'interfaces/event-json';
import GameMode from 'interfaces/game-mode';
import KudosuHistoryJson from 'interfaces/kudosu-history-json';
import { PageSection as PageSectionJson, PageSectionWithoutCount as PageSectionWithoutCountJson } from 'interfaces/profile-page/extras-json';
import { ScoreCurrentUserPinJson } from 'interfaces/score-json';
import SoloScoreJson, { isSoloScoreJsonForUser, SoloScoreJsonForUser } from 'interfaces/solo-score-json';
import UserCoverJson from 'interfaces/user-cover-json';
import { ProfileExtraPage, profileExtraPages } from 'interfaces/user-extended-json';
import UserMonthlyPlaycountJson from 'interfaces/user-monthly-playcount-json';
import UserReplaysWatchedCountJson from 'interfaces/user-replays-watched-count-json';
import { route } from 'laroute';
import { debounce, keyBy, pullAt } from 'lodash';
import { action, makeObservable, observable } from 'mobx';
import core from 'osu-core-singleton';
import { error, onErrorWithCallback } from 'utils/ajax';
import { jsonClone } from 'utils/json';
import { hideLoadingOverlay, showLoadingOverlay } from 'utils/loading-overlay';
import { apiShowMore, apiShowMoreRecentlyReceivedKudosu, hasMoreCheck } from 'utils/offset-paginator';
import { switchNever } from 'utils/switch-never';
import { ProfilePageSection, ProfilePageUserJson } from './extra-page-props';

const sectionToUrlType = {
  favouriteBeatmapsets: 'favourite',
  graveyardBeatmapsets: 'graveyard',
  guestBeatmapsets: 'guest',
  lovedBeatmapsets: 'loved',
  pendingBeatmapsets: 'pending',
  rankedBeatmapsets: 'ranked',
  scoresBest: 'best',
  scoresFirsts: 'firsts',
  scoresPinned: 'pinned',
  scoresRecent: 'recent',
} as const;

export function validPage(page: unknown) {
  if (typeof page === 'string' && (page === 'main' || profileExtraPages.includes(page as ProfileExtraPage))) {
    return page as Page;
  }

  return null;
}

interface BeatmapsetsJson {
  favourite: PageSectionJson<BeatmapsetExtendedJson>;
  graveyard: PageSectionJson<BeatmapsetExtendedJson>;
  guest: PageSectionJson<BeatmapsetExtendedJson>;
  loved: PageSectionJson<BeatmapsetExtendedJson>;
  pending: PageSectionJson<BeatmapsetExtendedJson>;
  ranked: PageSectionJson<BeatmapsetExtendedJson>;
}

interface HistoricalJson {
  beatmap_playcounts: PageSectionJson<BeatmapPlaycountJson>;
  monthly_playcounts: UserMonthlyPlaycountJson[];
  recent: PageSectionJson<SoloScoreJsonForUser>;
  replays_watched_counts: UserReplaysWatchedCountJson[];
}

interface InitialData {
  achievements: AchievementJson[];
  beatmaps: BeatmapsetsJson;
  current_mode: GameMode;
  historical: HistoricalJson;
  kudosu: {
    items: KudosuHistoryJson[];
    per_page: number;
  };
  recent_activity: {
    items: EventJson[];
    per_page: number;
  };
  scores_notice: string | null;
  top_ranks: TopScoresJson;
  user: ProfilePageUserJson;
}

export type Page = ProfileExtraPage | 'main';

interface ScorePinReorderParams {
  order1_score_id?: ScoreCurrentUserPinJson['score_id'];
  order3_score_id?: ScoreCurrentUserPinJson['score_id'];
  score_id: ScoreCurrentUserPinJson['score_id'];
  score_type: ScoreCurrentUserPinJson['score_type'];
}

interface State {
  beatmapsets: BeatmapsetsJson;
  currentPage: Page;
  editingUserPage: boolean;
  historical: HistoricalJson;
  kudosu: PageSectionWithoutCountJson<KudosuHistoryJson>;
  recentActivity: PageSectionWithoutCountJson<EventJson>;
  topsScores: TopScoresJson;
  user: ProfilePageUserJson;
}

interface TopScoresJson {
  best: PageSectionJson<SoloScoreJsonForUser>;
  firsts: PageSectionJson<SoloScoreJsonForUser>;
  pinned: PageSectionJson<SoloScoreJsonForUser>;
}

export default class Controller {
  readonly achievements: Partial<Record<string, AchievementJson>>;
  readonly currentMode: GameMode;
  @observable currentPage: Page = 'main';
  readonly debouncedSetDisplayCoverUrl = debounce((url: string | null) => this.setDisplayCoverUrl(url), 300);
  @observable displayCoverUrl: string | null;
  readonly hasSavedState: boolean;
  @observable isUpdatingCover = false;<commit_msg>use mapped types for less repetition<commit_after>// Copyright (c) ppy Pty Ltd <<EMAIL>>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

import AchievementJson from 'interfaces/achievement-json';
import BeatmapPlaycountJson from 'interfaces/beatmap-playcount-json';
import BeatmapsetExtendedJson from 'interfaces/beatmapset-extended-json';
import CurrentUserJson from 'interfaces/current-user-json';
import EventJson from 'interfaces/event-json';
import GameMode from 'interfaces/game-mode';
import KudosuHistoryJson from 'interfaces/kudosu-history-json';
import { PageSection as PageSectionJson, PageSectionWithoutCount as PageSectionWithoutCountJson } from 'interfaces/profile-page/extras-json';
import { ScoreCurrentUserPinJson } from 'interfaces/score-json';
import SoloScoreJson, { isSoloScoreJsonForUser, SoloScoreJsonForUser } from 'interfaces/solo-score-json';
import UserCoverJson from 'interfaces/user-cover-json';
import { ProfileExtraPage, profileExtraPages } from 'interfaces/user-extended-json';
import UserMonthlyPlaycountJson from 'interfaces/user-monthly-playcount-json';
import UserReplaysWatchedCountJson from 'interfaces/user-replays-watched-count-json';
import { route } from 'laroute';
import { debounce, keyBy, pullAt } from 'lodash';
import { action, makeObservable, observable } from 'mobx';
import core from 'osu-core-singleton';
import { error, onErrorWithCallback } from 'utils/ajax';
import { jsonClone } from 'utils/json';
import { hideLoadingOverlay, showLoadingOverlay } from 'utils/loading-overlay';
import { apiShowMore, apiShowMoreRecentlyReceivedKudosu, hasMoreCheck } from 'utils/offset-paginator';
import { switchNever } from 'utils/switch-never';
import { ProfilePageSection, ProfilePageUserJson } from './extra-page-props';

const sectionToUrlType = {
  favouriteBeatmapsets: 'favourite',
  graveyardBeatmapsets: 'graveyard',
  guestBeatmapsets: 'guest',
  lovedBeatmapsets: 'loved',
  pendingBeatmapsets: 'pending',
  rankedBeatmapsets: 'ranked',
  scoresBest: 'best',
  scoresFirsts: 'firsts',
  scoresPinned: 'pinned',
  scoresRecent: 'recent',
} as const;

type PageSectionLazyLoadJson<T> = Omit<PageSectionJson<T>, 'pagination'>;

// #region lazy loaded extra pages
const beatmapsetsExtraPageKeys = ['favourite', 'graveyard', 'guest', 'loved', 'pending', 'ranked'] as const;
type BeatmapsetsJson = Record<typeof beatmapsetsExtraPageKeys[number], PageSectionJson<BeatmapsetExtendedJson>>;
type BeatmapsetsLazyLoadResponse = Record<typeof beatmapsetsExtraPageKeys[number], PageSectionLazyLoadJson<BeatmapsetExtendedJson>>;

interface HistoricalJson {
  beatmap_playcounts: PageSectionJson<BeatmapPlaycountJson>;
  monthly_playcounts: UserMonthlyPlaycountJson[];
  recent: PageSectionJson<SoloScoreJsonForUser>;
  replays_watched_counts: UserReplaysWatchedCountJson[];
}

interface HistoricalLazyLoadResponse {
  beatmap_playcounts: PageSectionLazyLoadJson<BeatmapPlaycountJson>;
  monthly_playcounts: UserMonthlyPlaycountJson[];
  recent: PageSectionLazyLoadJson<SoloScoreJsonForUser>;
  replays_watched_counts: UserReplaysWatchedCountJson[];
}

const topScoresKeys = ['best', 'firsts', 'pinned'] as const;
type TopScoresJson = Record<typeof topScoresKeys[number], PageSectionJson<SoloScoreJsonForUser>>;
type TopScoresLazyLoadResponse = Record<typeof topScoresKeys[number], PageSectionLazyLoadJson<SoloScoreJsonForUser>>;
// #endregion

function convertLazyLoadJson<T>(json: PageSectionLazyLoadJson<T>): PageSectionJson<T> {
  return {
    ...json,
    pagination: { hasMore: json.count > json.items.length },
  };
}

export function validPage(page: unknown) {
  if (typeof page === 'string' && (page === 'main' || profileExtraPages.includes(page as ProfileExtraPage))) {
    return page as Page;
  }

  return null;
}

interface InitialData {
  achievements: AchievementJson[];
  beatmaps: BeatmapsetsJson;
  current_mode: GameMode;
  historical: HistoricalJson;
  kudosu: {
    items: KudosuHistoryJson[];
    per_page: number;
  };
  recent_activity: {
    items: EventJson[];
    per_page: number;
  };
  scores_notice: string | null;
  top_ranks: TopScoresJson;
  user: ProfilePageUserJson;
}

export type Page = ProfileExtraPage | 'main';

interface ScorePinReorderParams {
  order1_score_id?: ScoreCurrentUserPinJson['score_id'];
  order3_score_id?: ScoreCurrentUserPinJson['score_id'];
  score_id: ScoreCurrentUserPinJson['score_id'];
  score_type: ScoreCurrentUserPinJson['score_type'];
}

interface State {
  beatmapsets: BeatmapsetsJson;
  currentPage: Page;
  editingUserPage: boolean;
  historical: HistoricalJson;
  kudosu: PageSectionWithoutCountJson<KudosuHistoryJson>;
  recentActivity: PageSectionWithoutCountJson<EventJson>;
  topsScores: TopScoresJson;
  user: ProfilePageUserJson;
}

export default class Controller {
  readonly achievements: Partial<Record<string, AchievementJson>>;
  readonly currentMode: GameMode;
  @observable currentPage: Page = 'main';
  readonly debouncedSetDisplayCoverUrl = debounce((url: string | null) => this.setDisplayCoverUrl(url), 300);
  @observable displayCoverUrl: string | null;
  readonly hasSavedState: boolean;
  @observable isUpdatingCover = false;
{'fraction_non_alphanumeric': 0.10148641722193746, 'fraction_numerical': 0.0014351614556637623, 'mean_word_length': 6.855072463768116, 'pattern_counts': {'":': 0, '<': 44, '<?xml version=': 0, '>': 47, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3741916', 'n_tokens_mistral': 3027, 'n_tokens_neox': 2977, 'n_words': 665}