package lila.tournament import com.github.ghik.silencer.silent import reactivemongo.akkastream.{ cursorProducer, AkkaStreamCursor } import reactivemongo.api._ import reactivemongo.api.bson._ import BSONHandlers._ import lila.db.dsl._ import lila.hub.LightTeam.TeamID import lila.rating.Perf import lila.user.{ Perfs, User } final class PlayerRepo(coll: Coll)(implicit ec: scala.concurrent.ExecutionContext) { private def selectId(id: Tournament.ID) = $doc("_id" -> id) private def selectTour(tourId: Tournament.ID) = $doc("tid" -> tourId) private def selectTourUser(tourId: Tournament.ID, userId: User.ID) = $doc( "tid" -> tourId, "uid" -> userId ) private val selectActive = $doc("w" $ne true) private val selectWithdraw = $doc("w" -> true) private val bestSort = $doc("m" -> -1) def byId(id: Tournament.ID): Fu[Option[Player]] = coll.one[Player](selectId(id)) private[tournament] def bestByTour(tourId: Tournament.ID, nb: Int, skip: Int = 0): Fu[List[Player]] = coll.ext.find(selectTour(tourId)).sort(bestSort).skip(skip).list[Player](nb) private[tournament] def bestByTourWithRank( tourId: Tournament.ID, nb: Int, skip: Int = 0 ): Fu[RankedPlayers] = bestByTour(tourId, nb, skip).map { res => res .foldRight(List.empty[RankedPlayer] -> (res.size + skip)) { case (p, (res, rank)) => (RankedPlayer(rank, p) :: res, rank - 1) } ._1 } private[tournament] def bestByTourWithRankByPage( tourId: Tournament.ID, nb: Int, page: Int ): Fu[RankedPlayers] = bestByTourWithRank(tourId, nb, (page - 1) * nb) // very expensive private[tournament] def bestTeamIdsByTour( tourId: Tournament.ID, battle: TeamBattle ): Fu[List[TeamBattle.RankedTeam]] = { import TeamBattle.{ RankedTeam, TeamLeader } coll .aggregateList(maxDocs = 10) { framework => import framework._ Match(selectTour(tourId)) -> List( Sort(Descending("m")), GroupField("t")( "m" -> Push( $doc( "u" -> "$uid", "m" -> "$m" ) ) ), Project( $doc( "p" -> $doc( "$slice" -> $arr("$m", battle.nbLeaders) ) ) ) ) } .map { _.flatMap { doc => for { teamId <- doc.getAsOpt[TeamID]("_id") leadersBson <- doc.getAsOpt[List[Bdoc]]("p") leaders = leadersBson.flatMap { case p: Bdoc => for { id <- p.getAsOpt[User.ID]("u") magic <- p.int("m") } yield TeamLeader(id, magic) } } yield RankedTeam(0, teamId, leaders) }.sortBy(-_.magicScore).zipWithIndex map { case (rt, pos) => rt.copy(rank = pos + 1) } } map { ranked => if (ranked.size == battle.teams.size) ranked else ranked ::: battle.teams .foldLeft(List.empty[RankedTeam]) { case (missing, team) if !ranked.exists(_.teamId == team) => RankedTeam(missing.headOption.fold(ranked.size)(_.rank) + 1, team, Nil) :: missing case (acc, _) => acc } .reverse } } // very expensive private[tournament] def teamInfo( tourId: Tournament.ID, teamId: TeamID, @silent battle: TeamBattle ): Fu[TeamBattle.TeamInfo] = { coll .aggregateWith[Bdoc]() { framework => import framework._ Match(selectTour(tourId) ++ $doc("t" -> teamId)) -> List( Sort(Descending("m")), Facet( List( "agg" -> { Group(BSONNull)( "nb" -> SumAll, "rating" -> AvgField("r"), "perf" -> AvgField("e"), "score" -> AvgField("s") ) -> Nil }, "topPlayers" -> { Limit(50) -> Nil } ) ) ) } .headOption .map { _.flatMap { doc => for { aggs <- doc.getAsOpt[List[Bdoc]]("agg") agg <- aggs.headOption nbPlayers <- agg.int("nb") rating = agg.double("rating").??(math.round) perf = agg.double("perf").??(math.round) score = agg.double("score").??(math.round) topPlayers <- doc.getAsOpt[List[Player]]("topPlayers") } yield TeamBattle.TeamInfo(teamId, nbPlayers, rating.toInt, perf.toInt, score.toInt, topPlayers) } | TeamBattle.TeamInfo(teamId, 0, 0, 0, 0, Nil) } } def bestTeamPlayers(tourId: Tournament.ID, teamId: TeamID, nb: Int): Fu[List[Player]] = coll.ext.find($doc("tid" -> tourId, "t" -> teamId)).sort($sort desc "m").list[Player](nb) def countTeamPlayers(tourId: Tournament.ID, teamId: TeamID): Fu[Int] = coll.countSel($doc("tid" -> tourId, "t" -> teamId)) def teamsOfPlayers(tourId: Tournament.ID, userIds: List[User.ID]): Fu[List[(User.ID, TeamID)]] = coll.ext .find($doc("tid" -> tourId, "uid" $in userIds), $doc("_id" -> false, "uid" -> true, "t" -> true)) .list[Bdoc]() .map { _.flatMap { doc => doc.getAsOpt[User.ID]("uid") flatMap { userId => doc.getAsOpt[TeamID]("t") map { (userId, _) } } } } def teamVs(tourId: Tournament.ID, game: lila.game.Game): Fu[Option[TeamBattle.TeamVs]] = game.twoUserIds ?? { case (w, b) => teamsOfPlayers(tourId, List(w, b)).dmap(_.toMap) map { m => (m.get(w) |@| m.get(b)).tupled ?? { case (wt, bt) => TeamBattle.TeamVs(chess.Color.Map(wt, bt)).some } } } def countActive(tourId: Tournament.ID): Fu[Int] = coll.countSel(selectTour(tourId) ++ selectActive) def count(tourId: Tournament.ID): Fu[Int] = coll.countSel(selectTour(tourId)) def removeByTour(tourId: Tournament.ID) = coll.delete.one(selectTour(tourId)).void def remove(tourId: Tournament.ID, userId: User.ID) = coll.delete.one(selectTourUser(tourId, userId)).void def filterExists(tourIds: List[Tournament.ID], userId: User.ID): Fu[List[Tournament.ID]] = coll.primitive[Tournament.ID]( $doc( "tid" $in tourIds, "uid" -> userId ), "tid" ) def existsActive(tourId: Tournament.ID, userId: User.ID) = coll.exists(selectTourUser(tourId, userId) ++ selectActive) def exists(tourId: Tournament.ID, userId: User.ID) = coll.exists(selectTourUser(tourId, userId)) def unWithdraw(tourId: Tournament.ID) = coll.update .one( selectTour(tourId) ++ selectWithdraw, $doc("$unset" -> $doc("w" -> true)), multi = true ) .void def find(tourId: Tournament.ID, userId: User.ID): Fu[Option[Player]] = coll.ext.find(selectTourUser(tourId, userId)).one[Player] def update(tourId: Tournament.ID, userId: User.ID)(f: Player => Fu[Player]) = find(tourId, userId) orFail s"No such player: $tourId/$userId" flatMap f flatMap { player => coll.update.one(selectId(player._id), player).void } def join(tourId: Tournament.ID, user: User, perfLens: Perfs => Perf, team: Option[TeamID]) = find(tourId, user.id) flatMap { case Some(p) if p.withdraw => coll.update.one(selectId(p._id), $unset("w")) case Some(_) => funit case None => coll.insert.one(Player.make(tourId, user, perfLens, team)) } void def withdraw(tourId: Tournament.ID, userId: User.ID) = coll.update.one(selectTourUser(tourId, userId), $set("w" -> true)).void private[tournament] def withPoints(tourId: Tournament.ID): Fu[List[Player]] = coll.ext .find( selectTour(tourId) ++ $doc("m" $gt 0) ) .list[Player]() private[tournament] def userIds(tourId: Tournament.ID): Fu[List[User.ID]] = coll.distinctEasy[User.ID, List]("uid", selectTour(tourId)) private[tournament] def nbActiveUserIds(tourId: Tournament.ID): Fu[Int] = coll.countSel(selectTour(tourId) ++ selectActive) def winner(tourId: Tournament.ID): Fu[Option[Player]] = coll.ext.find(selectTour(tourId)).sort(bestSort).one[Player] // freaking expensive (marathons) private[tournament] def computeRanking(tourId: Tournament.ID): Fu[Ranking] = coll .aggregateWith[Bdoc]() { framework => import framework._ Match(selectTour(tourId)) -> List( Sort(Descending("m")), Group(BSONNull)("uids" -> PushField("uid")) ) } .headOption map { _ ?? { _ get "uids" match { case Some(BSONArray(uids)) => // mutable optimized implementation val b = Map.newBuilder[User.ID, Int] var r = 0 for (u <- uids) { b += (u.asInstanceOf[BSONString].value -> r) r = r + 1 } b.result case _ => Map.empty } } } def computeRankOf(player: Player): Fu[Int] = coll.countSel(selectTour(player.tourId) ++ $doc("m" $gt player.magicScore)) // expensive, cache it private[tournament] def averageRating(tourId: Tournament.ID): Fu[Int] = coll .aggregateWith[Bdoc]() { framework => import framework._ Match(selectTour(tourId)) -> List( Group(BSONNull)("rating" -> AvgField("r")) ) } .headOption map { ~_.flatMap(_.double("rating").map(_.toInt)) } def byTourAndUserIds(tourId: Tournament.ID, userIds: Iterable[User.ID]): Fu[List[Player]] = coll.ext .find(selectTour(tourId) ++ $doc("uid" $in userIds)) .list[Player]() .chronometer .logIfSlow(200, logger) { players => s"PlayerRepo.byTourAndUserIds $tourId ${userIds.size} user IDs, ${players.size} players" } .result def pairByTourAndUserIds(tourId: Tournament.ID, id1: User.ID, id2: User.ID): Fu[Option[(Player, Player)]] = byTourAndUserIds(tourId, List(id1, id2)) map { case List(p1, p2) if p1.is(id1) && p2.is(id2) => Some(p1 -> p2) case List(p1, p2) if p1.is(id2) && p2.is(id1) => Some(p2 -> p1) case _ => none } def setPerformance(player: Player, performance: Int) = coll.update.one(selectId(player.id), $doc("$set" -> $doc("e" -> performance))).void private def rankPlayers(players: List[Player], ranking: Ranking): RankedPlayers = players .flatMap { p => ranking get p.userId map { RankedPlayer(_, p) } } .sortBy(_.rank) def rankedByTourAndUserIds( tourId: Tournament.ID, userIds: Iterable[User.ID], ranking: Ranking ): Fu[RankedPlayers] = byTourAndUserIds(tourId, userIds) .map { rankPlayers(_, ranking) } .chronometer .logIfSlow(200, logger) { players => s"PlayerRepo.rankedByTourAndUserIds $tourId ${userIds.size} user IDs, ${ranking.size} ranking, ${players.size} players" } .result def searchPlayers(tourId: Tournament.ID, term: String, nb: Int): Fu[List[User.ID]] = User.couldBeUsername(term) ?? { term.nonEmpty ?? coll.primitive[User.ID]( selector = $doc( "tid" -> tourId, "uid" $startsWith term.toLowerCase ), sort = $sort desc "m", nb = nb, field = "uid" ) } private[tournament] def sortedCursor( tournamentId: Tournament.ID, batchSize: Int, readPreference: ReadPreference = ReadPreference.secondaryPreferred ): AkkaStreamCursor[Player] = coll.ext .find(selectTour(tournamentId)) .sort($sort desc "m") .batchSize(batchSize) .cursor[Player](readPreference) }