class AdditionalReplayInfo(models.Model):
    """
    Infos that are only relevant to a few Replay objects are not worth their
    own attribute in the Replay class.
    """

    replay = models.ForeignKey(Replay, on_delete=CASCADE)
    key = models.CharField(max_length=32)
    value = models.CharField(max_length=512)

    def __unicode__(self):
        return "AdditionalReplayInfo({}, {}): '{}'='{}'".format(self.pk, self.replay, self.key, self.value) class Allyteam(models.Model):
    numallies = models.IntegerField()
    startrectbottom = models.FloatField(blank=True, null=True)
    startrectleft = models.FloatField(blank=True, null=True)
    startrectright = models.FloatField(blank=True, null=True)
    startrecttop = models.FloatField(blank=True, null=True)
    winner = models.BooleanField(default=False)
    replay = models.ForeignKey(Replay, on_delete=CASCADE)
    num = models.SmallIntegerField()

    def __unicode__(self):
        return "Allyteam({}) win:{}".format(self.id, self.winner) class BAwards(models.Model):
    replay = models.ForeignKey(Replay, on_delete=CASCADE)
    ecoKillAward1st = models.ForeignKey(Player, blank=True, null=True, related_name="+", on_delete=CASCADE)
    ecoKillAward1stScore = models.IntegerField(default=-1)
    ecoKillAward2nd = models.ForeignKey(Player, blank=True, null=True, related_name="+", on_delete=CASCADE)
    ecoKillAward2ndScore = models.IntegerField(default=-1)
    ecoKillAward3rd = models.ForeignKey(Player, blank=True, null=True, related_name="+", on_delete=CASCADE)
    ecoKillAward3rdScore = models.IntegerField(default=-1)
    fightKillAward1st = models.ForeignKey(Player, blank=True, null=True, related_name="+", on_delete=CASCADE)
    fightKillAward1stScore = models.IntegerField(default=-1)
    fightKillAward2nd = models.ForeignKey(Player, blank=True, null=True, related_name="+", on_delete=CASCADE)
    fightKillAward2ndScore = models.IntegerField(default=-1)
    fightKillAward3rd = models.ForeignKey(Player, blank=True, null=True, related_name="+", on_delete=CASCADE)
    fightKillAward3rdScore = models.IntegerField(default=-1)
    effKillAward1st = models.ForeignKey(Player, blank=True, null=True, related_name="+", on_delete=CASCADE)
    effKillAward1stScore = models.FloatField(default=-1)
    effKillAward2nd = models.ForeignKey(Player, blank=True, null=True, related_name="+", on_delete=CASCADE)
    effKillAward2ndScore = models.FloatField(default=-1)
    effKillAward3rd = models.ForeignKey(Player, blank=True, null=True, related_name="+", on_delete=CASCADE)
    effKillAward3rdScore = models.FloatField(default=-1)
    cowAward = models.ForeignKey(Player, blank=True, null=True, related_name="+", on_delete=CASCADE)
    cowAwardScore = models.IntegerField(default=-1)
    ecoAward = models.ForeignKey(Player, blank=True, null=True, related_name="+", on_delete=CASCADE)
    ecoAwardScore = models.IntegerField(default=-1)
    dmgRecAward = models.ForeignKey(Player, blank=True, null=True, related_name="+", on_delete=CASCADE)
    dmgRecAwardScore = models.FloatField(default=-1)
    sleepAward = models.ForeignKey(Player, blank=True, null=True, related_name="+", on_delete=CASCADE)
    sleepAwardScore = models.IntegerField(default=-1)

    def __unicode__(self):
        return (
            "BAwards({}) Replay: {} | EcoKill: {},{},{} | FightKill: {},{},{} | EffKill: {},{},{} | Cow: {} | "
            "Eco: {} | DmgRec: {} | Sleep: {}".format(
                self.pk,
                self.replay.pk,
                self.ecoKillAward1st,
                self.ecoKillAward2nd,
                self.ecoKillAward3rd,
                self.fightKillAward1st,
                self.fightKillAward2nd,
                self.fightKillAward3rd,
                self.effKillAward1st,
                self.effKillAward2nd,
                self.effKillAward3rd,
                self.cowAward,
                self.ecoAward,
                self.dmgRecAward,
                self.sleepAward,
            )
        ) class CursedAwards(models.Model):
    replay = models.ForeignKey(Replay, on_delete=CASCADE)
    player = models.ForeignKey(Player, on_delete=CASCADE)

    air = models.PositiveIntegerField(default=0)
    bug = models.PositiveIntegerField(default=0)
    cap = models.PositiveIntegerField(default=0)
    dran = models.PositiveIntegerField(default=0)
    drankill = models.PositiveIntegerField(default=0)
    emp = models.PositiveIntegerField(default=0)
    fire = models.PositiveIntegerField(default=0)
    friend = models.PositiveIntegerField(default=0)
    heart = models.PositiveIntegerField(default=0)
    hero = models.PositiveIntegerField(default=0)
    herokill = models.PositiveIntegerField(default=0)
    hover = models.PositiveIntegerField(default=0)
    kam = models.PositiveIntegerField(default=0)
    lycan = models.PositiveIntegerField(default=0)
    mex = models.PositiveIntegerField(default=0)
    mexkill = models.PositiveIntegerField(default=0)
    nux = models.PositiveIntegerField(default=0)
    ouch = models.PositiveIntegerField(default=0)
    point = models.PositiveIntegerField(default=0)
    pwn = models.PositiveIntegerField(default=0)
    rezz = models.PositiveIntegerField(default=0)
    share = models.PositiveIntegerField(default=0)
    shell = models.PositiveIntegerField(default=0)
    slow = models.PositiveIntegerField(default=0)
    sweeper = models.PositiveIntegerField(default=0)

    field_to_image = dict(
        air="trophy_air.png",
        bug="trophy_bug.png",
        cap="trophy_cap.png",
        dran="trophy_dran.png",
        drankill="trophy_dragon.png",
        emp="trophy_emp.png",
        fire="trophy_fire.png",
        friend="trophy_friend.png",
        heart="trophy_heart.png",
        hero="trophy_hero.png",
        herokill="trophy_herokill.png",
        hover="trophy_hover.png",
        kam="trophy_kam.png",
        lycan="trophy_lycan.png",
        mex="trophy_mex.png",
        mexkill="trophy_mexkill.png",
        nux="trophy_nux.png",
        ouch="trophy_ouch.png",
        point="trophy_point.png",
        pwn="trophy_pwn.png",
        rezz="trophy_rezz.png",
        share="trophy_share.png",
        shell="trophy_shell.png",
        slow="trophy_slow.png",
        sweeper="trophy_sweeper.png",
    )

    field_to_name = dict(
        air="Airforce General",
        bug="Bug Hunter",
        cap="Mind Magician",
        dran="Dragons & Angels",
        drankill="Dragon & Angel Slayer",
        emp="Stun Wizard",
        fire="Master Grill-Chef",
        friend="Friendly Fire Award",
        heart="Queen Heart Breaker",
        hero="Heros do the job",
        herokill="Hero Hunter",
        hover="Hover Admiral",
        kam="Kamikaze Award",
        lycan="Werewolf Hunter",
        mex="Mineral Prospector",
        mexkill="Loot & Pillage",
        nux="Apocalyptic Achievement Award",
        ouch="Big Purple Heart",
        point="This is my Land",
        pwn="Total Pain",
        rezz="Vile Necromancer",
        share="Share Bear",
        shell="Turtle Shell",
        slow="Traffic Cop",
        sweeper="Land Sweeper",
    )

    @classmethod
    def sorted_field_names(cls):
        return sorted([f.name for f in cls._meta.fields if f.name not in ("id", "replay", "player")])

    def as_dict(self, only_positive=False):
        res = dict()
        for field_name in self.sorted_field_names():
            val = getattr(self, field_name)
            if only_positive and not val > 0:
                continue
            res[field_name] = val
        return res

    def __unicode__(self):
        return "CursedAwards({}, {}, {})".format(
            self.pk,
            self.replay.pk,
            [
                (f.name, f.value_from_object(self))
                for f in self._meta.fields
                if f.name not in ("id", "replay", "player") and f.value_from_object(self) > 0
            ],
        ) class Map(models.Model):
    name = models.CharField(max_length=128, db_index=True)
    startpos = models.CharField(max_length=1024, blank=True, null=True)
    height = models.IntegerField()
    width = models.IntegerField()
    metadata2 = JSONTextField(blank=True, null=True, default=None)

    def __repr__(self):
        return "Map({}, {})".format(self.pk, self.name)

    def __unicode__(self):
        return self.name

    def get_absolute_url(self):
        return reverse("srs/rmap", args=[self.name])

    @property
    def replay_count(self):
        return Replay.objects.filter(map_info=self).count()

    class Meta:
        ordering = ["name"] class MapImg(models.Model):
    filename = models.CharField(max_length=128)
    startpostype = models.IntegerField(blank=True, null=True, verbose_name="-1 means full image")
    map_info = models.ForeignKey(Map, on_delete=CASCADE)

    def __unicode__(self):
        return "MapImg({}, {}, {})".format(self.pk, self.map_info.name, self.startpostype)

    def get_absolute_url(self):
        return "{}maps/{}".format(settings.STATIC_URL, self.filename) class MapOption(MapModOption):
    pass class ModOption(MapModOption):
    pass class Player(models.Model):
    account = models.ForeignKey(PlayerAccount, blank=True, null=True, on_delete=CASCADE)
    name = models.CharField(max_length=128, db_index=True)
    rank = models.SmallIntegerField()
    skill = models.CharField(max_length=16, blank=True)
    skilluncertainty = models.SmallIntegerField(default=-1, blank=True)
    spectator = models.BooleanField(default=False)
    team = models.ForeignKey("Team", blank=True, null=True, on_delete=CASCADE)
    replay = models.ForeignKey(Replay, on_delete=CASCADE)
    startposx = models.FloatField(blank=True, null=True)
    startposy = models.FloatField(blank=True, null=True)
    startposz = models.FloatField(blank=True, null=True)

    def __unicode__(self):
        return "Player({}, {})".format(self.id, self.name)

    def get_absolute_url(self):
        return reverse("srs/player", args=[self.account.accountid])

    class Meta:
        ordering = ["name"] class PlayerAccount(models.Model):
    accountid = models.IntegerField(unique=True)
    countrycode = models.CharField(max_length=2)
    preffered_name = models.CharField(max_length=128, db_index=True)
    sldb_privacy_mode = models.SmallIntegerField(default=1)

    def __unicode__(self):
        return "PlayerAccount({}: {})".format(
            self.accountid,
            reduce(lambda x, y: "{}|{}".format(x, y), self.get_names())[:40],
        )

    def get_absolute_url(self):
        return reverse("srs/player", args=[self.accountid])

    @property
    def replay_count(self):
        return Player.objects.filter(account__in=self).count()

    @property
    def spectator_count(self):
        return Player.objects.filter(account__in=self, spectator=True).count()

    def get_rating(self, game, match_type):
        try:
            rating, _ = Rating.objects.get_or_create(playeraccount=self, game=game, match_type=match_type)
            return rating
        except MultipleObjectsReturned:
            # this shouldn't happen, was a bug, now delete last object and return first
            ratings = Rating.objects.filter(playeraccount=self, game=game, match_type=match_type)
            ratings.last().delete()
            return ratings.first()

    def get_names(self):
        pls = list(Player.objects.filter(account=self).values_list("name", flat=True))
        if pls:
            try:
                pls.remove(self.preffered_name)
            except ValueError:
                pass  # Player with name "pref_name"  was removed from DB, but PlayerAccount stayed
            else:
                pls.insert(0, self.preffered_name)
            return uniqify_list(pls)
        else:
            return [self.preffered_name]

    def get_all_games(self):
        gametypes = Replay.objects.filter(player__account=self, player__spectator=False).values_list(
            "gametype", flat=True
        )
        return Game.objects.filter(gamerelease__name__in=uniqify_list(gametypes)).distinct()

    def get_all_games_no_bots(self):
        gametypes = (
            Replay.objects.filter(player__account=self, player__spectator=False)
            .exclude(tags=Tag.objects.get(name="Bot"))
            .values_list("gametype", flat=True)
        )
        return Game.objects.filter(gamerelease__name__in=uniqify_list(gametypes)).distinct()

    def get_user(self):
        try:
            return User.objects.get(userprofile__accountid=self.accountid)
        except ObjectDoesNotExist:
            return None

    @property
    def bawards(self):
        """
        Find number of won BA awards (for all award types).

        :return: dict: {'cowAward': 0, 'dmgRecAward': 2, 'ecoAward': 3, ...}
        """
        res = dict()
        field_names = [
            field.name for field in BAwards._meta.get_fields() if "Award" in field.name and "Score" not in field.name
        ]
        for award in field_names:
            kwargs = {"{}__account".format(award): self}
            res[award] = BAwards.objects.filter(**kwargs).count()
        return res

    @property
    def xtawards(self):
        """
        Find top 10 hero and "lost in service" units for this player. Sorted by headcount.

        :return: tuple: heroes, lost. each is a list of tuples:
            ([(u'Sumo', 9), (u'Light Laser Tower', 8), ...], [(), ..])
        """
        awards = XTAwards.objects.filter(player__account=self, isAlive=1).values_list("unit", flat=True)
        co = Counter(awards)
        heroes = co.most_common(10)
        awards = XTAwards.objects.filter(player__account=self, isAlive=0).values_list("unit", flat=True)
        co = Counter(awards)
        lost = co.most_common(10)
        return heroes, lost

    @property
    def cursed_awards(self):
        """
        Find number of won cursed awards (for all award types).

        :return: [{'type': 'air', 'name': '..', 'img': images/..', 'count': 12}, ...]
        """
        awards = CursedAwards.objects.filter(player__account=self)
        co = Counter()
        for award in awards:
            co.update(award.as_dict(True).keys())
        return [
            {
                "type": c[0],
                "name": CursedAwards.field_to_name[c[0]],
                "img": os.path.join("img/cursed_awards", CursedAwards.field_to_image[c[0]]),
                "count": c[1],
            }
            for c in co.most_common()
        ]

    class Meta:
        ordering = ["accountid"] class PlayerStats(models.Model):
    replay = models.ForeignKey(Replay, on_delete=CASCADE)
    stats = models.BinaryField(blank=True, null=True, default=None)

    @property
    def decompressed(self):
        return ujson.loads(zlib.decompress(self.stats))

    def __unicode__(self):
        return "PlayerStats(pk={}, replay.pk={}, {!r}...)".format(self.pk, self.replay.pk, self.stats[:4]) class RatingHistory(RatingBase):
    match = models.ForeignKey(Replay, on_delete=CASCADE)
    match_date = models.DateTimeField(
        blank=True, null=True, db_index=True
    )  # this fields is redundant, but neccessary for db-side ordering of tables

    def __unicode__(self):
        return "RatingHistory({}, {}): {}".format(self.pk, self.match_date, super(RatingHistory, self))

    class Meta:
        ordering = ["-match_date", "playername"] class Replay(models.Model):
    versionString = models.CharField(max_length=128)
    gameID = models.CharField(max_length=32, unique=True)
    unixTime = models.DateTimeField(verbose_name="date of match", db_index=True)
    wallclockTime = models.CharField(max_length=32, verbose_name="length of match")
    autohostname = models.CharField(max_length=128, blank=True, null=True, db_index=True)
    gametype = models.CharField(max_length=256, db_index=True)
    startpostype = models.IntegerField(blank=True, null=True)
    title = models.CharField(max_length=256, db_index=True)
    short_text = models.CharField(max_length=50, db_index=True)
    long_text = models.CharField(max_length=513, db_index=True)
    notcomplete = models.BooleanField(default=True)
    map_info = models.ForeignKey(Map, blank=True, null=True, on_delete=CASCADE)
    map_img = models.ForeignKey(MapImg, blank=True, null=True, on_delete=CASCADE)
    tags = models.ManyToManyField(Tag)
    uploader = models.ForeignKey(User, on_delete=CASCADE)
    upload_date = models.DateTimeField(auto_now_add=True, db_index=True)
    filename = models.CharField(max_length=256)
    path = models.CharField(max_length=256)
    download_count = models.IntegerField(default=0)
    comment_count = models.IntegerField(default=0)
    rated = models.BooleanField(default=False)
    published = models.BooleanField(default=False)

    def __unicode__(self):
        return "Replay({}, {!r}, {!r})".format(self.pk, self.gameID, self.title)

    def to_dict(self):
        return {
            "id": self.id,
            "versionString": self.versionString,
            "gameID": self.gameID,
            "unixTime": self.unixTime,
            "wallclockTime": self.wallclockTime,
            "autohostname": self.autohostname,
            "gametype": self.gametype,
            "startpostype": self.startpostype,
            "title": self.title,
            "short_text": self.short_text,
            "long_text": self.long_text,
            "map_name": self.map_info.name,
            "map_startpos": self.map_info.startpos,
            "map_width": self.map_info.width,
            "map_height": self.map_info.height,
            "tags": ",".join(self.tags.all().values_list("name", flat=True)) if self.tags.exists() else "",
            "uploader": self.uploader.username,
            "upload_date": self.upload_date,
        }

    def get_absolute_url(self):
        return reverse("srs/replay", args=[str(self.gameID)])

    @property
    def was_succ_uploaded(self):
        return not UploadTmp.objects.filter(replay=self).exists()

    class Meta:
        get_latest_by = "upload_date"
        ordering = ["-upload_date"]

    @property
    def game(self):
        return self.game_release.game

    @property
    def game_release(self):
        try:
            gr = GameRelease.objects.get(name=self.gametype)
            return gr
        except ObjectDoesNotExist:
            # new game release
            pass

        # look for existing game name
        game = None
        for i in range(len(self.gametype)):
            _game = Game.objects.filter(name=self.gametype[: -1 * i])
            if _game.count() == 1:
                game = _game.first()
                break
            elif _game.count() > 1:
                msg = "Found %r Games with name %r: %r.\ngametype: %r\ngameID: %r\nmatch: %r." % (
                    _game.count(),
                    self.gametype[: -1 * i],
                    _game,
                    self.gametype,
                    self.gameID,
                    self.__unicode__(),
                )
                logger.error(msg)
                send_mail([x[1] for x in settings.ADMINS], "SRS: ambiguous game name", msg)
            else:
                pass
        if not game:
            logger.debug("not game")
            msg = "Unknown gametype: %r\ngameID: %r\nmatch: %r." % (
                self.gametype,
                self.gameID,
                self.__unicode__(),
            )
            logger.error(msg)
            send_mail([x[1] for x in settings.ADMINS], "SRS: Unknown game name", msg)

            game_name = str()
            version = str()
            in_version = False
            for gr_name_part in self.gametype.split():
                version_start = ["v", "V", "b", "("]
                version_start.extend(map(str, range(10)))
                if (
                    in_version
                    or gr_name_part.upper().startswith("TEST")
                    or gr_name_part.upper().startswith("RC")
                    or gr_name_part[0] in version_start
                ):
                    in_version = True
                    version += "{} ".format(gr_name_part)
                else:
                    game_name += "{} ".format(gr_name_part)

            game_name = game_name.rstrip()
            while game_name[-1] in [" ", "-"]:
                game_name = game_name[:-1]
        else:
            game_name = game.name
            version = self.gametype[len(game.name) :]
        version = version.strip()
        if not version:
            version = "n/a"

        if version[0].upper() == "V" or version[0] == ".":
            version = version[1:]

        game, _ = Game.objects.get_or_create(
            name=game_name,
            defaults={"abbreviation": reduce(lambda x, y: x + y, [gn[0].upper() for gn in game_name.split()])},
        )
        return GameRelease.objects.create(name=self.gametype, game=game, version=version)

    @property
    def match_type(self):
        """returns string (from searching through tags): 1v1 / Team / FFA / TeamFFA'"""
        # quick and dirty using tags
        try:
            tag = self.tags.filter(name__regex=r"^[0-9]v[0-9]$")[0]
            if tag.name == "1v1":
                return "1v1"
            else:
                return "Team"
        except IndexError:
            pass
        try:
            tag = self.tags.get(name__regex=r"^TeamFFA$")
            return tag.name
        except ObjectDoesNotExist:
            pass
        try:
            tag = self.tags.get(name__regex=r"^FFA$")
            return tag.name
        except ObjectDoesNotExist:
            pass

        # thoroughly using player/team count
        allyteams = Allyteam.objects.filter(replay=self)
        if allyteams.count() == 2:
            if PlayerAccount.objects.filter(player__team__allyteam__in=allyteams).exclude(accountid=0).count() == 2:
                return "1v1"
            else:
                return "Team"
        elif allyteams.count() > 2:
            if (
                PlayerAccount.objects.filter(player__team__allyteam__in=allyteams).exclude(accountid=0).count()
                == allyteams.count()
            ):
                return "FFA"
            else:
                return "TeamFFA"
        else:
            # this is kind of a broken match, but not returning anything breaks the web site
            return "1v1"

    @property
    def match_type_short(self):
        """returns string (from match_type()): 1 / T / F / G"""
        if self.match_type == "TeamFFA":
            return "G"
        else:
            return self.match_type[0]

    @property
    def num_players(self):
        """returns string (from counting players): 1v1 / 1v5 / 6v6 / 2v2v2v2 / ..."""
        try:
            allyteams = Allyteam.objects.filter(replay=self)
            at_sizes = [PlayerAccount.objects.filter(player__team__allyteam=at).count() for at in allyteams]
            at_sizes.sort()
            return str(reduce(lambda x, y: "{}v{}".format(x, y), at_sizes))
        except:
            logger.exception("FIXME: to broad exception handling.")
            return "?v?"

    @property
    def match_end(self):
        length = self.wallclockTime.split(":")
        try:
            length2 = datetime.timedelta(seconds=int(length[2]), minutes=int(length[1]), hours=int(length[0]))
        except KeyError:
            return self.unixTime
        return self.unixTime + length2

    @property
    def duration_ISO_8601(self):
        length = self.wallclockTime.split(":")
        try:
            return "PT{0}H{1}M{2}S".format(*length)
        except IndexError:
            return self.wallclockTime

    playername = "N/A"  # something must exists, or PlayersReplayTable will always have an empty column

    def _playername(self, playeraccount):
        player = Player.objects.filter(replay=self, account=playeraccount)[
            0
        ]  # not using get() for the case of a spec-cheater
        return player.name

    def _faction_result(self, playeraccount):
        """
        Create cached entries, so this function (and the DB queries) runs only once per line in PlayersReplayTable.
        """
        try:
            player = Player.objects.get(replay=self, account=playeraccount)
        except MultipleObjectsReturned:  # happens on Bots player page
            if playeraccount.accountid == 0:
                player = Player.objects.filter(replay=self, account=playeraccount)[0]
            else:
                player = Player.objects.get(replay=self, account=playeraccount, spectator=False)
        try:
            team = Team.objects.filter(replay=self, teamleader=player)[0]
            if team.allyteam.winner:
                self._result_cache = (playeraccount, "won")
            else:
                self._result_cache = (playeraccount, "lost")
            self._faction_cache = (playeraccount, team.side)
        except (ObjectDoesNotExist, IndexError):
            self._result_cache = (playeraccount, "spec")
            self._faction_cache = (playeraccount, "spec")

    faction = "N/A"  # something must exists, or PlayersReplayTable will always have an empty column

    def _faction(self, playeraccount):
        """
        This is used by PlayersReplayTable
        """
        if not hasattr(self, "_faction_cache") or not self._faction_cache[0] == playeraccount:
            self._faction_result(playeraccount)
        return self._faction_cache[1]

    result = "N/A"  # something must exists, or PlayersReplayTable will always have an empty column

    def _result(self, playeraccount):
        """
        This is used by PlayersReplayTable
        """
        if not hasattr(self, "_result_cache") or not self._result_cache[0] == playeraccount:
            self._faction_result(playeraccount)
        return self._result_cache[1]

    @property
    def bawards(self):
        try:
            return BAwards.objects.get(replay=self)
        except ObjectDoesNotExist:
            return None

    @property
    def cursed_awards(self):
        """
        :return: [('shell', {
            'players': [(<Player a>, 12), (<Player b>, 34)],
            'img': 'trophy_shell.png',
            'name': 'Turtle Shell'
            }),
        ...]
        """
        res = dict()
        ca_fields = CursedAwards.sorted_field_names()
        try:
            for ca in CursedAwards.objects.filter(replay=self):
                for fn in ca_fields:
                    cav = getattr(ca, fn)
                    if cav:
                        res.setdefault(fn, {})["name"] = CursedAwards.field_to_name[fn]
                        res[fn]["img"] = os.path.join("img/cursed_awards", CursedAwards.field_to_image[fn])
                        res[fn].setdefault("players", []).append((ca.player, cav))
                        if len(res[fn]["players"]) > 1:
                            res[fn]["players"] = sorted(
                                res[fn]["players"],
                                key=operator.itemgetter(1),
                                reverse=True,
                            )
        except ObjectDoesNotExist:
            return None
        return sorted(res.items(), key=operator.itemgetter(0)) class SrsTiming(object):
    def __init__(self):
        self.times = dict()
        self.counter = 0

    def start(self, name):
        self.times[name] = (self.counter, datetime.datetime.now(), name)
        self.counter += 1

    def stop(self, name):
        self.times[name] = (
            self.times[name][0],
            datetime.datetime.now() - self.times[name][1],
            name,
        )

    @property
    def sorted_list(self):
        return sorted(self.times.values(), key=operator.itemgetter(0))

    def __str__(self):
        return "\n".join([f"{t[2]}: {t[1].seconds}.{int(t[1].microseconds / 10000)}" for t in self.sorted_list]) class Tag(models.Model):
    name = models.CharField(max_length=128, unique=True)

    def __repr__(self):
        return "Tag({}, {})".format(self.pk, self.name)

    def __unicode__(self):
        return self.name

    def get_absolute_url(self):
        return reverse("srs/tag", args=[self.name])

    @property
    def replay_count(self):
        return Replay.objects.filter(tags=self).count() class Team(models.Model):
    allyteam = models.ForeignKey(Allyteam, on_delete=CASCADE)
    handicap = models.IntegerField()
    rgbcolor = models.CharField(max_length=23)
    side = models.CharField(max_length=32)
    startposx = models.IntegerField(blank=True, null=True)
    startposy = models.IntegerField(blank=True, null=True)
    startposz = models.IntegerField(blank=True, null=True)
    teamleader = models.ForeignKey(Player, related_name="+", on_delete=CASCADE)
    replay = models.ForeignKey(Replay, on_delete=CASCADE)
    num = models.SmallIntegerField()

    def __unicode__(self):
        return "Team({}, {})".format(self.pk, self.teamleader.name) class TeamStats(models.Model):
    replay = models.ForeignKey(Replay, on_delete=CASCADE)
    stat_type = models.CharField(max_length=32)
    stats = models.BinaryField(blank=True, null=True, default=None)

    graphid2label = OrderedDict(
        (
            ("metalUsed", "Metal Used"),
            ("metalProduced", "Metal Produced"),
            ("metalExcess", "Metal Excess"),
            ("metalReceived", "Metal Received"),
            ("metalSent", "Metal Sent"),
            ("energyUsed", "Energy Used"),
            ("energyProduced", "Energy Produced"),
            ("energyExcess", "Energy Excess"),
            ("energyReceived", "Energy Received"),
            ("energySent", "Energy Sent"),
            ("damageDealt", "Damage Dealt"),
            ("damageReceived", "Damage Received"),
            ("unitsProduced", "Units Produced"),
            ("unitsKilled", "Units Killed"),
            ("unitsDied", "Units Died"),
            ("unitsReceived", "Units Received"),
            ("unitsSent", "Units Sent"),
            ("unitsCaptured", "Units Captured"),
            ("unitsOutCaptured", "Units Stolen"),
        )
    )

    @property
    def decompressed(self):
        return ujson.loads(zlib.decompress(self.stats))

    @property
    def label(self):
        return self.graphid2label[self.stat_type]

    def __unicode__(self):
        return "TeamStats(pk={}, replay.pk={}, stat_type={}, {!r}...)".format(
            self.pk, self.replay.pk, self.stat_type, self.stats[:4]
        ) class UploadTmp(models.Model):
    replay = models.ForeignKey(Replay, on_delete=CASCADE)

    def __unicode__(self):
        return "UploadTmp({}, {})".format(self.pk, self.replay) class XTAwards(models.Model):
    replay = models.ForeignKey(Replay, on_delete=CASCADE)
    isAlive = models.SmallIntegerField(default=-1)
    player = models.ForeignKey(Player, on_delete=CASCADE)
    unit = models.CharField(max_length=1024)
    kills = models.IntegerField(default=-1)
    age = models.IntegerField(default=-1)

    def __unicode__(self):
        return "XTAwards({}) Replay: {} | isAlive: {} | player: {} | unit: {} | kills: {} | age: {}".format(
            self.pk,
            self.replay.pk,
            self.isAlive,
            self.player.name,
            self.unit,
            self.kills,
            self.age,
        ) class Parse_demo_file:
    BA_platform_data_to_API = {
        "CPU": "cpuName",
        "CPU cores": "cpuCores",
        "RAM": "ram",
        "GPU": "glRenderer",
        "GPU VRAM": "gpuMemorySize",
        "OS": "osFamily",
    }

    def __init__(self, filename):
        self.filename = filename
        self.demofile = None
        self.header = dict()
        self.game_setup = {
            "ai": {},
            "allyteam": {},
            "mapoptions": {},
            "modoptions": {},
            "player": {},
            "restrict": {},
            "team": {},
            "host": {},
        }
        self.additional = {
            "gameover_frame": False,
            "chat": list(),
            "faction_change": dict(),
            "not_connected": dict(),
            "quit": dict(),
            "kicked": dict(),
            "timeout": dict(),
        }
        self.script = ""
        self.player_stats = None
        self.team_stats = None
        self.stats_thread = None
        self.tmp_stats = {}

    def read_blob(self, seek_size: int, blob_size: int) -> bytes:
        self.demofile.seek(seek_size)
        return self.demofile.read(blob_size)

    def read_blob_into_int(self, seek_size, blob_size):
        return unpack("i", self.read_blob(seek_size, blob_size))

    def check_magic(self):
        """
        - may raise IOError when opening a file to read
        - may raise Exception when file is not a spring demofile
        """
        filemagic = magic.from_file(self.filename, mime=True)

        if filemagic.endswith("gzip"):
            myopen = gzip.open
        else:
            myopen = open
        with myopen(self.filename, "rb") as demofile:
            demofile.seek(0)
            _magic = demofile.read(16)
            if not _magic.startswith(b"spring demofile"):
                raise BadFileType("Not a spring demofile.")

    def parse(self):
        """
        reads data from sdf, populates self.header and self.game_setup
        - may raise IOError when opening a file to read or write
        - may raise Exception when file is not a spring demofile
        """
        filemagic = magic.from_file(self.filename, mime=True)
        if filemagic.endswith("gzip"):
            myopen = gzip.open
        else:
            myopen = open
        # Example 20161016_233916_Charlie in the Hills v2_103.sdfz file size = 1104963 byte
        #         20161016_233916_Charlie in the Hills v2_103.sdf  file size = 3187862 byte

        # {'demoStreamSize': 3088877,
        #  'gameID': 'f34405581adec916289cf47c6b65bf7c',
        #  'gameTime': '0:28:45',
        #  'headerSize': 352,
        #  'magic': 'spring demofile',
        #  'numPlayers': 23,
        #  'numTeams': 10,
        #  'playerStatElemSize': 20,
        #  'playerStatSize': 460,
        #  'scriptSize': 4533,
        #  'teamStatElemSize': 80,
        #  'teamStatPeriod': 15,
        #  'teamStatSize': 93640,
        #  'unixTime': '2016-10-16 23:39:16',
        #  'version': 5,
        #  'versionString': '103',
        #  'wallclockTime': '0:30:43',
        #  'winningAllyTeamsSize': 1}
        self.start_stats_thread()
        with myopen(self.filename, "rb") as self.demofile:
            self.parse_header()  # pos:        0 -     352 (0 -> headerSize)
            self.parse_script()  # pos:      352 -    4885 (headerSize -> + scriptSize)
            self.parse_demostream()  # pos: 4885 - 3093743 (headerSize + scriptSize -> + demoStreamSize | END)
            #                                                        352 + 4533 + 3088877 = 3093762
            # self.parse_player_stats() # pos: ? -       ? (numPlayers * playerStatElemSize = playerStatSize
            #                                                         -> 23 * 20 = 460)
            # self.parse_team_stats()   # pos: ? -       ? (teamStatSize = numTeams * char/uint (size of teams stats)
            #                                                              + numTeams * teams stats * teamStatElemSize)
            self.parse_winningAllyTeams()  # pos: 3093762 - 3093763 (headerSize + scriptSize + demoStreamSize
            #                                                        -> + winningAllyTeamsSize)
        logger.debug("Waiting for stats thread...")
        self.join_stats_thread()
        logger.info("Stats thread finished.")
        logger.debug("len(self.player_stats)=%d", len(self.player_stats))
        logger.debug("len(self.team_stats)=%d", len(self.team_stats))

    def parse_header(self):
        #
        # struct DemoFileHeader from rts/System/LoadSave/demofile.h
        #
        self.header["magic"] = self.read_blob(0, 16)  # char magic[16]; ///< DEMOFILE_MAGIC
        if not self.header["magic"].startswith(b"spring demofile"):
            raise Exception("Not a spring demofile.")
        self.header["version"] = self.read_blob_into_int(16, 4)[0]  # int version; ///< DEMOFILE_VERSION
        self.header["headerSize"] = self.read_blob_into_int(20, 4)[
            0
        ]  # int headerSize; ///< Size of the DemoFileHeader, minor version number.
        if self.header["version"] > 4:  # springrts commit ddf5be4f8a7006dcb870c5736b1a59c455df0535
            self.header["versionString"] = self.read_blob(
                24, 256
            )  # char versionString[256]; ///< Spring version string, e.g. "0.75b2", "0.75b2+svn4123"
        else:
            self.header["versionString"] = self.read_blob(24, 16)
        self.header["gameID"] = self.read_blob(
            280, 16
        )  # boost::uint8_t gameID[16]; ///< Unique game identifier. Identical for each player of the game.
        self.header["unixTime"] = self.read_blob(
            296, 8
        )  # boost::uint64_t unixTime; ///< Unix time when game was started.
        self.header["scriptSize"] = self.read_blob_into_int(304, 4)[0]  # int scriptSize; ///< Size of startscript.
        self.header["demoStreamSize"] = self.read_blob_into_int(308, 4)[
            0
        ]  # int demoStreamSize; ///< Size of the demo stream.
        self.header["gameTime"] = self.read_blob_into_int(312, 4)[
            0
        ]  # int gameTime; ///< Total number of seconds game time.
        self.header["wallclockTime"] = self.read_blob_into_int(316, 4)[
            0
        ]  # int wallclockTime; ///< Total number of seconds wallclock time.
        self.header["numPlayers"] = self.read_blob_into_int(320, 4)[
            0
        ]  # int numPlayers; ///< Number of players for which stats are saved.
        self.header["playerStatSize"] = self.read_blob_into_int(324, 4)[
            0
        ]  # int playerStatSize; ///< Size of the entire player statistics chunk.
        self.header["playerStatElemSize"] = self.read_blob_into_int(328, 4)[
            0
        ]  # int playerStatElemSize; ///< sizeof(CPlayer::Statistics)
        self.header["numTeams"] = self.read_blob_into_int(332, 4)[
            0
        ]  # int numTeams; ///< Number of teams for which stats are saved.
        self.header["teamStatSize"] = self.read_blob_into_int(336, 4)[
            0
        ]  # int teamStatSize; ///< Size of the entire team statistics chunk.
        self.header["teamStatElemSize"] = self.read_blob_into_int(340, 4)[
            0
        ]  # int teamStatElemSize; ///< sizeof(CTeam::Statistics)
        self.header["teamStatPeriod"] = self.read_blob_into_int(344, 4)[
            0
        ]  # int teamStatPeriod; ///< Interval (in seconds) between team stats.
        self.header["winningAllyTeamsSize"] = self.read_blob_into_int(348, 4)[
            0
        ]  # int winningAllyTeamsSize; ///< The size of the vector of the winning ally teams

        #        self.header['void_swab']            = self.read_blob(352, self.header['headerSize']-352)    # lol?

        #        demo_stream = self.read_blob(self.demofile.tell(), self.header['demoStreamSize'])
        #        i = 0
        #        player_stats = []
        #        while i < self.header['numPlayers']:
        #            player_stats[i] = self.read_blob(self.demofile.tell(), self.header['playerStatElemSize'])
        #            i += 1
        self.header["magic"] = self.header["magic"].decode("utf-8").partition("\x00")[0]
        self.header["versionString"] = self.header["versionString"].decode("utf-8").partition("\x00")[0]
        self.header["gameID"] = "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" % unpack(
            "16B", self.header["gameID"]
        )
        self.header["unixTime"] = "%s" % strftime(
            "%Y-%m-%d %H:%M:%S", localtime(unpack("Q", self.header["unixTime"])[0])
        )
        self.header["gameTime"] = "%s" % str(timedelta(seconds=self.header["gameTime"]))
        self.header["wallclockTime"] = "%s" % str(timedelta(seconds=self.header["wallclockTime"]))
        logger.debug("self.header=\n%s", pprint.pformat(self.header))
        return self.header

    def parse_winningAllyTeams(self):
        self.winningAllyTeams = []
        winnning_team = 0
        self.demofile.seek(self.header["headerSize"] + self.header["scriptSize"] + self.header["demoStreamSize"])
        while winnning_team < self.header["winningAllyTeamsSize"]:
            blob = self.read_blob(self.demofile.tell(), 1)
            logger.debug("blob=%r", blob)
            team = unpack("B", blob)
            self.winningAllyTeams.append(team[0])
            winnning_team += 1
        logger.debug("self.winningAllyTeams=%r", self.winningAllyTeams)
        return self.winningAllyTeams

    def parse_script(self):
        script = self.read_blob(self.header["headerSize"], self.header["scriptSize"])
        game = re.match(rb"^\[game\]\n\{(?P<data>.*)\}\n", script, re.DOTALL).groupdict()
        game_data = game["data"].replace(b"\n", b"").replace(b"[options]{}", b"")
        section_iter = re.finditer(rb"\[(?P<name>.*?)\]\{(?P<data>.*?)\}", game_data, re.DOTALL)

        self.script = Script()

        for section_ in section_iter:
            section = section_.groupdict()
            if section and section["data"].strip():
                if section["name"].startswith(b"player"):
                    player = ScriptPlayer(section["name"], section["data"])
                    if player.spectator:
                        self.script.spectators[player.name] = player
                    else:
                        self.script.players[player.name] = player
                    self.game_setup["player"][player.num] = player.__dict__
                elif section["name"].startswith(b"ai"):
                    bot = ScriptAI(section["name"], section["data"])
                    if not hasattr(bot, "name"):
                        bot.name = section["name"]
                    self.script.bots[bot.name] = bot
                    self.game_setup["ai"][bot.name] = bot.__dict__
                elif section["name"].startswith(b"allyteam"):
                    ally = ScriptAlly(section["name"], section["data"])
                    self.script.allies.append(ally)
                    self.game_setup["allyteam"][ally.num] = ally.__dict__
                elif section["name"].startswith(b"team"):
                    team = ScriptTeam(section["name"], section["data"])
                    self.script.teams.append(team)
                    self.game_setup["team"][team.num] = team.__dict__
                elif section["name"] == "restrict":
                    self.script.restrictions = ScriptRestrictions(section["name"], section["data"])
                    self.game_setup["restrict"] = self.script.restrictions
                elif section["name"] == "mapoptions":
                    self.script.mapoptions = ScriptMapoptions(section["name"], section["data"])
                    self.game_setup["mapoptions"] = self.script.mapoptions.__dict__
                elif section["name"] == "modoptions":
                    self.script.modoptions = ScriptModoptions(section["name"], section["data"])
                    self.game_setup["modoptions"] = self.script.modoptions.__dict__
        game_txt = game["data"].split(b"}")[-1:][0].replace(b"\n", b"")
        self.game_setup["host"] = ScriptGamesetup("game_setup_host", game_txt).__dict__
        self.script.other["mapname"] = self.game_setup["host"]["mapname"]
        self.script.other["modname"] = self.game_setup["host"]["gametype"]
        # logger.debug("self.game_setup=\n%s", pprint.pformat(self.game_setup))
        logger.debug("self.script=\n%s", pprint.pformat(self.script.__dict__))
        return self.script

    def _read(self, length):
        return self.demofile.read(length)

    def _tell(self):
        return self.demofile.tell()

    def _readPacket(self):
        maxSize = self.header["demoStreamSize"] or sys.maxsize
        if maxSize:
            modGameTime, length = struct.unpack("<fI", self._read(8))
            if self._tell() + length - self.header["headerSize"] - self.header["scriptSize"] == maxSize:
                return False
            data = self._read(length)
        if not data:
            return False
        return {"modGameTime": modGameTime, "length": length, "data": data}

    @classmethod
    def _ba_stats_to_platform_api(cls, ba_stats_str):
        stats = {}
        for m in re.finditer(rb"^(?P<name>\w+): *(?P<value>.*)$", ba_stats_str[3:], re.MULTILINE):
            pl_stats = m.groupdict()
            if re.match(rb".*bit.*Hz", pl_stats["value"]):
                # found resolution, color depth etc: >>1920x1058:24bit @60Hz (windowed)<<
                stats["resolution_x"] = pl_stats["name"].split(b"x")[0]
                stats["resolution_y"] = pl_stats["name"].split(b"x")[1]
                m = re.match(
                    rb"(?P<bit>\d+)bit @(?P<hz>\d+)Hz (?P<windowed>\(.*\))*",
                    pl_stats["value"],
                ).groupdict()
                stats["color_depth"] = m["bit"]
                stats["refresh_rate"] = m["hz"]
                stats["windowed"] = bool(m["windowed"])
            else:
                stats[cls.BA_platform_data_to_API[pl_stats["name"]]] = pl_stats["value"]
        try:
            # remove surplus spaces
            stats["cpuName"] = b" ".join(stats["cpuName"].split())
        except KeyError:
            pass
        try:
            # remove surplus 'MB'
            stats["ram"] = stats["ram"][:-2]
        except KeyError:
            pass
        for attr in (
            "gpuMemorySize",
            "resolution_x",
            "resolution_y",
            "color_depth",
            "refresh_rate",
            "sdlVersionCompiledMajor",
            "sdlVersionCompiledMinor",
            "sdlVersionCompiledPatch",
            "sdlVersionLinkedMajor",
            "sdlVersionLinkedMinor",
            "sdlVersionLinkedPatch",
            "ram",
        ):
            try:
                stats[attr] = int(stats[attr])
            except (KeyError, ValueError):
                pass
        return stats

    def parse_demostream(self):
        def _save_playerinfo(playername, key, value):
            if playername in self.players:
                setattr(self.players[playername], key, value)
                if key in ("quit", "kicked", "timeout"):
                    self.additional[key][playername] = self.players[playername]
            else:
                setattr(self.spectators[playername], key, value)

        def _dictify(obj):
            if isinstance(obj, dict):
                return obj
            else:
                return obj.__dict__

        self.players = self.script.players
        self.spectators = self.script.spectators
        self.bots = self.script.bots
        self.teams = self.script.teams
        self.allies = self.script.allies
        self.options = dict(
            list(_dictify(self.script.modoptions).items())
            + list(_dictify(self.script.other).items())
            + list(_dictify(self.script.mapoptions).items())
        )
        self.restrictions = self.script.restrictions
        self.gameid = self.header["gameID"]

        self.demofile.seek(self.header["headerSize"] + self.header["scriptSize"])
        packet = True
        currentFrame = 0
        playerIDToName = {}
        if DEBUG:
            kop = open("/tmp/msg.data", "wb")
            stats_fp = open("/tmp/stats.log", "wb")
            stats_fp.write("gameID: {}\n".format(self.header["gameID"]).encode())
        demoparser = Demoparser()
        while packet:
            packet = self._readPacket()
            try:
                messageData = demoparser.parsePacket(packet)
                if DEBUG:
                    kop.write(repr(messageData).encode() + b"\n")

                def clean(name: Union[bytes, str]) -> str:
                    if isinstance(name, bytes):
                        return name.replace(b"\x00", b"").decode()
                    else:
                        return name.replace("\x00", "")

                if messageData:
                    try:
                        clean_name = clean(messageData["playerName"])
                    except KeyError:
                        pass
                    if messageData["cmd"] == "keyframe":
                        currentFrame = messageData["framenum"]
                    elif messageData["cmd"] == "setplayername":
                        playerIDToName[messageData["playerNum"]] = clean_name
                        _save_playerinfo(clean_name, "connected", True)
                        _save_playerinfo(clean_name, "playerNum", messageData["playerNum"])
                    elif messageData["cmd"] == "startplaying" and messageData["countdown"] == 0:
                        self.game_started = True
                    elif messageData["cmd"] == "gameover":
                        if not self.game_started:
                            logger.error("game not started on gameover found")
                        else:
                            self.game_over = currentFrame
                            self.additional["gameover_frame"] = currentFrame
                    elif messageData["cmd"] == "gameid":
                        if self.header["gameID"] != messageData["gameID"]:
                            self.gameid = messageData["gameID"]
                            logger.error(
                                "messageData['gameID']: %s != self.header['gameID']: %s",
                                messageData["gameID"],
                                self.header["gameID"],
                            )
                    elif messageData["cmd"] == "playerleft":
                        playername = clean(messageData["playerName"])
                        if messageData["bIntended"] == 0:
                            _save_playerinfo(playername, "timeout", True)
                        if messageData["bIntended"] == 1:
                            _save_playerinfo(playername, "quit", True)
                        if messageData["bIntended"] == 2:
                            _save_playerinfo(playername, "kicked", True)
                    elif messageData["cmd"] == "team":
                        if clean_name in self.script.spectators.keys():
                            continue
                        if messageData["action"] == "team_died":  # team died event
                            deadTeam = messageData["param"]
                            for name, rank in self.players.items():
                                if rank.team == deadTeam:
                                    self.players[name].died = currentFrame
                        elif messageData["action"] == "giveaway":
                            # giving everything away == death
                            self.players[clean_name].died = currentFrame
                    elif messageData["cmd"] == "startpos":
                        if messageData["ready"] == 1:
                            playername = clean(messageData["playerName"])
                            if playername in self.players:
                                self.game_setup["player"][self.players[playername].num]["startposx"] = messageData["x"]
                                self.game_setup["player"][self.players[playername].num]["startposy"] = messageData["y"]
                                self.game_setup["player"][self.players[playername].num]["startposz"] = messageData["z"]
                    elif messageData["cmd"] == "chat":
                        self.additional["chat"].append(
                            {
                                "fromID": messageData["fromID"],
                                "playerName": messageData["playerName"],
                                "toID": messageData["toID"],
                                "message": messageData["message"][:-1],
                            }
                        )
                    elif messageData["cmd"] == "luamsg":
                        if messageData["msgid"] == 36:
                            if DEBUG:
                                stats_fp.write(
                                    "{}: >>{}<<\n".format(messageData["playerNum"], messageData["msg"]).encode()
                                )
                            if messageData["msg"][:6] == "$y$CPU":
                                # found BA platform stats
                                stats = self._ba_stats_to_platform_api(messageData["msg"])
                                if "cpuName" in stats and "osFamily" in stats:
                                    self.additional.setdefault("ba_platform_stats", {})[
                                        messageData["playerNum"]
                                    ] = stats
                                else:
                                    logger.warning(
                                        "Platform stats missing cpuName or osFamily in %r generated from msg=%r",
                                        stats,
                                        messageData["msg"],
                                    )
                        elif messageData["msgid"] == 49 and messageData["msg"].startswith(b"180"):
                            # https://springrts.com/phpbb/viewtopic.php?f=54&t=36150&p=581964#p581964
                            # CursedID-TeamID:awardtype/counter:awardtype/counter: ...
                            # e.g. "180-0:hero/150:hover/803:rezz/28"
                            teamid = re.findall(rb"^180-(\d+):?.*$", messageData["msg"])[0]
                            cursed_awards = re.findall(rb":((\w+)/(\d+))*", messageData["msg"])
                            self.additional.setdefault("cursed_awards", {})[teamid] = [
                                (ca[1], ca[2]) for ca in cursed_awards
                            ]
                        elif messageData["msgid"] == 138:
                            # faction change
                            playername = clean(messageData["playerName"])
                            faction = struct.unpack(
                                "<%iB" % (len(messageData["msg"][1:])),
                                messageData["msg"][1:],
                            )
                            logger.debug(
                                "%s(%d) changed faction to '%s'",
                                playername,
                                messageData["playerNum"],
                                faction,
                            )
                            self.additional["faction_change"][playername] = (
                                self.players[playername],
                                faction,
                            )
                        elif messageData["msgid"] == 161:
                            # BA Awards
                            # http://imolarpg.dyndns.org/trac/balatest/browser/trunk/luarules/gadgets/gui_awards.lua?rev=2070#L389
                            # history:
                            # r1786: start sending data from awards gadget to replay site using luauimsg
                            # r1846: use luamsg, change msg numbers from 150-156 to 161-167
                            # r1850: use luarulesmsg, add 1 to each team# to have always positive numbers
                            # r2043: the scores of each award are appended to the team#, separated by ':' (BA 7.91).
                            # r2070: fix typo causing fightKillAward2ndScore to always be 'nil'
                            try:
                                # unfortunately numbers in this sequence were encoded differently, so parsing
                                # is a little tricky, example:
                                # r1850: (len:  31): '\xa17\xa15\xa16\xa25\xa215\xa23\xa33\xa312\xa315\xa40\xa510\xa615\xa74'  # noqa: E501
                                # r2043: (len: 124): '\xa11:1147\xa10:0\xa10:0\xa21:946348\xa23:nil\xa22:102684\xa33:1.317666888237\xa31:1.2887364625931\xa32:0.75686019659042\xa40\xa51:6590859\xa63:56526.171875\xa70:0'  # noqa: E501
                                # (unpack'B'): (161, 55, 161, 53, 161, 54, 162, 53, 162, 49, 53, 162, 51, 163, 51, 163, 49, 50, 163, 49, 53, 164, 48, 165, 49, 48, 166, 49, 53, 167, 52)  # noqa: E501
                                # (unpack'c'): ('\xa1', '7', '\xa1', '5', '\xa1', '6', '\xa2', '5', '\xa2', '1', '5', '\xa2', '3', '\xa3', '3', '\xa3', '1', '2', '\xa3', '1', '5', '\xa4', '0', '\xa5', '1', '0', '\xa6', '1', '5', '\xa7', '4')  # noqa: E501
                                # award markers are ints 161-167 encoded in one unsigned char, but the
                                # player numbers are ints 0-31 encoded each digit as a single char
                                # ':' is 58 in str_B (and ':' in str_c)
                                str_B = struct.unpack("B" * len(messageData["msg"]), messageData["msg"])
                                str_c = struct.unpack("c" * len(messageData["msg"]), messageData["msg"])
                                start = end = 0
                                awards_data = list()
                                for pos in range(1, len(str_B)):
                                    if int(str_B[pos]) > 160:

                                        def f_(s):
                                            if s.find(b".") >= 0:
                                                return float(s)
                                            else:
                                                return int(s)

                                        end = pos
                                        awards_data.append(str_B[start])
                                        s_ = b"".join(str_c[start + 1 : end]).split(b":")
                                        try:
                                            awards_data.append([int(s_[0]) - 1, f_(s_[1])])
                                        except IndexError:
                                            # gadget version < r2043 has no ':score' after the teamID
                                            awards_data.append([int(s_[0]) - 1, -1])
                                        except ValueError:
                                            # beware typo in gadget:
                                            # http://imolarpg.dyndns.org/trac/balatest/changeset/2070
                                            awards_data.append([int(s_[0]) - 1, -2])
                                        start = end
                                else:
                                    awards_data.append(str_B[start])
                                    s_ = b"".join(str_c[start + 1 :]).split(b":")
                                    try:
                                        awards_data.append([int(s_[0]) - 1, f_(s_[1])])
                                    except IndexError:
                                        # gadget version < r2043 has no ':score' after the teamID
                                        awards_data.append([int(s_[0]) - 1, -1])
                                # Substract 1 from each players number.
                                # It was added in gadget to make all numbers positive.
                                awards = {
                                    "ecoKillAward": (
                                        awards_data[1],
                                        awards_data[3],
                                        awards_data[5],
                                    ),  # 161
                                    "fightKillAward": (
                                        awards_data[7],
                                        awards_data[9],
                                        awards_data[11],
                                    ),  # 162
                                    "effKillAward": (
                                        awards_data[13],
                                        awards_data[15],
                                        awards_data[17],
                                    ),  # 163
                                    "cowAward": awards_data[19],  # 164
                                    "ecoAward": awards_data[21],  # 165
                                    "dmgRecAward": awards_data[23],
                                }  # 166
                                if len(awards_data) > 24:
                                    awards["sleepAward"] = awards_data[25]  # 167
                                else:
                                    awards["sleepAward"] = [-1, -1]
                                self.additional["awards"] = awards
                            except Exception as exc:
                                logger.error("FIXME: to broad exception handling.")
                                logger.exception(
                                    "detecting BA Awards, messageData: %r, Exception: %s",
                                    messageData,
                                    exc,
                                )
                        elif messageData["msgid"] == 199:
                            # XTA Awards, 2014-04-01
                            # forum thread: http://springrts.com/phpbb/viewtopic.php?f=71&t=28019&start=120#p555847
                            # XTA_AWARDMARKER,":",isAlive,":",team,":",name,":",kills,":",age
                            # The fields for 'heroes award':
                            #   1: string '\199', identify XTA unit award
                            #   2: isAlive: 1
                            #   3: teamID, owner of the corresponding unit (0)
                            #   4: name of unit to get award (Maverick)
                            #   5: number of kills (27)
                            #   6: age of unit when game ends (4)
                            # The fields for 'lost in service award':
                            #   1: string '\199', identify XTA unit award
                            #   2: isAlive: 0
                            #   3: teamID, owner of the corresponding unit (0)
                            #   4: name of unit to get award (Commander)
                            #   5: number of kills (43)
                            #   6: age of unit when game ends (1)
                            (xta_isAlive, xta_team, xta_name, xta_kills, xta_age,) = messageData["msg"][
                                2:
                            ].split(b":")
                            xtawards = {
                                "isAlive": int(xta_isAlive),
                                "team": int(xta_team),
                                "name": xta_name,
                                "kills": int(xta_kills),
                                "age": int(xta_age),
                            }
                            try:
                                if xtawards not in self.additional["xtawards"]:
                                    self.additional["xtawards"].append(xtawards)
                            except KeyError:
                                self.additional["xtawards"] = [xtawards]
                        else:
                            # logger.debug('unknown luamsg messageData["msgid"]=%r messageData["msg"]=%r',
                            #   messageData["msgid"], messageData["msg"])
                            pass
            except Exception as exc:
                logger.error("FIXME: to broad exception handling.")
                logger.exception("Exception parsing packet '%s': %s", packet, exc)
                # raise e

        if DEBUG:
            kop.close()
            stats_fp.close()

        for pnum, player in self.players.items():
            if not hasattr(player, "connected"):
                self.additional["not_connected"][pnum] = player

    def start_stats_thread(self):
        if not self.stats_thread:
            self.tmp_stats = {}
            self.stats_thread = threading.Thread(target=self.get_match_stats, args=(self.filename, self.tmp_stats))
            self.stats_thread.start()
        return self.stats_thread

    def join_stats_thread(self):
        if self.stats_thread:
            self.stats_thread.join()
            self.stats_thread = None
            self.player_stats = self.tmp_stats["player_stats"]
            self.team_stats = self.tmp_stats["team_stats"]
            self.tmp_stats = {}

    @staticmethod
    def get_match_stats(filename, stats):
        match_stats_generation = MatchStatsGeneration(filename)
        stats["player_stats"], stats["team_stats"] = match_stats_generation.make_stats()

    def parse_player_stats(self):
        if not self.player_stats:
            self.start_stats_thread()
            self.join_stats_thread()
        return self.player_stats

    def parse_team_stats(self):
        if not self.team_stats:
            self.start_stats_thread()
            self.join_stats_thread()
        return self.team_stats

    def player_stats_as_jsonz(self):
        return zlib.compress(ujson.dumps(self.player_stats))

    def team_stats_as_jsonz(self):
        return dict((k, zlib.compress(ujson.dumps(v))) for k, v in self.team_stats.items()) class SLDBError(Exception):
class SLDBstatusError(SLDBError):
class SLDBbadArgumentError(SLDBError):
class SLDBConnectionError(SLDBError):
    def __init__(self, service, status):
    def __str__(self):
    def __init__(self, arg, val):
    def __str__(self):
def skill2rank(trueskill):
def privatize_skill(ts):
def demoskill2float(skill):
def _my_ip4_addresses():
def _query_sldb(service, *args, **kwargs):
def _get_PlayerAccount(accountid, privacy_mode=1, preffered_name=""):
def get_sldb_playerskill(game_abbr, accountids, user=None, privatize=True):
def get_sldb_pref(accountid, pref):
def set_sldb_pref(accountid, pref, value=None):
def get_sldb_match_skills(gameIDs):
def get_sldb_leaderboards(game, match_types=["1", "T", "F", "G", "L"]):
def get_sldb_player_stats(game_abbr, accountid):
def get_sldb_player_ts_history_graphs(game_abbr, accountid): class SpringMaps:
    def __init__(self, mapname):
        self.map_name = mapname
        self.full_image_filename = "{}.jpg".format(self.map_name)
        self.map_info = None
        self.thumb = None

    @property
    def full_base_image_filepath(self) -> str:
        return path_join(settings.MAPS_PATH, self.full_image_filename)

    @staticmethod
    def get_replay_image_filepath(map_name, gameID) -> str:
        return "{}_{}.jpg".format(map_name, gameID)

    @classmethod
    def get_full_replay_image_filepath(cls, map_name, gameID) -> str:
        filename = cls.get_replay_image_filepath(map_name, gameID)
        return path_join(settings.MAPS_PATH, filename)

    def fetch_info(self):
        """
        fetches map information from springfiles.springrts.com, stores it in self.map_info
        - may raise an Exception when connecting to server
        """
        try:
            self.map_info = fetch_map_info(self.map_name)
        except:
            logger.exception("FIXME: to broad exception handling.")
            logger.warning("Could not retrieve map-info for map %r.", self.map_name)
            self.map_info = None
        return self.map_info

    def fetch_img(self):
        """
        fetches map image from springfiles.springrts.com
        """
        if not self.map_info:
            self.fetch_info()
        if self.map_info:
            map_url = self.map_info[0]["mapimages"][0]
            logger.info(
                "Downloaded map image from %r into %r...",
                map_url,
                self.full_base_image_filepath,
            )
            response = requests.get(map_url, stream=True)
            if response.status_code == 200:
                with open(self.full_base_image_filepath, "wb") as fp:
                    for chunk in response:
                        fp.write(chunk)
            else:
                logger.error('Could not download image %r. Setting image to "map_img_not_avail.jpg".')
                copyfile(
                    path_join(settings.IMG_PATH, "map_img_not_avail.jpg"),
                    self.full_base_image_filepath,
                )
        else:
            logger.warning("We have no map-info, setting image to 'map_img_not_avail.jpg'.")
            copyfile(
                path_join(settings.IMG_PATH, "map_img_not_avail.jpg"),
                self.full_base_image_filepath,
            )
        return self.full_image_filename

    def make_home_thumb(self):
        if not self.map_info or not path_exists(self.full_base_image_filepath):
            self.fetch_img()
        image = Image.open(self.full_base_image_filepath)
        size = settings.THUMBNAIL_SIZES["home"]
        image.thumbnail(size, Image.ANTIALIAS)
        self.thumb = "{}_home.jpg".format(self.map_name)
        image.save(path_join(settings.MAPS_PATH, self.thumb), "JPEG")
        return self.thumb

    def create_map_with_boxes(self, replay):
        """
        create a map picture with start boxes (if any) and player start positions
        """
        img = Image.open(self.full_base_image_filepath)

        full_img_x, full_img_y = img.size
        # map positions for players are in pixel
        # map pixel size = Spring Map Size (like 16x16) * 512
        # from: http://springrts.com/wiki/Mapdev:diffuse#Image_File
        if replay.map_info.width > 128:
            # springfiles.springrts.com returnd pixel size
            map_px_x = replay.map_info.width
            map_px_y = replay.map_info.height
        else:
            # springfiles.springrts.com returnd Spring Map Size
            map_px_x = replay.map_info.width * 512
            map_px_y = replay.map_info.height * 512

        map_img_mult_x = map_px_x / full_img_x
        map_img_mult_y = map_px_y / full_img_y

        #
        # create start boxes
        #
        allyteams = Allyteam.objects.filter(
            replay=replay,
            startrectleft__isnull=False,
            startrecttop__isnull=False,
            startrectright__isnull=False,
            startrectbottom__isnull=False,
        )
        if allyteams.exists():
            # 6 defined colors
            colors = [
                (200, 0, 0),
                (0, 200, 0),
                (0, 0, 200),
                (200, 200, 0),
                (200, 0, 200),
                (0, 200, 200),
            ]
            if Allyteam.objects.filter(replay=replay).count() > 6:
                # random colors for the remaining AllyTeams
                for _ in range(0, Allyteam.objects.filter(replay=replay).count() - 6):
                    colors.append(
                        (
                            random.randint(0, 255),
                            random.randint(0, 255),
                            random.randint(0, 255),
                        )
                    )
            c_count = 0
            for at in allyteams:
                # transparency mask
                team_layer = Image.new(
                    "RGBA",
                    (
                        int(at.startrectright * full_img_x) - int(at.startrectleft * full_img_x),
                        int(at.startrectbottom * full_img_y) - int(at.startrecttop * full_img_y),
                    ),
                    (256, 256, 256, 96),
                )
                # draw border of box (no transparency)
                draw = ImageDraw.Draw(team_layer)
                tl_x, tl_y = team_layer.size
                for i in range(4):  # 4px border
                    draw.rectangle(
                        [(0 + i, 0 + i), (tl_x - i, tl_y - i)],
                        fill=None,
                        outline=(256, 256, 256, 256),
                    )
                del draw
                # overlay map with partly transparent box
                img.paste(
                    colors[c_count],
                    box=(
                        int(at.startrectleft * full_img_x),
                        int(at.startrecttop * full_img_y),
                        int(at.startrectright * full_img_x),
                        int(at.startrectbottom * full_img_y),
                    ),
                    mask=team_layer,
                )
                c_count += 1

        draw = ImageDraw.Draw(img)
        players = Player.objects.filter(
            replay=replay,
            spectator=False,
            startposx__isnull=False,
            startposz__isnull=False,
        )
        if players.exists():
            #
            # draw players actual start positions
            #
            font = ImageFont.truetype(path_join(settings.FONTS_PATH, "VeraMono.ttf"), 28)
            for player in players:
                pl_img_pos_x = player.startposx / map_img_mult_x
                pl_img_pos_y = player.startposz / map_img_mult_y  # z is in spring what y is in img
                team_color = ImageColor.getrgb("#{}".format(player.team.rgbcolor))
                # center number and circle above startpoint --> move up and left by text-size/2
                text_w, text_h = font.getsize(str(player.team.num))
                if player.team.num > 9:
                    w = 15
                else:
                    w = 0
                draw.ellipse(
                    (
                        pl_img_pos_x - 12 - text_w / 2,
                        pl_img_pos_y - 8 - text_h / 2,
                        pl_img_pos_x + 30 + w - text_w / 2,
                        pl_img_pos_y + 36 - text_h / 2,
                    ),
                    outline=team_color,
                    fill=None,
                )
                draw.ellipse(
                    (
                        pl_img_pos_x - 11 - text_w / 2,
                        pl_img_pos_y - 7 - text_h / 2,
                        pl_img_pos_x + 29 + w - text_w / 2,
                        pl_img_pos_y + 35 - text_h / 2,
                    ),
                    outline=team_color,
                    fill=None,
                )
                draw.ellipse(
                    (
                        pl_img_pos_x - 10 - text_w / 2,
                        pl_img_pos_y - 6 - text_h / 2,
                        pl_img_pos_x + 28 + w - text_w / 2,
                        pl_img_pos_y + 34 - text_h / 2,
                    ),
                    outline=team_color,
                    fill=None,
                )
                draw.ellipse(
                    (
                        pl_img_pos_x - 9 - text_w / 2,
                        pl_img_pos_y - 5 - text_h / 2,
                        pl_img_pos_x + 27 + w - text_w / 2,
                        pl_img_pos_y + 33 - text_h / 2,
                    ),
                    outline=team_color,
                    fill="black",
                )
                draw.text(
                    (pl_img_pos_x - text_w / 2, pl_img_pos_y - text_h / 2),
                    str(player.team.num),
                    font=font,
                )
        else:
            #
            # draw start positions saved in map
            #
            for startpos in [
                (float(pair.split(",")[0]), float(pair.split(",")[1])) for pair in replay.map_info.startpos.split("|")
            ]:
                draw_pos_x = startpos[0] / map_img_mult_x
                draw_pos_y = startpos[1] / map_img_mult_y
                draw.ellipse(
                    (draw_pos_x - 5, draw_pos_y - 5, draw_pos_x + 5, draw_pos_y + 5),
                    outline="white",
                    fill="green",
                )
        del draw

        # img.thumbnail(settings.THUMBNAIL_SIZES["replay"], Image.ANTIALIAS)
        full_path = self.get_full_replay_image_filepath(self.map_name, replay.gameID)
        try:
            img.save(full_path, "JPEG")
        except:
            logger.exception("FIXME: to broad exception handling.")
            logger.exception("Could not save %r.", full_path)
        return self.get_replay_image_filepath(self.map_name, replay.gameID)
import datetime
import errno
import gzip
import logging
import os
import pprint
import shutil
import stat
import coreapi
import django.contrib.auth
import django.utils.timezone
import magic
from os.path import join as path_join
from tempfile import mkstemp
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.sitemaps import ping_google
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Min
from .models import (
    AdditionalReplayInfo,
    Allyteam,
    BAwards,
    CursedAwards,
    Map,
    MapImg,
    MapOption,
    ModOption,
    Player,
    PlayerAccount,
    PlayerStats,
    RatingHistory,
    Replay,
    SrsTiming,
    Tag,
    Team,
    TeamStats,
    UploadTmp,
    XTAwards,
)
from .parse_demo_file import Parse_demo_file
from .sldb import SLDBError, demoskill2float, get_sldb_match_skills, sldb_gametype2matchtype
from .springmaps import SpringMaps

            # detect single player and add tag
            try:
                if demofile.game_setup["host"]["hostip"] == "" and demofile.game_setup["host"]["ishost"] == 1:
                    sptag, _ = Tag.objects.get_or_create(name="Single Player")
                    replay.tags.add(sptag)
            except KeyError:
                pass

    timer.stop("  teams")
    logger.debug(
        "replay(%d) saved Teams (%s)",
        replay.pk,
        Team.objects.filter(replay=replay).values_list("id"),
    )

    # work around XTA and Zero-Ks usage of empty AllyTeams
    at_empty = Allyteam.objects.filter(replay=replay, team__isnull=True)
    if at_empty.exists():
        logger.debug("replay(%s) deleting useless AllyTeams: %s", replay.pk, at_empty)
        at_empty.delete()

    timer.start("  map creation")
    # get / create map infos
    smap = SpringMaps(demofile.game_setup["host"]["mapname"])
    try:
        replay.map_info = Map.objects.get(name=demofile.game_setup["host"]["mapname"])
        logger.debug("replay(%d) using existing map_info.pk=%d", replay.pk, replay.map_info.pk)
        smap.full_image_filename = MapImg.objects.get(startpostype=-1, map_info=replay.map_info).filename
        if not os.path.exists(smap.full_base_image_filepath):
            raise FileNotFoundError
    except (ObjectDoesNotExist, FileNotFoundError) as exc:
        # 1st time upload for this map: fetch info and full map, create thumb
        # for index page
        smap.fetch_info()
        if smap.map_info:
            startpos = str()
            for coord in smap.map_info[0]["metadata"]["StartPos"]:
                startpos += "%f,%f|" % (coord["x"], coord["z"])
            startpos = startpos[:-1]
            height = smap.map_info[0]["metadata"]["Height"]
            width = smap.map_info[0]["metadata"]["Width"]
            full_img = smap.fetch_img()
        else:
            # no result - either springfiles.springrts.com is down or it didn't find the map
            err = (
                "No map info from springfiles.springrts.com, using empty img for map '%s'."
                % demofile.game_setup["host"]["mapname"]
            )
            logger.error(err)
            startpos = ""
            height = 170
            width = 340
            full_img = path_join(settings.MAPS_PATH, "{}.jpg".format(smap.map_name))
            shutil.copy(path_join(settings.MAPS_PATH, "__no_map_img.jpg"), full_img)

        smap.make_home_thumb()
        if smap.map_info:
            metadata = smap.map_info[0]
        else:
            metadata = dict()
        if isinstance(exc, ObjectDoesNotExist):
            replay.map_info = Map.objects.create(
                name=demofile.game_setup["host"]["mapname"],
                startpos=startpos,
                height=height,
                width=width,
                metadata2=metadata,
            )
            MapImg.objects.create(filename=full_img, startpostype=-1, map_info=replay.map_info)
            logger.debug(
                "replay(%d) created new map_info and MapImg: map_info.pk=%d",
                replay.pk,
                replay.map_info.pk,
            )
            replay.save()
    #
    # startpostype = 0: Fixed            |
    # startpostype = 1: Random           |
    # startpostype = 2: ChooseInGame     | allyteam { .. startrectbottom, ... }
    # startpostype = 3: ChooseBeforeGame | team { startposx, startposz } ...
    #
    # relayhoststartpostype = 0 --> startpostype = 3
    # relayhoststartpostype = 1 --> startpostype = 3
    # relayhoststartpostype = 2 --> startpostype = 2
    # relayhoststartpostype = 3 --> startpostype = 3
    #

    # always make new img, as players will never choose exact same startpos
    try:
        mapfile = smap.create_map_with_boxes(replay)
    except:
        logger.error("FIXME: to broad exception handling.")
        logger.exception(
            "error creating map img for Replay(%s) on map '%s'",
            replay.pk,
            replay.map_info.name,
        )
        mapfile = "error creating map img"

    replay.map_img = MapImg.objects.create(filename=mapfile, startpostype=2, map_info=replay.map_info)
    replay.save()

    logger.debug("replay(%d) created new map_img(%d)", replay.pk, replay.map_img.pk)
    timer.stop("  map creation")

    timer.start("  tags + descriptions")
    # auto add tag 1v1 2v2 etc.
    autotag = set_autotag(replay)

    # save descriptions
    save_desc(replay, short, long_text, autotag)
    timer.stop("  tags + descriptions")

    timer.start("  demofile.additional")
    # store if demofile doesn't contain the GAMEOVER msg, because of the spring94.1 bug:
    # http://springrts.com/mantis/view.php?id=3950
    # http://springrts.com/mantis/view.php?id=3804
    if not demofile.additional["gameover_frame"]:
        logger.debug("replay(%d) has no GAMEOVER msg", replay.pk)

        AdditionalReplayInfo.objects.get_or_create(replay=replay, key="gameover", value="")