<?php

if (!defined('AOWOW_REVISION'))
    die('illegal access');

class AjaxProfile extends AjaxHandler
{
    private   $undo        = false;

    protected $validParams = ['link', 'unlink', 'pin', 'unpin', 'public', 'private', 'avatar', 'resync', 'status', 'save', 'delete', 'purge', 'summary', 'load'];
    protected $_get        = array(
        'id'         => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkIdList'  ],
        'items'      => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxProfile::checkItemList'],
        'size'       => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkTextLine'],
        'guild'      => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkEmptySet'],
        'arena-team' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkEmptySet'],
        'user'       => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxProfile::checkUser'    ]
    );

    protected $_post        = array(
        'name'         => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkTextLine'],
        'level'        => ['filter' => FILTER_SANITIZE_NUMBER_INT],
        'class'        => ['filter' => FILTER_SANITIZE_NUMBER_INT],
        'race'         => ['filter' => FILTER_SANITIZE_NUMBER_INT],
        'gender'       => ['filter' => FILTER_SANITIZE_NUMBER_INT],
        'nomodel'      => ['filter' => FILTER_SANITIZE_NUMBER_INT],
        'talenttree1'  => ['filter' => FILTER_SANITIZE_NUMBER_INT],
        'talenttree2'  => ['filter' => FILTER_SANITIZE_NUMBER_INT],
        'talenttree3'  => ['filter' => FILTER_SANITIZE_NUMBER_INT],
        'activespec'   => ['filter' => FILTER_SANITIZE_NUMBER_INT],
        'talentbuild1' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxProfile::checkTalentString'],
        'glyphs1'      => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxProfile::checkGlyphString' ],
        'talentbuild2' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxProfile::checkTalentString'],
        'glyphs2'      => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxProfile::checkGlyphString' ],
        'icon'         => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkTextLine'    ],
        'description'  => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkTextBlob'    ],
        'source'       => ['filter' => FILTER_SANITIZE_NUMBER_INT],
        'copy'         => ['filter' => FILTER_SANITIZE_NUMBER_INT],
        'public'       => ['filter' => FILTER_SANITIZE_NUMBER_INT],
        'gearscore'    => ['filter' => FILTER_SANITIZE_NUMBER_INT],
        'inv'          => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkIdListUnsigned', 'flags' => FILTER_REQUIRE_ARRAY],
    );

    public function __construct(array $params)
    {
        parent::__construct($params);

        if (!$this->params)
            return;

        if (!Cfg::get('PROFILER_ENABLE'))
            return;

        switch ($this->params[0])
        {
            case 'unlink':
                $this->undo = true;
            case 'link':
                $this->handler = 'handleLink';              // always returns null
                break;
            case 'unpin':
                $this->undo = true;
            case 'pin':
                $this->handler = 'handlePin';               // always returns null
                break;
            case 'private':
                $this->undo = true;
            case 'public':
                $this->handler = 'handlePrivacy';           // always returns null
                break;
            case 'avatar':
                $this->handler = 'handleAvatar';            // sets an image header
                break;                                      // so it has to die here or another header will be set
            case 'resync':
                $this->handler = 'handleResync';            // always returns "1"
                break;
            case 'status':
                $this->handler = 'handleStatus';            // returns status object
                break;
            case 'save':
                $this->handler = 'handleSave';
                break;
            case 'delete':
                $this->handler = 'handleDelete';
                break;
            case 'purge':
                $this->handler = 'handlePurge';
                break;
            case 'summary':                                 // page is generated by jScript
                die();                                      // just be empty
            case 'load':
                $this->handler = 'handleLoad';
                break;
        }
    }

    /*  params
            id: <prId1,prId2,..,prIdN>
            user: <string> [optional]
        return: null
    */
    protected function handleLink() : void                  // links char with account
    {
        if (!User::$id || empty($this->_get['id']))
        {
            trigger_error('AjaxProfile::handleLink - profileId empty or user not logged in', E_USER_ERROR);
            return;
        }

        $uid = User::$id;
        if ($this->_get['user'] && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU))
        {
            if (!($uid = DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE user = ?', $this->_get['user'])))
            {
                trigger_error('AjaxProfile::handleLink - user "'.$this->_get['user'].'" does not exist', E_USER_ERROR);
                return;
            }
        }

        if ($this->undo)
            DB::Aowow()->query('DELETE FROM ?_account_profiles WHERE accountId = ?d AND profileId IN (?a)', $uid, $this->_get['id']);
        else
        {
            foreach ($this->_get['id'] as $prId)            // only link characters, not custom profiles
            {
                if ($prId = DB::Aowow()->selectCell('SELECT id FROM ?_profiler_profiles WHERE id = ?d AND realm IS NOT NULL', $prId))
                    DB::Aowow()->query('INSERT IGNORE INTO ?_account_profiles VALUES (?d, ?d, 0)', $uid, $prId);
                else
                {
                    trigger_error('AjaxProfile::handleLink - profile #'.$prId.' is custom or does not exist', E_USER_ERROR);
                    return;
                }
            }
        }
    }

    /*  params
            id: <prId1,prId2,..,prIdN>
            user: <string> [optional]
        return: null
    */
    protected function handlePin() : void                   // (un)favorite
    {
        if (!User::$id || empty($this->_get['id'][0]))
        {
            trigger_error('AjaxProfile::handlePin - profileId empty or user not logged in', E_USER_ERROR);
            return;
        }

        $uid = User::$id;
        if ($this->_get['user'] && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU))
        {
            if (!($uid = DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE user = ?', $this->_get['user'])))
            {
                trigger_error('AjaxProfile::handlePin - user "'.$this->_get['user'].'" does not exist', E_USER_ERROR);
                return;
            }
        }

        // since only one character can be pinned at a time we can reset everything
        DB::Aowow()->query('UPDATE ?_account_profiles  SET extraFlags = extraFlags & ?d WHERE accountId = ?d', ~PROFILER_CU_PINNED, $uid);
        // and set a single char if necessary
        if (!$this->undo)
            DB::Aowow()->query('UPDATE ?_account_profiles  SET extraFlags = extraFlags | ?d WHERE profileId = ?d AND accountId = ?d',  PROFILER_CU_PINNED, $this->_get['id'][0], $uid);
    }

    /*  params
            id: <prId1,prId2,..,prIdN>
            user: <string> [optional]
        return: null
    */
    protected function handlePrivacy() : void               // public visibility
    {
        if (!User::$id || empty($this->_get['id'][0]))
        {
            trigger_error('AjaxProfile::handlePrivacy - profileId empty or user not logged in', E_USER_ERROR);
            return;
        }

        $uid = User::$id;
        if ($this->_get['user'] && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU))
        {
            if (!($uid = DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE user = ?', $this->_get['user'])))
            {
                trigger_error('AjaxProfile::handlePrivacy - user "'.$this->_get['user'].'" does not exist', E_USER_ERROR);
                return;
            }
        }

        if ($this->undo)
        {
            DB::Aowow()->query('UPDATE ?_account_profiles  SET extraFlags = extraFlags & ?d WHERE profileId IN (?a) AND accountId = ?d', ~PROFILER_CU_PUBLISHED, $this->_get['id'], $uid);
            DB::Aowow()->query('UPDATE ?_profiler_profiles SET cuFlags    = cuFlags    & ?d WHERE id        IN (?a) AND user      = ?d', ~PROFILER_CU_PUBLISHED, $this->_get['id'], $uid);
        }
        else
        {
            DB::Aowow()->query('UPDATE ?_account_profiles  SET extraFlags = extraFlags | ?d WHERE profileId IN (?a) AND accountId = ?d',  PROFILER_CU_PUBLISHED, $this->_get['id'], $uid);
            DB::Aowow()->query('UPDATE ?_profiler_profiles SET cuFlags    = cuFlags    | ?d WHERE id        IN (?a) AND user      = ?d',  PROFILER_CU_PUBLISHED, $this->_get['id'], $uid);
        }
    }

    /*  params
            id: <prId>
            size: <string> [optional]
        return: image-header
    */
    protected function handleAvatar() : void                // image
    {
        // something happened in the last years: those textures do not include tiny icons
        $sizes = [/* 'tiny' => 15, */'small' => 18, 'medium' => 36, 'large' => 56];
        $aPath = 'uploads/avatars/%d.jpg';
        $s     = $this->_get['size'] ?: 'medium';

        if (!$this->_get['id'] || !preg_match('/^([0-9]+)\.(jpg|gif)$/', $this->_get['id'][0], $matches) || !in_array($s, array_keys($sizes)))
        {
            trigger_error('AjaxProfile::handleAvatar - malformed request received', E_USER_ERROR);
            return;
        }

        $this->contentType = $matches[2] == 'png' ? MIME_TYPE_PNG : MIME_TYPE_JPEG;

        $id   = $matches[1];
        $dest = imageCreateTruecolor($sizes[$s], $sizes[$s]);

        if (file_exists(sprintf($aPath, $id)))
        {
            $offsetX = $offsetY = 0;

            switch ($s)
            {
                case 'tiny':
                    $offsetX += $sizes['small'];
                case 'small':
                    $offsetY += $sizes['medium'];
                case 'medium':
                    $offsetX += $sizes['large'];
            }

            $src = imageCreateFromJpeg(printf($aPath, $id));
            imagecopymerge($dest, $src, 0, 0, $offsetX, $offsetY, $sizes[$s], $sizes[$s], 100);
        }
        else
            trigger_error('AjaxProfile::handleAvatar - avatar file #'.$id.' not found', E_USER_ERROR);

        if ($matches[2] == 'gif')
            imageGif($dest);
        else
            imageJpeg($dest);
    }

    /*  params
            id: <prId1,prId2,..,prIdN>
            user: <string> [optional, not used]
        return: 1
    */
    protected function handleResync() : string
    {
        if ($chars = DB::Aowow()->select('SELECT realm, realmGUID FROM ?_profiler_profiles WHERE id IN (?a)', $this->_get['id']))
        {
            foreach ($chars as $c)
                Profiler::scheduleResync(Type::PROFILE, $c['realm'], $c['realmGUID']);
        }
        else
            trigger_error('AjaxProfile::handleResync - profiles '.implode(', ', $this->_get['id']).' not found in db', E_USER_ERROR);

        return '1';
    }

    /*  params
            id: <prId1,prId2,..,prIdN>
        return
            <status object>
            [
                nQueueProcesses,
                [statusCode, timeToRefresh, curQueuePos, errorCode, nResyncTries],
                [<anotherStatus>]
                ...
            ]

            not all fields are required, if zero they are omitted
            statusCode:
                0: end the request
                1: waiting
                2: working...
                3: ready; click to view
                4: error / retry
            errorCode:
                0: unk error
                1: char does not exist
                2: armory gone
    */
    protected function handleStatus() : string
    {
        // roster resync for this guild was requested -> get char list
        if ($this->_get['guild'])
            $ids = DB::Aowow()->selectCol('SELECT id FROM ?_profiler_profiles WHERE guild IN (?a)', $this->_get['id']);
        else if ($this->_get['arena-team'])
            $ids = DB::Aowow()->selectCol('SELECT profileId FROM ?_profiler_arena_team_member WHERE arenaTeamId IN (?a)', $this->_get['id']);
        else
            $ids = $this->_get['id'];

        if (!$ids)
        {
            trigger_error('AjaxProfile::handleStatus - no profileIds to resync'.($this->_get['guild'] ? ' for guild #'.$this->_get['guild'] : ($this->_get['arena-team'] ? ' for areana team #'.$this->_get['arena-team'] : '')), E_USER_ERROR);
            return Util::toJSON([1, [PR_QUEUE_STATUS_ERROR, 0, 0, PR_QUEUE_ERROR_CHAR]]);
        }

        $response = Profiler::resyncStatus(Type::PROFILE, $ids);
        return Util::toJSON($response);
    }

    /*  params (get))
            id: <prId1,0> [0: new profile]
        params (post)
            <various char data> [see below]
        return:
            proileId [onSuccess]
            -1       [onError]
    */
    protected function handleSave() : string                // unKill a profile
    {
        // todo (med): detail check this post-data
        $cuProfile = array(
            'user'         => User::$id,
         // 'userName'     => User::$displayName,
            'name'         => $this->_post['name'],
            'level'        => $this->_post['level'],
            'class'        => $this->_post['class'],
            'race'         => $this->_post['race'],
            'gender'       => $this->_post['gender'],
            'nomodelMask'  => $this->_post['nomodel'],
            'talenttree1'  => $this->_post['talenttree1'],
            'talenttree2'  => $this->_post['talenttree2'],
            'talenttree3'  => $this->_post['talenttree3'],
            'talentbuild1' => $this->_post['talentbuild1'],
            'talentbuild2' => $this->_post['talentbuild2'],
            'activespec'   => $this->_post['activespec'],
            'glyphs1'      => $this->_post['glyphs1'],
            'glyphs2'      => $this->_post['glyphs2'],
            'gearscore'    => $this->_post['gearscore'],
            'icon'         => $this->_post['icon'],
            'cuFlags'      => PROFILER_CU_PROFILE | ($this->_post['public'] ? PROFILER_CU_PUBLISHED : 0)
        );

        if (strstr($cuProfile['icon'], 'profile=avatar'))   // how the profiler is supposed to handle icons is beyond me
            $cuProfile['icon'] = '';

        if ($_ = $this->_post['description'])
            $cuProfile['description'] = $_;

        if ($_ = $this->_post['source'])                    // should i also set sourcename?
            $cuProfile['sourceId'] = $_;

        if ($_ = $this->_post['copy'])                      // gets set to source profileId when "save as" is clicked. Whats the difference to 'source' though?
        {
            // get character origin info if possible
            if ($r = DB::Aowow()->selectCell('SELECT realm FROM ?_profiler_profiles WHERE id = ?d AND realm IS NOT NULL', $_))
                $cuProfile['realm'] = $r;

            $cuProfile['sourceId'] = $_;
        }

        if (!empty($cuProfile['sourceId']))
            $cuProfile['sourceName'] = DB::Aowow()->selectCell('SELECT name FROM ?_profiler_profiles WHERE id = ?d', $cuProfile['sourceId']);

        $charId = -1;
        if ($id = $this->_get['id'][0])                     // update
        {
            if ($charId = DB::Aowow()->selectCell('SELECT id FROM ?_profiler_profiles WHERE id = ?d', $id))
                DB::Aowow()->query('UPDATE ?_profiler_profiles SET ?a WHERE id = ?d', $cuProfile, $id);
        }
        else                                                // new
        {
            $nProfiles = DB::Aowow()->selectCell('SELECT COUNT(*) FROM ?_profiler_profiles WHERE user = ?d AND (cuFlags & ?d) = 0 AND realmGUID IS NULL', User::$id, PROFILER_CU_DELETED);
            if ($nProfiles < 10 || User::isPremium())
                if ($newId = DB::Aowow()->query('INSERT INTO ?_profiler_profiles (?#) VALUES (?a)', array_keys($cuProfile), array_values($cuProfile)))
                    $charId = $newId;
        }

        // update items
        if ($charId != -1)
        {
            // ok, 'funny' thing: wether an item has en extra prismatic sockel is determined contextual
            // either the socket is -1 or it has an itemId in a socket where there shouldn't be one
            $keys  = ['id', 'slot', 'item', 'subitem', 'permEnchant', 'tempEnchant', 'gem1', 'gem2', 'gem3', 'gem4'];

            // validate Enchantments
            $enchIds = array_merge(
                array_column($this->_post['inv'], 3),       // perm enchantments
                array_column($this->_post['inv'], 4)        // temp enchantments (not used..?)
            );
            $enchs = new EnchantmentList(array(['id', $enchIds]));

            // validate items
            $itemIds = array_merge(
                array_column($this->_post['inv'], 1),       // base item
                array_column($this->_post['inv'], 5),       // gem slot 1
                array_column($this->_post['inv'], 6),       // gem slot 2
                array_column($this->_post['inv'], 7),       // gem slot 3
                array_column($this->_post['inv'], 8)        // gem slot 4
            );

            $items = new ItemList(array(['id', $itemIds]));
            if (!$items->error)
            {
                foreach ($this->_post['inv'] as $slot => $itemData)
                {
                    if ($slot + 1 == array_sum($itemData))  // only slot definition set => empty slot
                    {
                        DB::Aowow()->query('DELETE FROM ?_profiler_items WHERE id = ?d AND slot = ?d', $charId, $itemData[0]);
                        continue;
                    }

                    // item does not exist
                    if (!$items->getEntry($itemData[1]))
                        continue;

                    // sub-item check
                    if (!$items->getRandEnchantForItem($itemData[1]))
                        $itemData[2] = 0;

                    // item sockets are fubar
                    $nSockets = $items->json[$itemData[1]]['nsockets'] ?? 0;
                    $nSockets += in_array($slot, [SLOT_WAIST, SLOT_WRISTS, SLOT_HANDS]) ? 1 : 0;
                    for ($i = 5; $i < 9; $i++)
                        if ($itemData[$i] > 0 && (!$items->getEntry($itemData[$i]) || $i >= (5 + $nSockets)))
                            $itemData[$i] = 0;

                    // item enchantments are borked
                    if ($itemData[3] && !$enchs->getEntry($itemData[3]))
                        $itemData[3] = 0;

                    if ($itemData[4] && !$enchs->getEntry($itemData[4]))
                        $itemData[4] = 0;

                    // looks good
                    array_unshift($itemData, $charId);
                    DB::Aowow()->query('REPLACE INTO ?_profiler_items (?#) VALUES (?a)', $keys, $itemData);
                }
            }
        }

        return (string)$charId;
    }

    /*  params
            id: <prId1,prId2,..,prIdN>
        return
            null
    */
    protected function handleDelete() : void                // kill a profile
    {
        if (!User::$id || !$this->_get['id'])
        {
            trigger_error('AjaxProfile::handleDelete - profileId empty or user not logged in', E_USER_ERROR);
            return;
        }

        // only flag as deleted; only custom profiles
        DB::Aowow()->query(
            'UPDATE ?_profiler_profiles SET cuFlags = cuFlags | ?d WHERE id IN (?a) AND cuFlags & ?d {AND user = ?d}',
            PROFILER_CU_DELETED,
            $this->_get['id'],
            PROFILER_CU_PROFILE,
            User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU) ? DBSIMPLE_SKIP : User::$id
        );
    }

    /*  params
            id: profileId
            items: string       [itemIds.join(':')]
            unnamed: unixtime   [only to force the browser to reload instead of cache]
        return
            lots...
    */
    protected function handleLoad() : string
    {
        // titles, achievements, characterData, talents, pets
        // and some onLoad-hook to .. load it registerProfile($data)
        // everything else goes through data.php .. strangely enough

        if (!$this->_get['id'])
        {
            trigger_error('AjaxProfile::handleLoad - profileId empty', E_USER_ERROR);
            return '';
        }

        $pBase = DB::Aowow()->selectRow('SELECT pg.name AS guildname, p.* FROM ?_profiler_profiles p LEFT JOIN ?_profiler_guild pg ON pg.id = p.guild WHERE p.id = ?d', $this->_get['id'][0]);
        if (!$pBase)
        {
            trigger_error('Profiler::handleLoad - called with invalid profileId #'.$this->_get['id'][0], E_USER_WARNING);
            return '';
        }

        if (($pBase['cuFlags'] & PROFILER_CU_DELETED) && !User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU))
            return '';


        $rData = [];
        foreach (Profiler::getRealms() as $rId => $rData)
            if ($rId == $pBase['realm'])
                break;

        if (!$rData)                                        // realm doesn't exist or access is restricted
            return '';

        $profile = array(
            'id'                => $pBase['id'],
            'source'            => $pBase['id'],
            'level'             => $pBase['level'],
            'classs'            => $pBase['class'],
            'race'              => $pBase['race'],
            'faction'           => Game::sideByRaceMask(1 << ($pBase['race'] - 1)) - 1,
            'gender'            => $pBase['gender'],
            'skincolor'         => $pBase['skincolor'],
            'hairstyle'         => $pBase['hairstyle'],
            'haircolor'         => $pBase['haircolor'],
            'facetype'          => $pBase['facetype'],
            'features'          => $pBase['features'],
            'title'             => $pBase['title'],
            'name'              => $pBase['name'],
            'guild'             => "$'".$pBase['guildname']."'",
            'published'         => !!($pBase['cuFlags'] & PROFILER_CU_PUBLISHED),
            'pinned'            => !!($pBase['cuFlags'] & PROFILER_CU_PINNED),
            'nomodel'           => $pBase['nomodelMask'],
            'playedtime'        => $pBase['playedtime'],
            'lastupdated'       => $pBase['lastupdated'] * 1000,
            'talents'           => array(
                'builds' => array(                          // notice the bullshit to prevent the talent-string from becoming a float! NOTICE IT!!
                    ['talents' => '$"'.$pBase['talentbuild1'].'"', 'glyphs' => $pBase['glyphs1']],
                    ['talents' => '$"'.$pBase['talentbuild2'].'"', 'glyphs' => $pBase['glyphs2']]
                ),
                'active' => $pBase['activespec']
            ),
            // set later
            'inventory'         => [],
            'bookmarks'         => [],                      // list of userIds who claimed this profile (claiming and owning are two different things)

            // completion lists: [subjectId => amount/timestamp/1]
            'skills'            => [],                      // skillId => [curVal, maxVal]
            'reputation'        => [],                      // factionId => curVal
            'titles'            => [],                      // titleId => 1
            'spells'            => [],                      // spellId => 1; recipes, vanity pets, mounts
            'achievements'      => [],                      // achievementId => timestamp
            'quests'            => [],                      // questId => 1
            'achievementpoints' => 0,                       // max you have
            'statistics'        => [],                      // all raid activity    [achievementId => killCount]
            'activity'          => [],                      // recent raid activity [achievementId => 1] (is a subset of statistics)
        );

        if ($pBase['cuFlags'] & PROFILER_CU_PROFILE)
        {
            // this parameter is _really_ strange .. probably still not doing this right
            $profile['source']      = $pBase['realm'] ? $pBase['sourceId'] : 0;

            $profile['sourcename']  = $pBase['sourceName'];
            $profile['description'] = $pBase['description'];
            $profile['user']        = $pBase['user'];
            $profile['username']    = DB::Aowow()->selectCell('SELECT displayName FROM ?_account WHERE id = ?d', $pBase['user']);
        }

        // custom profiles inherit this when copied from real char :(
        if ($pBase['realm'])
        {
            $profile['region']      = [$rData['region'], Lang::profiler('regions', $rData['region'])];
            $profile['battlegroup'] = [Profiler::urlize(Cfg::get('BATTLEGROUP')), Cfg::get('BATTLEGROUP')];
            $profile['realm']       = [Profiler::urlize($rData['name'], true), $rData['name']];
        }

        // bookmarks
        if ($_ = DB::Aowow()->selectCol('SELECT accountId FROM ?_account_profiles WHERE profileId = ?d', $pBase['id']))
            $profile['bookmarks'] = $_;

        // arena teams - [size(2|3|5) => DisplayName]; DisplayName gets urlized to use as link
        if ($at = DB::Aowow()->selectCol('SELECT type AS ARRAY_KEY, name FROM ?_profiler_arena_team at JOIN ?_profiler_arena_team_member atm ON atm.arenaTeamId = at.id WHERE atm.profileId = ?d', $pBase['id']))
            $profile['arenateams'] = $at;

        // pets if hunter fields: [name:name, family:petFamily, npc:npcId, displayId:modelId, talents:talentString]
        if ($pets = DB::Aowow()->select('SELECT `name`, `family`, `npc`, `displayId`, CONCAT("$\"", `talents`, "\"") AS "talents" FROM ?_profiler_pets WHERE `owner` = ?d', $pBase['id']))
            $profile['pets'] = $pets;

        // source for custom profiles; profileId => [name, ownerId, iconString(optional)]
        if ($customs = DB::Aowow()->select('SELECT id AS ARRAY_KEY, name, user, icon FROM ?_profiler_profiles WHERE sourceId = ?d AND sourceId <> id {AND (cuFlags & ?d) = 0}', $pBase['id'], User::isInGroup(U_GROUP_STAFF) ? DBSIMPLE_SKIP : PROFILER_CU_DELETED))
        {
            foreach ($customs as $id => $cu)
            {
                if (!$cu['icon'])
                    unset($cu['icon']);

                $profile['customs'][$id] = array_values($cu);
            }
        }


        /* $profile[]
            // CUSTOM
            'auras'             => [],                      // custom list of buffs, debuffs [spellId]

            // UNUSED
            'glyphs'            => [],                      // provided list of already known glyphs (post cataclysm feature)
        */


        // questId => [cat1, cat2]
        $profile['quests'] = [];
        if ($quests = DB::Aowow()->selectCol('SELECT `questId` FROM ?_profiler_completion_quests WHERE `id` = ?d', $pBase['id']))
        {
            $qList = new QuestList(array(['id', $quests], Cfg::get('SQL_LIMIT_NONE')));
            if (!$qList->error)
                foreach ($qList->iterate() as $id => $__)
                    $profile['quests'][$id] = [$qList->getField('cat1'), $qList->getField('cat2')];
        }

        // skillId => [value, max]
        $profile['skills'] = DB::Aowow()->select('SELECT `skillId` AS ARRAY_KEY, `value` AS "0", `max` AS "1" FROM ?_profiler_completion_skills WHERE `id` = ?d', $pBase['id']);

        // factionId => amount
        $profile['reputation'] = DB::Aowow()->selectCol('SELECT `factionId` AS ARRAY_KEY, `standing` FROM ?_profiler_completion_reputation WHERE `id` = ?d', $pBase['id']);

        // titleId => 1
        $profile['titles'] = DB::Aowow()->selectCol('SELECT `titleId` AS ARRAY_KEY, 1 FROM ?_profiler_completion_titles WHERE `id` = ?d', $pBase['id']);

        // achievementId => js date object
        $profile['achievements'] = DB::Aowow()->selectCol('SELECT `achievementId` AS ARRAY_KEY, CONCAT("$new Date(", `date` * 1000, ")") FROM ?_profiler_completion_achievements WHERE `id` = ?d', $pBase['id']);

        // just points
        $profile['achievementpoints'] = $profile['achievements'] ? DB::Aowow()->selectCell('SELECT SUM(`points`) FROM ?_achievement WHERE `id` IN (?a)', array_keys($profile['achievements'])) : 0;

        // achievementId => counter
        $profile['statistics'] = DB::Aowow()->selectCol('SELECT `achievementId` AS ARRAY_KEY, `counter` FROM ?_profiler_completion_statistics WHERE `id` = ?d', $pBase['id']);

        // achievementId => 1
        $profile['activity'] = DB::Aowow()->selectCol('SELECT `achievementId` AS ARRAY_KEY, 1 FROM ?_profiler_completion_statistics WHERE `id` = ?d AND `date` > ?d', $pBase['id'], time() - MONTH);

        // spellId => 1
        $profile['spells'] = DB::Aowow()->selectCol('SELECT `spellId` AS ARRAY_KEY, 1 FROM ?_profiler_completion_spells WHERE `id` = ?d', $pBase['id']);


        $gItems = [];

        $usedSlots = [];
        if ($this->_get['items'])
        {
            $phItems = new ItemList(array(['id', $this->_get['items']], ['slot', INVTYPE_NON_EQUIP, '!']));
            if (!$phItems->error)
            {
                $data  = $phItems->getListviewData(ITEMINFO_JSON | ITEMINFO_SUBITEMS);
                foreach ($phItems->iterate() as $iId => $__)
                {
                    $sl = $phItems->getField('slot');
                    foreach (Profiler::$slot2InvType as $slot => $invTypes)
                    {
                        if (in_array($sl, $invTypes) && !in_array($slot, $usedSlots))
                        {
                            // get and apply inventory
                            $gItems[$iId] = array(
                                'name_'.User::$localeString => $phItems->getField('name', true),
                                'quality'                   => $phItems->getField('quality'),
                                'icon'                      => $phItems->getField('iconString'),
                                'jsonequip'                 => $data[$iId]
                            );
                            $profile['inventory'][$slot] = [$iId, 0, 0, 0, 0, 0, 0, 0];

                            $usedSlots[] = $slot;
                            break;
                        }
                    }
                }
            }
        }

        if ($items = DB::Aowow()->select('SELECT * FROM ?_profiler_items WHERE id = ?d', $pBase['id']))
        {
            $itemz = new ItemList(array(['id', array_column($items, 'item')], Cfg::get('SQL_LIMIT_NONE')));
            if (!$itemz->error)
            {
                $data = $itemz->getListviewData(ITEMINFO_JSON | ITEMINFO_SUBITEMS);

                foreach ($items as $i)
                {
                    if ($itemz->getEntry($i['item']) && !in_array($i['slot'], $usedSlots))
                    {
                        // get and apply inventory
                        $gItems[$i['item']] = array(
                            'name_'.User::$localeString => $itemz->getField('name', true),
                            'quality'                   => $itemz->getField('quality'),
                            'icon'                      => $itemz->getField('iconString'),
                            'jsonequip'                 => $data[$i['item']]
                        );
                        $profile['inventory'][$i['slot']] = [$i['item'], $i['subItem'], $i['permEnchant'], $i['tempEnchant'], $i['gem1'], $i['gem2'], $i['gem3'], $i['gem4']];
                    }
                }
            }
        }

        $buff = '';
        foreach ($gItems as $id => $item)
            $buff .= 'g_items.add('.$id.', '.Util::toJSON($item, JSON_NUMERIC_CHECK | JSON_UNESCAPED_UNICODE).");\n";


        // if ($au = $char->getField('auras'))
        // {
            // $auraz = new SpellList(array(['id', $char->getField('auras')], Cfg::get('SQL_LIMIT_NONE')));
            // $dataz = $auraz->getListviewData();
            // $modz  = $auraz->getProfilerMods();

            // // get and apply aura-mods
            // foreach ($dataz as $id => $data)
            // {
                // $mods = [];
                // if (!empty($modz[$id]))
                // {
                    // foreach ($modz[$id] as $k => $v)
                    // {
                        // if (is_array($v))
                            // $mods[] = $v;
                        // else if ($str = @Game::$itemMods[$k])
                            // $mods[$str] = $v;
                    // }
                // }

                // $buff .= 'g_spells.add('.$id.", {id:".$id.", name:'".Util::jsEscape(mb_substr($data['name'], 1))."', icon:'".$data['icon']."', modifier:".Util::toJSON($mods)."});\n";
            // }
            // $buff .= "\n";
        // }


        // load available titles
        Util::loadStaticFile('p-titles-'.$pBase['gender'], $buff, true);

        // add profile to buffer
        $buff .= "\n\n\$WowheadProfiler.registerProfile(".Util::toJSON($profile).");";

        return $buff."\n";
    }

    /*  params
            id: <prId>
            data: <mode>                [string, tabName]
        return
            null
    */
    protected function handlePurge() : void { }             // removes completion data (as uploaded by the wowhead client) Just fail silently if someone triggers this manually

    protected static function checkItemList($val) : array
    {
        // expecting item-list
        if (preg_match('/\d+(:\d+)*/', $val))
            return array_map('intVal', explode(':', $val));

        return [];
    }

    protected static function checkUser(string $val) : string
    {
        if (User::isValidName($val))
            return $val;

        return '';
    }

    protected static function checkTalentString(string $val) : string
    {
        if (preg_match('/^\d+$/', $val))
            return $val;

        return '';
    }

    protected static function checkGlyphString(string $val) : string
    {
        if (preg_match('/^\d+(:\d+)*$/', $val))
            return $val;

        return '';
    }
}

?>
