<commit_before><?php

// 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.

namespace App\Models\Multiplayer;

use App\Exceptions\InvariantException;
use App\Libraries\Multiplayer\Mod;
use App\Libraries\Multiplayer\Ruleset;
use App\Models\Beatmap;
use App\Models\Model;

/**
 * @property json|null $allowed_mods
 * @property Beatmap $beatmap
 * @property int $beatmap_id
 * @property \Carbon\Carbon|null $created_at
 * @property int $id
 * @property int|null $playlist_order
 * @property json|null $required_mods
 * @property Room $room
 * @property int $room_id
 * @property int|null $ruleset_id
 * @property \Illuminate\Database\Eloquent\Collection $scores RoomScore
 * @property \Carbon\Carbon|null $updated_at
 */
class PlaylistItem extends Model
{
    protected $table = 'multiplayer_playlist_items';
    protected $casts = [
        'allowed_mods' => 'object',
        'required_mods' => 'object',
    ];

    public static function assertBeatmapsExist(array $playlistItems)
    {
        $requestedBeatmapIds = array_map(function ($item) {
            return $item->beatmap_id;
        }, $playlistItems);

        $beatmapIds = Beatmap::whereIn('beatmap_id', $requestedBeatmapIds)->pluck('beatmap_id')->all();
        $missing = array_diff($requestedBeatmapIds, $beatmapIds);

        if ($missing !== []) {
            $missingText = implode(', ', $missing);
            throw new InvariantException("beatmaps not found: {$missingText}");
        }
    }

    public static function fromJsonParams($json)
    {
        $obj = new self;
        foreach (['beatmap_id', 'ruleset_id'] as $field) {
            $obj->$field = array_get($json, $field);
            if (!present($obj->$field)) {
                throw new InvariantException("{$field} is required.");
            }
        }

        $obj->allowed_mods = Mod::parseInputArray(
            array_get($json, 'allowed_mods') ?? [],
            $obj->ruleset_id
        );

        $obj->required_mods = Mod::parseInputArray(
            array_get($json, 'required_mods') ?? [],
            $obj->ruleset_id
        );

        return $obj;
    }

    public function room()
    {
        return $this->belongsTo(Room::class, 'room_id');
    }

    public function beatmap()
    {
        return $this->belongsTo(Beatmap::class, 'beatmap_id')->withTrashed();
    }

    public function scores()
    {
        return $this->hasMany(RoomScore::class);
    }

    public function topScores()
    {
        $scores = $this->scores()->completed()->get();

        // sort by total_score desc and then date asc if scores are equal
        $baseResult = $scores->sort(function ($a, $b) {
            if ($a->total_score === $b->total_score) {
                if ($a->ended_at->timestamp === $b->ended_at->timestamp) {
                    // On the rare chance that both were submitted in the same second, default to submission order
                    return ($a->id < $b->id) ? -1 : 1;
                }

                return ($a->ended_at->timestamp < $b->ended_at->timestamp) ? -1 : 1;
            }

            return ($a->total_score > $b->total_score) ? -1 : 1;
        });

        $result = [];
        $users = [];

        foreach ($baseResult as $entry) {
            if (isset($users[$entry->user_id])) {
                continue;
            }

            // if (count($result) >= $limit) {
            //     break;
            // }

            $users[$entry->user_id] = true;
            $result[] = $entry;
        }

        return $result;
    }

    private function validateRuleset()
    {
        // osu beatmaps can be played in any mode, but non-osu maps can only be played in their specific modes
        if ($this->beatmap->playmode !== Ruleset::OSU && $this->beatmap->playmode !== $this->ruleset_id) {
            throw new InvariantException("invalid ruleset_id for beatmap {$this->beatmap->beatmap_id}");
        }
    }

    private function validateModOverlaps()
    {
        $dupeMods = array_intersect(
            array_column($this->allowed_mods, 'acronym'),
            array_column($this->required_mods, 'acronym')
        );

        if (count($dupeMods) > 0) {
            throw new InvariantException('mod cannot be listed as both allowed and required: '.implode(', ', $dupeMods));<commit_msg>use PlaylistItemUserHighScore to get topScores()<commit_after><?php

// 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.

namespace App\Models\Multiplayer;

use App\Exceptions\InvariantException;
use App\Libraries\Multiplayer\Mod;
use App\Libraries\Multiplayer\Ruleset;
use App\Models\Beatmap;
use App\Models\Model;

/**
 * @property json|null $allowed_mods
 * @property Beatmap $beatmap
 * @property int $beatmap_id
 * @property \Carbon\Carbon|null $created_at
 * @property int $id
 * @property int|null $playlist_order
 * @property json|null $required_mods
 * @property Room $room
 * @property int $room_id
 * @property int|null $ruleset_id
 * @property \Illuminate\Database\Eloquent\Collection $scores RoomScore
 * @property \Carbon\Carbon|null $updated_at
 */
class PlaylistItem extends Model
{
    protected $table = 'multiplayer_playlist_items';
    protected $casts = [
        'allowed_mods' => 'object',
        'required_mods' => 'object',
    ];

    public static function assertBeatmapsExist(array $playlistItems)
    {
        $requestedBeatmapIds = array_map(function ($item) {
            return $item->beatmap_id;
        }, $playlistItems);

        $beatmapIds = Beatmap::whereIn('beatmap_id', $requestedBeatmapIds)->pluck('beatmap_id')->all();
        $missing = array_diff($requestedBeatmapIds, $beatmapIds);

        if ($missing !== []) {
            $missingText = implode(', ', $missing);
            throw new InvariantException("beatmaps not found: {$missingText}");
        }
    }

    public static function fromJsonParams($json)
    {
        $obj = new self;
        foreach (['beatmap_id', 'ruleset_id'] as $field) {
            $obj->$field = array_get($json, $field);
            if (!present($obj->$field)) {
                throw new InvariantException("{$field} is required.");
            }
        }

        $obj->allowed_mods = Mod::parseInputArray(
            array_get($json, 'allowed_mods') ?? [],
            $obj->ruleset_id
        );

        $obj->required_mods = Mod::parseInputArray(
            array_get($json, 'required_mods') ?? [],
            $obj->ruleset_id
        );

        return $obj;
    }

    public function room()
    {
        return $this->belongsTo(Room::class, 'room_id');
    }

    public function beatmap()
    {
        return $this->belongsTo(Beatmap::class, 'beatmap_id')->withTrashed();
    }

    public function highScores()
    {
        return $this->hasMany(PlaylistItemUserHighScore::class);
    }

    public function scores()
    {
        return $this->hasMany(RoomScore::class);
    }

    public function topScores()
    {
        $scores = $this->highScores()->get();

        // sort by total_score desc and then date asc if scores are equal
        // no index on scores or timestamps
        return $scores->sort(function ($a, $b) {
            if ($a->total_score === $b->total_score) {
                if ($a->updated_at->timestamp === $b->updated_at->timestamp) {
                    // On the rare chance that both were submitted in the same second, default to submission order
                    return $a->id < $b->id;
                }

                return $a->updated_at->timestamp < $b->updated_at->timestamp;
            }

            return $b->total_score - $a->total_score;
        })->values();
    }

    private function validateRuleset()
    {
        // osu beatmaps can be played in any mode, but non-osu maps can only be played in their specific modes
        if ($this->beatmap->playmode !== Ruleset::OSU && $this->beatmap->playmode !== $this->ruleset_id) {
            throw new InvariantException("invalid ruleset_id for beatmap {$this->beatmap->beatmap_id}");
        }
    }

    private function validateModOverlaps()
    {
        $dupeMods = array_intersect(
            array_column($this->allowed_mods, 'acronym'),
            array_column($this->required_mods, 'acronym')
        );

        if (count($dupeMods) > 0) {
            throw new InvariantException('mod cannot be listed as both allowed and required: '.implode(', ', $dupeMods));
{'fraction_non_alphanumeric': 0.13468913422428822, 'fraction_numerical': 0.0013945380592678676, 'mean_word_length': 2.425955414012739, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 98, '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': '1996246', 'n_tokens_mistral': 2604, 'n_tokens_neox': 2522, 'n_words': 602}