# vim: set fileencoding=utf-8
from typing import Dict, List, Optional
from typing_extensions import Final

from bemani.backend.ddr.base import DDRBase
from bemani.backend.ddr.ddr2013 import DDR2013
from bemani.backend.ddr.common import (
    DDRGameAreaHiscoreHandler,
    DDRGameFriendHandler,
    DDRGameHiscoreHandler,
    DDRGameLoadDailyHandler,
    DDRGameLoadHandler,
    DDRGameLockHandler,
    DDRGameLogHandler,
    DDRGameMessageHandler,
    DDRGameNewHandler,
    DDRGameOldHandler,
    DDRGameRankingHandler,
    DDRGameRecorderHandler,
    DDRGameSaveHandler,
    DDRGameScoreHandler,
    DDRGameShopHandler,
    DDRGameTaxInfoHandler,
)
from bemani.common import VersionConstants, Profile, Time, intish
from bemani.data import Score, UserID
from bemani.protocol import Node


class DDR2014(
    DDRGameAreaHiscoreHandler,
    DDRGameFriendHandler,
    DDRGameHiscoreHandler,
    DDRGameLoadDailyHandler,
    DDRGameLoadHandler,
    DDRGameLockHandler,
    DDRGameLogHandler,
    DDRGameMessageHandler,
    DDRGameNewHandler,
    DDRGameOldHandler,
    DDRGameRankingHandler,
    DDRGameRecorderHandler,
    DDRGameSaveHandler,
    DDRGameScoreHandler,
    DDRGameShopHandler,
    DDRGameTaxInfoHandler,
    DDRBase,
):

    name: str = 'DanceDanceRevolution 2014'
    version: int = VersionConstants.DDR_2014

    GAME_STYLE_SINGLE: Final[int] = 0
    GAME_STYLE_DOUBLE: Final[int] = 1
    GAME_STYLE_VERSUS: Final[int] = 2

    GAME_RANK_AAA: Final[int] = 1
    GAME_RANK_AA: Final[int] = 2
    GAME_RANK_A: Final[int] = 3
    GAME_RANK_B: Final[int] = 4
    GAME_RANK_C: Final[int] = 5
    GAME_RANK_D: Final[int] = 6
    GAME_RANK_E: Final[int] = 7

    GAME_CHART_SINGLE_BEGINNER: Final[int] = 0
    GAME_CHART_SINGLE_BASIC: Final[int] = 1
    GAME_CHART_SINGLE_DIFFICULT: Final[int] = 2
    GAME_CHART_SINGLE_EXPERT: Final[int] = 3
    GAME_CHART_SINGLE_CHALLENGE: Final[int] = 4
    GAME_CHART_DOUBLE_BASIC: Final[int] = 5
    GAME_CHART_DOUBLE_DIFFICULT: Final[int] = 6
    GAME_CHART_DOUBLE_EXPERT: Final[int] = 7
    GAME_CHART_DOUBLE_CHALLENGE: Final[int] = 8

    GAME_HALO_NONE: Final[int] = 0
    GAME_HALO_GREAT_COMBO: Final[int] = 1
    GAME_HALO_PERFECT_COMBO: Final[int] = 2
    GAME_HALO_MARVELOUS_COMBO: Final[int] = 3
    GAME_HALO_GOOD_COMBO: Final[int] = 4

    GAME_MAX_SONGS: Final[int] = 800

    def previous_version(self) -> Optional[DDRBase]:
        return DDR2013(self.data, self.config, self.model)

    def game_to_db_rank(self, game_rank: int) -> int:
        return {
            self.GAME_RANK_AAA: self.RANK_AAA,
            self.GAME_RANK_AA: self.RANK_AA,
            self.GAME_RANK_A: self.RANK_A,
            self.GAME_RANK_B: self.RANK_B,
            self.GAME_RANK_C: self.RANK_C,
            self.GAME_RANK_D: self.RANK_D,
            self.GAME_RANK_E: self.RANK_E,
        }[game_rank]

    def db_to_game_rank(self, db_rank: int) -> int:
        return {
            self.RANK_AAA: self.GAME_RANK_AAA,
            self.RANK_AA_PLUS: self.GAME_RANK_AA,
            self.RANK_AA: self.GAME_RANK_AA,
            self.RANK_AA_MINUS: self.GAME_RANK_A,
            self.RANK_A_PLUS: self.GAME_RANK_A,
            self.RANK_A: self.GAME_RANK_A,
            self.RANK_A_MINUS: self.GAME_RANK_B,
            self.RANK_B_PLUS: self.GAME_RANK_B,
            self.RANK_B: self.GAME_RANK_B,
            self.RANK_B_MINUS: self.GAME_RANK_C,
            self.RANK_C_PLUS: self.GAME_RANK_C,
            self.RANK_C: self.GAME_RANK_C,
            self.RANK_C_MINUS: self.GAME_RANK_D,
            self.RANK_D_PLUS: self.GAME_RANK_D,
            self.RANK_D: self.GAME_RANK_D,
            self.RANK_E: self.GAME_RANK_E,
        }[db_rank]

    def game_to_db_chart(self, game_chart: int) -> int:
        return {
            self.GAME_CHART_SINGLE_BEGINNER: self.CHART_SINGLE_BEGINNER,
            self.GAME_CHART_SINGLE_BASIC: self.CHART_SINGLE_BASIC,
            self.GAME_CHART_SINGLE_DIFFICULT: self.CHART_SINGLE_DIFFICULT,
            self.GAME_CHART_SINGLE_EXPERT: self.CHART_SINGLE_EXPERT,
            self.GAME_CHART_SINGLE_CHALLENGE: self.CHART_SINGLE_CHALLENGE,
            self.GAME_CHART_DOUBLE_BASIC: self.CHART_DOUBLE_BASIC,
            self.GAME_CHART_DOUBLE_DIFFICULT: self.CHART_DOUBLE_DIFFICULT,
            self.GAME_CHART_DOUBLE_EXPERT: self.CHART_DOUBLE_EXPERT,
            self.GAME_CHART_DOUBLE_CHALLENGE: self.CHART_DOUBLE_CHALLENGE,
        }[game_chart]

    def db_to_game_chart(self, db_chart: int) -> int:
        return {
            self.CHART_SINGLE_BEGINNER: self.GAME_CHART_SINGLE_BEGINNER,
            self.CHART_SINGLE_BASIC: self.GAME_CHART_SINGLE_BASIC,
            self.CHART_SINGLE_DIFFICULT: self.GAME_CHART_SINGLE_DIFFICULT,
            self.CHART_SINGLE_EXPERT: self.GAME_CHART_SINGLE_EXPERT,
            self.CHART_SINGLE_CHALLENGE: self.GAME_CHART_SINGLE_CHALLENGE,
            self.CHART_DOUBLE_BASIC: self.GAME_CHART_DOUBLE_BASIC,
            self.CHART_DOUBLE_DIFFICULT: self.GAME_CHART_DOUBLE_DIFFICULT,
            self.CHART_DOUBLE_EXPERT: self.GAME_CHART_DOUBLE_EXPERT,
            self.CHART_DOUBLE_CHALLENGE: self.GAME_CHART_DOUBLE_CHALLENGE,
        }[db_chart]

    def db_to_game_halo(self, db_halo: int) -> int:
        if db_halo == self.HALO_MARVELOUS_FULL_COMBO:
            combo_type = self.GAME_HALO_MARVELOUS_COMBO
        elif db_halo == self.HALO_PERFECT_FULL_COMBO:
            combo_type = self.GAME_HALO_PERFECT_COMBO
        elif db_halo == self.HALO_GREAT_FULL_COMBO:
            combo_type = self.GAME_HALO_GREAT_COMBO
        elif db_halo == self.HALO_GOOD_FULL_COMBO:
            combo_type = self.GAME_HALO_GOOD_COMBO
        else:
            combo_type = self.GAME_HALO_NONE
        return combo_type

    def handle_game_common_request(self, request: Node) -> Node:
        game = Node.void('game')
        for flagid in range(512):
            flag = Node.void('flag')
            game.add_child(flag)

            flag.set_attribute('id', str(flagid))
            flag.set_attribute('t', '0')
            flag.set_attribute('s1', '0')
            flag.set_attribute('s2', '0')
            flag.set_attribute('area', str(self.get_machine_region()))
            flag.set_attribute('is_final', '0')

        # Last month's hit chart
        hit_chart = self.data.local.music.get_hit_chart(self.game, self.music_version, self.GAME_MAX_SONGS, 30)
        counts_by_reflink = [0] * self.GAME_MAX_SONGS
        for (reflink, plays) in hit_chart:
            if reflink >= 0 and reflink < self.GAME_MAX_SONGS:
                counts_by_reflink[reflink] = plays
        game.add_child(Node.u32_array('cnt_music_monthly', counts_by_reflink))

        # Last week's hit chart
        hit_chart = self.data.local.music.get_hit_chart(self.game, self.music_version, self.GAME_MAX_SONGS, 7)
        counts_by_reflink = [0] * self.GAME_MAX_SONGS
        for (reflink, plays) in hit_chart:
            if reflink >= 0 and reflink < self.GAME_MAX_SONGS:
                counts_by_reflink[reflink] = plays
        game.add_child(Node.u32_array('cnt_music_weekly', counts_by_reflink))

        # Last day's hit chart
        hit_chart = self.data.local.music.get_hit_chart(self.game, self.music_version, self.GAME_MAX_SONGS, 1)
        counts_by_reflink = [0] * self.GAME_MAX_SONGS
        for (reflink, plays) in hit_chart:
            if reflink >= 0 and reflink < self.GAME_MAX_SONGS:
                counts_by_reflink[reflink] = plays
        game.add_child(Node.u32_array('cnt_music_daily', counts_by_reflink))

        return game

    def handle_game_load_m_request(self, request: Node) -> Node:
        extid = intish(request.attribute('code'))
        refid = request.attribute('refid')

        if extid is not None:
            # Rival score loading
            userid = self.data.remote.user.from_extid(self.game, self.version, extid)
        else:
            # Self score loading
            userid = self.data.remote.user.from_refid(self.game, self.version, refid)

        if userid is not None:
            scores = self.data.remote.music.get_scores(self.game, self.music_version, userid)
        else:
            scores = []

        sortedscores: Dict[int, Dict[int, Score]] = {}
        for score in scores:
            if score.id not in sortedscores:
                sortedscores[score.id] = {}
            sortedscores[score.id][score.chart] = score

        game = Node.void('game')
        for song in sortedscores:
            music = Node.void('music')
            game.add_child(music)
            music.set_attribute('reclink', str(song))

            for chart in sortedscores[song]:
                score = sortedscores[song][chart]
                try:
                    gamechart = self.db_to_game_chart(chart)
                except KeyError:
                    # Don't support this chart in this game
                    continue
                gamerank = self.db_to_game_rank(score.data.get_int('rank'))
                combo_type = self.db_to_game_halo(score.data.get_int('halo'))

                typenode = Node.void('type')
                music.add_child(typenode)
                typenode.set_attribute('diff', str(gamechart))

                typenode.add_child(Node.u32('score', score.points))
                typenode.add_child(Node.u16('count', score.plays))
                typenode.add_child(Node.u8('rank', gamerank))
                typenode.add_child(Node.u8('combo_type', combo_type))
                # The game optionally receives hard, life8, life4, risky, assist_clear, normal_clear
                # u8 values too, and saves music scores with these set, but the UI doesn't appear to
                # do anything with them, so we don't care.

        return game

    def handle_game_save_m_request(self, request: Node) -> Node:
        refid = request.attribute('refid')
        songid = int(request.attribute('mid'))
        chart = self.game_to_db_chart(int(request.attribute('mtype')))

        # Calculate statistics
        data = request.child('data')
        points = int(data.attribute('score'))
        combo = int(data.attribute('combo'))
        rank = self.game_to_db_rank(int(data.attribute('rank')))
        if points == 1000000:
            halo = self.HALO_MARVELOUS_FULL_COMBO
        elif int(data.attribute('perf_fc')) != 0:
            halo = self.HALO_PERFECT_FULL_COMBO
        elif int(data.attribute('great_fc')) != 0:
            halo = self.HALO_GREAT_FULL_COMBO
        elif int(data.attribute('good_fc')) != 0:
            halo = self.HALO_GOOD_FULL_COMBO
        else:
            halo = self.HALO_NONE
        trace = request.child_value('trace')

        # Save the score, regardless of whether we have a refid. If we save
        # an anonymous score, it only goes into the DB to count against the
        # number of plays for that song/chart.
        userid = self.data.remote.user.from_refid(self.game, self.version, refid)
        self.update_score(
            userid,
            songid,
            chart,
            points,
            rank,
            halo,
            combo,
            trace,
        )

        # No response needed
        game = Node.void('game')
        return game

    def handle_game_load_edit_request(self, request: Node) -> Node:
        return Node.void('game')

    def handle_game_save_resultshot_request(self, request: Node) -> Node:
        return Node.void('game')

    def handle_game_trace_request(self, request: Node) -> Node:
        # This is almost identical to 2013 and below, except it will never
        # even try to request course traces, so we fork from common functionality.
        extid = int(request.attribute('code'))
        chart = int(request.attribute('type'))
        mid = intish(request.attribute('mid'))

        # Base packet is just game, if we find something we add to it
        game = Node.void('game')

        # Rival trace loading
        userid = self.data.remote.user.from_extid(self.game, self.version, extid)
        if userid is None:
            # Nothing to load
            return game

        # Load trace from song score
        songscore = self.data.remote.music.get_score(
            self.game,
            self.music_version,
            userid,
            mid,
            self.game_to_db_chart(chart),
        )
        if songscore is not None and 'trace' in songscore.data:
            game.add_child(Node.u32('size', len(songscore.data['trace'])))
            game.add_child(Node.u8_array('trace', songscore.data['trace']))

        return game

    def format_profile(self, userid: UserID, profile: Profile) -> Node:
        root = Node.void('game')

        # Look up play stats we bridge to every mix
        play_stats = self.get_play_statistics(userid)

        # Basic game settings
        root.add_child(Node.string('seq', ''))
        root.add_child(Node.u32('code', profile.extid))
        root.add_child(Node.string('name', profile.get_str('name')))
        root.add_child(Node.u8('area', profile.get_int('area', self.get_machine_region())))
        root.add_child(Node.u32('cnt_s', play_stats.get_int('single_plays')))
        root.add_child(Node.u32('cnt_d', play_stats.get_int('double_plays')))
        root.add_child(Node.u32('cnt_b', play_stats.get_int('battle_plays')))  # This could be wrong, its a guess
        root.add_child(Node.u32('cnt_m0', play_stats.get_int('cnt_m0')))
        root.add_child(Node.u32('cnt_m1', play_stats.get_int('cnt_m1')))
        root.add_child(Node.u32('cnt_m2', play_stats.get_int('cnt_m2')))
        root.add_child(Node.u32('cnt_m3', play_stats.get_int('cnt_m3')))
        root.add_child(Node.u32('cnt_m4', play_stats.get_int('cnt_m4')))
        root.add_child(Node.u32('cnt_m5', play_stats.get_int('cnt_m5')))
        root.add_child(Node.u32('exp', play_stats.get_int('exp')))
        root.add_child(Node.u32('exp_o', profile.get_int('exp_o')))
        root.add_child(Node.u32('star', profile.get_int('star')))
        root.add_child(Node.u32('star_c', profile.get_int('star_c')))
        root.add_child(Node.u8('combo', profile.get_int('combo', 0)))
        root.add_child(Node.u8('timing_diff', profile.get_int('early_late', 0)))

        # Character stuff
        chara = Node.void('chara')
        root.add_child(chara)
        chara.set_attribute('my', str(profile.get_int('chara', 30)))
        root.add_child(Node.u16_array('chara_opt', profile.get_int_array('chara_opt', 96, [208] * 96)))

        # Drill rankings
        if 'title_gr' in profile:
            title_gr = Node.void('title_gr')
            root.add_child(title_gr)
            title_grdict = profile.get_dict('title_gr')
            if 't' in title_grdict:
                title_gr.set_attribute('t', str(title_grdict.get_int('t')))
            if 's' in title_grdict:
                title_gr.set_attribute('s', str(title_grdict.get_int('s')))
            if 'd' in title_grdict:
                title_gr.set_attribute('d', str(title_grdict.get_int('d')))

        # Calorie mode
        if 'weight' in profile:
            workouts = self.data.local.user.get_time_based_achievements(
                self.game,
                self.version,
                userid,
                achievementtype='workout',
                since=Time.now() - Time.SECONDS_IN_DAY,
            )
            total = sum([w.data.get_int('calories') for w in workouts])
            workout = Node.void('workout')
            root.add_child(workout)
            workout.set_attribute('weight', str(profile.get_int('weight')))
            workout.set_attribute('day', str(total))
            workout.set_attribute('disp', '1')

            # Unsure if this should be last day, or total calories ever
            totalcalorie = Node.void('totalcalorie')
            root.add_child(totalcalorie)
            totalcalorie.set_attribute('total', str(total))

        # Daily play counts
        daycount = Node.void('daycount')
        root.add_child(daycount)
        daycount.set_attribute('playcount', str(play_stats.today_plays))

        # Daily combo stuff, unknown how this works
        dailycombo = Node.void('dailycombo')
        root.add_child(dailycombo)
        dailycombo.set_attribute('daily_combo', str(0))
        dailycombo.set_attribute('daily_combo_lv', str(0))

        # Last cursor settings
        last = Node.void('last')
        root.add_child(last)
        lastdict = profile.get_dict('last')
        last.set_attribute('rival1', str(lastdict.get_int('rival1', -1)))
        last.set_attribute('rival2', str(lastdict.get_int('rival2', -1)))
        last.set_attribute('rival3', str(lastdict.get_int('rival3', -1)))
        last.set_attribute('fri', str(lastdict.get_int('rival1', -1)))  # This literally goes to the same memory in 2014
        last.set_attribute('style', str(lastdict.get_int('style')))
        last.set_attribute('mode', str(lastdict.get_int('mode')))
        last.set_attribute('cate', str(lastdict.get_int('cate')))
        last.set_attribute('sort', str(lastdict.get_int('sort')))
        last.set_attribute('mid', str(lastdict.get_int('mid')))
        last.set_attribute('mtype', str(lastdict.get_int('mtype')))
        last.set_attribute('cid', str(lastdict.get_int('cid')))
        last.set_attribute('ctype', str(lastdict.get_int('ctype')))
        last.set_attribute('sid', str(lastdict.get_int('sid')))

        # Result stars
        result_star = Node.void('result_star')
        root.add_child(result_star)
        result_stars = profile.get_int_array('result_stars', 9)
        for i in range(9):
            result_star.set_attribute(f'slot{i + 1}', str(result_stars[i]))

        # Groove gauge level-ups
        gr_s = Node.void('gr_s')
        root.add_child(gr_s)
        index = 1
        for entry in profile.get_int_array('gr_s', 5):
            gr_s.set_attribute(f'gr{index}', str(entry))
            index = index + 1

        gr_d = Node.void('gr_d')
        root.add_child(gr_d)
        index = 1
        for entry in profile.get_int_array('gr_d', 5):
            gr_d.set_attribute(f'gr{index}', str(entry))
            index = index + 1

        # Options in menus
        root.add_child(Node.s16_array('opt', profile.get_int_array('opt', 16)))
        root.add_child(Node.s16_array('opt_ex', profile.get_int_array('opt_ex', 16)))
        option_ver = Node.void('option_ver')
        root.add_child(option_ver)
        option_ver.set_attribute('ver', str(profile.get_int('option_ver', 2)))
        if 'option_02' in profile:
            root.add_child(Node.s16_array('option_02', profile.get_int_array('option_02', 24)))

        # Unlock flags
        root.add_child(Node.u8_array('flag', profile.get_int_array('flag', 512, [1] * 512)[:256]))
        root.add_child(Node.u8_array('flag_ex', profile.get_int_array('flag', 512, [1] * 512)))

        # Ranking display?
        root.add_child(Node.u16_array('rank', profile.get_int_array('rank', 100)))

        # Rivals
        links = self.data.local.user.get_links(self.game, self.version, userid)
        for link in links:
            if link.type[:7] != 'friend_':
                continue

            pos = int(link.type[7:])
            friend = self.get_profile(link.other_userid)
            play_stats = self.get_play_statistics(link.other_userid)
            if friend is not None:
                friendnode = Node.void('friend')
                root.add_child(friendnode)
                friendnode.set_attribute('pos', str(pos))
                friendnode.set_attribute('vs', '0')
                friendnode.set_attribute('up', '0')
                friendnode.add_child(Node.u32('code', friend.extid))
                friendnode.add_child(Node.string('name', friend.get_str('name')))
                friendnode.add_child(Node.u8('area', friend.get_int('area', self.get_machine_region())))
                friendnode.add_child(Node.u32('exp', play_stats.get_int('exp')))
                friendnode.add_child(Node.u32('star', friend.get_int('star')))

                # Drill rankings
                if 'title' in friend:
                    title = Node.void('title')
                    friendnode.add_child(title)
                    titledict = friend.get_dict('title')
                    if 't' in titledict:
                        title.set_attribute('t', str(titledict.get_int('t')))
                    if 's' in titledict:
                        title.set_attribute('s', str(titledict.get_int('s')))
                    if 'd' in titledict:
                        title.set_attribute('d', str(titledict.get_int('d')))

                if 'title_gr' in friend:
                    title_gr = Node.void('title_gr')
                    friendnode.add_child(title_gr)
                    title_grdict = friend.get_dict('title_gr')
                    if 't' in title_grdict:
                        title_gr.set_attribute('t', str(title_grdict.get_int('t')))
                    if 's' in title_grdict:
                        title_gr.set_attribute('s', str(title_grdict.get_int('s')))
                    if 'd' in title_grdict:
                        title_gr.set_attribute('d', str(title_grdict.get_int('d')))

                # Groove gauge level-ups
                gr_s = Node.void('gr_s')
                friendnode.add_child(gr_s)
                index = 1
                for entry in friend.get_int_array('gr_s', 5):
                    gr_s.set_attribute(f'gr{index}', str(entry))
                    index = index + 1

                gr_d = Node.void('gr_d')
                friendnode.add_child(gr_d)
                index = 1
                for entry in friend.get_int_array('gr_d', 5):
                    gr_d.set_attribute(f'gr{index}', str(entry))
                    index = index + 1

        # Target stuff
        target = Node.void('target')
        root.add_child(target)
        target.set_attribute('flag', str(profile.get_int('target_flag')))
        target.set_attribute('setnum', str(profile.get_int('target_setnum')))

        # Play area
        areas = profile.get_int_array('play_area', 55)
        play_area = Node.void('play_area')
        root.add_child(play_area)
        for i in range(len(areas)):
            play_area.set_attribute(f'play_cnt{i}', str(areas[i]))

        return root

    def unformat_profile(self, userid: UserID, request: Node, oldprofile: Profile) -> Profile:
        newprofile = oldprofile.clone()
        play_stats = self.get_play_statistics(userid)

        # Grab last node and accessories so we can make decisions based on type
        last = request.child('last')
        lastdict = newprofile.get_dict('last')
        mode = int(last.attribute('mode'))
        style = int(last.attribute('style'))
        is_dp = style == self.GAME_STYLE_DOUBLE

        # Drill rankings
        title = request.child('title')
        title_gr = request.child('title_gr')
        titledict = newprofile.get_dict('title')
        title_grdict = newprofile.get_dict('title_gr')

        # Groove radar level ups
        gr = request.child('gr')

        # Set the correct values depending on if we're single or double play
        if is_dp:
            play_stats.increment_int('double_plays')
            if gr is not None:
                newprofile.replace_int_array(
                    'gr_d',
                    5,
                    [
                        intish(gr.attribute('gr1')),
                        intish(gr.attribute('gr2')),
                        intish(gr.attribute('gr3')),
                        intish(gr.attribute('gr4')),
                        intish(gr.attribute('gr5')),
                    ],
                )
            if title is not None:
                titledict.replace_int('d', title.value)
                newprofile.replace_dict('title', titledict)
            if title_gr is not None:
                title_grdict.replace_int('d', title.value)
                newprofile.replace_dict('title_gr', title_grdict)
        else:
            play_stats.increment_int('single_plays')
            if gr is not None:
                newprofile.replace_int_array(
                    'gr_s',
                    5,
                    [
                        intish(gr.attribute('gr1')),
                        intish(gr.attribute('gr2')),
                        intish(gr.attribute('gr3')),
                        intish(gr.attribute('gr4')),
                        intish(gr.attribute('gr5')),
                    ],
                )
            if title is not None:
                titledict.replace_int('s', title.value)
                newprofile.replace_dict('title', titledict)
            if title_gr is not None:
                title_grdict.replace_int('s', title.value)
                newprofile.replace_dict('title_gr', title_grdict)
        play_stats.increment_int(f'cnt_m{mode}')

        # Result stars
        result_star = request.child('result_star')
        if result_star is not None:
            newprofile.replace_int_array(
                'result_stars',
                9,
                [
                    intish(result_star.attribute('slot1')),
                    intish(result_star.attribute('slot2')),
                    intish(result_star.attribute('slot3')),
                    intish(result_star.attribute('slot4')),
                    intish(result_star.attribute('slot5')),
                    intish(result_star.attribute('slot6')),
                    intish(result_star.attribute('slot7')),
                    intish(result_star.attribute('slot8')),
                    intish(result_star.attribute('slot9')),
                ],
            )

        # Target stuff
        target = request.child('target')
        if target is not None:
            newprofile.replace_int('target_flag', intish(target.attribute('flag')))
            newprofile.replace_int('target_setnum', intish(target.attribute('setnum')))

        # Update last attributes
        lastdict.replace_int('rival1', intish(last.attribute('rival1')))
        lastdict.replace_int('rival2', intish(last.attribute('rival2')))
        lastdict.replace_int('rival3', intish(last.attribute('rival3')))
        lastdict.replace_int('style', intish(last.attribute('style')))
        lastdict.replace_int('mode', intish(last.attribute('mode')))
        lastdict.replace_int('cate', intish(last.attribute('cate')))
        lastdict.replace_int('sort', intish(last.attribute('sort')))
        lastdict.replace_int('mid', intish(last.attribute('mid')))
        lastdict.replace_int('mtype', intish(last.attribute('mtype')))
        lastdict.replace_int('cid', intish(last.attribute('cid')))
        lastdict.replace_int('ctype', intish(last.attribute('ctype')))
        lastdict.replace_int('sid', intish(last.attribute('sid')))
        newprofile.replace_dict('last', lastdict)

        # Grab character options
        chara = request.child('chara')
        if chara is not None:
            newprofile.replace_int('chara', intish(chara.attribute('my')))
        chara_opt = request.child('chara_opt')
        if chara_opt is not None:
            # A bug in old versions of AVS returns the wrong number for set
            newprofile.replace_int_array('chara_opt', 96, chara_opt.value[:96])

        # Options
        option_02 = request.child('option_02')
        if option_02 is not None:
            # A bug in old versions of AVS returns the wrong number for set
            newprofile.replace_int_array('option_02', 24, option_02.value[:24])

        # Experience and stars
        exp = request.child_value('exp')
        if exp is not None:
            play_stats.replace_int('exp', play_stats.get_int('exp') + exp)
        star = request.child_value('star')
        if star is not None:
            newprofile.replace_int('star', newprofile.get_int('star') + star)
        star_c = request.child_value('star_c')
        if star_c is not None:
            newprofile.replace_int('star_c', newprofile.get_int('star_c') + exp)

        # Update game flags
        for child in request.children:
            if child.name not in ['flag', 'flag_ex']:
                continue
            try:
                value = int(child.attribute('data'))
                offset = int(child.attribute('no'))
            except ValueError:
                continue

            flags = newprofile.get_int_array('flag', 512, [1] * 512)
            if offset < 0 or offset >= len(flags):
                continue
            flags[offset] = value
            newprofile.replace_int_array('flag', 512, flags)

        # Workout mode support
        newweight = -1
        oldweight = newprofile.get_int('weight')
        for child in request.children:
            if child.name != 'weight':
                continue
            newweight = child.value
        if newweight < 0:
            newweight = oldweight

        # Either update or unset the weight depending on the game
        if newweight == 0:
            # Weight is unset or we declined to use this feature, remove from profile
            if 'weight' in newprofile:
                del newprofile['weight']
        else:
            # Weight has been set or previously retrieved, we should save calories
            newprofile.replace_int('weight', newweight)
            total = 0
            for child in request.children:
                if child.name != 'calory':
                    continue
                total += child.value
            self.data.local.user.put_time_based_achievement(
                self.game,
                self.version,
                userid,
                0,
                'workout',
                {
                    'calories': total,
                    'weight': newweight,
                },
            )

        # Look up old friends
        oldfriends: List[Optional[UserID]] = [None] * 10
        links = self.data.local.user.get_links(self.game, self.version, userid)
        for link in links:
            if link.type[:7] != 'friend_':
                continue

            pos = int(link.type[7:])
            oldfriends[pos] = link.other_userid

        # Save any rivals that were added/removed/changed
        newfriends = oldfriends[:]
        for child in request.children:
            if child.name != 'friend':
                continue

            code = int(child.attribute('code'))
            pos = int(child.attribute('pos'))

            if pos >= 0 and pos < 10:
                if code == 0:
                    # We cleared this friend
                    newfriends[pos] = None
                else:
                    # Try looking up the userid
                    newfriends[pos] = self.data.remote.user.from_extid(self.game, self.version, code)

        # Diff the set of links to determine updates
        for i in range(10):
            if newfriends[i] == oldfriends[i]:
                continue

            if newfriends[i] is None:
                # Kill the rival in this location
                self.data.local.user.destroy_link(
                    self.game,
                    self.version,
                    userid,
                    f'friend_{i}',
                    oldfriends[i],
                )
            elif oldfriends[i] is None:
                # Add rival in this location
                self.data.local.user.put_link(
                    self.game,
                    self.version,
                    userid,
                    f'friend_{i}',
                    newfriends[i],
                    {},
                )
            else:
                # Changed the rival here, kill the old one, add the new one
                self.data.local.user.destroy_link(
                    self.game,
                    self.version,
                    userid,
                    f'friend_{i}',
                    oldfriends[i],
                )
                self.data.local.user.put_link(
                    self.game,
                    self.version,
                    userid,
                    f'friend_{i}',
                    newfriends[i],
                    {},
                )

        # Play area counter
        shop_area = int(request.attribute('shop_area'))
        if shop_area >= 0 and shop_area < 55:
            areas = newprofile.get_int_array('play_area', 55)
            areas[shop_area] = areas[shop_area] + 1
            newprofile.replace_int_array('play_area', 55, areas)

        # Keep track of play statistics
        self.update_play_statistics(userid, play_stats)

        return newprofile