//
//  MLPubSubProcessor.m
//  monalxmpp
//
//  Created by Thilo Molitor on 31.10.20.
//  Copyright © 2020 Monal.im. All rights reserved.
//

#import <Foundation/Foundation.h>

#import "MLConstants.h"
#import "MLPubSubProcessor.h"
#import "MLPubSub.h"
#import "MLHandler.h"
#import "xmpp.h"
#import "DataLayer.h"
#import "MLImageManager.h"
#import "MLNotificationQueue.h"
#import "MLMucProcessor.h"
#import "XMPPIQ.h"
#import "HelperTools.h"

@interface MLPubSubProcessor()

@end

@interface MLMucProcessor ()
-(void) sendDiscoQueryFor:(NSString*) roomJid withJoin:(BOOL) join andBookmarksUpdate:(BOOL) updateBookmarks;
-(void) sendJoinPresenceFor:(NSString*) room;
-(NSString*) calculateNickForMuc:(NSString*) room;
@end

@implementation MLPubSubProcessor

$$class_handler(mdsHandler, $$ID(xmpp*, account), $$ID(NSString*, jid), $$ID(NSString*, type), $_ID((NSDictionary<NSString*, MLXMLNode*>*), data))
    DDLogDebug(@"Got new mds displayed status from '%@' (should be own jid)...", jid);
    if(![jid isEqualToString:account.connectionProperties.identity.jid])
    {
        DDLogWarn(@"Ignoring mds update not coming from our own jid");
        return;
    }
    
    if([type isEqualToString:@"publish"])
        [account updateMdsData:data];
$$

$$class_handler(handleMdsFetchResult, $$ID(xmpp*, account), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason), $_ID((NSDictionary<NSString*, MLXMLNode*>*), data))
    if(!success)
    {
        //item-not-found means: no mds items in storage --> use an empty data dict
        if([errorIq check:@"error/{urn:ietf:params:xml:ns:xmpp-stanzas}item-not-found"])
            data = @{};
        else
        {
            DDLogWarn(@"Could not fetch mds from pep, doing nothing!");
            return;
        }
    }
    
    //call +notify handler to process our data dictionary containing all mds items
    [account updateMdsData:data];
$$

$$class_handler(avatarHandler, $$ID(xmpp*, account), $$ID(NSString*, jid), $$ID(NSString*, type), $_ID((NSDictionary<NSString*, MLXMLNode*>*), data))
    DDLogDebug(@"Got new avatar metadata from '%@'", jid);
    if([type isEqualToString:@"publish"])
    {
        for(NSString* entry in data)
        {
            MLXMLNode* metadata = [data[entry] findFirst:@"{urn:xmpp:avatar:metadata}metadata/info"];
            NSString* avatarHash = [metadata findFirst:@"/@id"];
            if(!avatarHash)     //the user disabled his avatar
            {
                DDLogInfo(@"User '%@' disabled his avatar", jid);
                [[MLImageManager sharedInstance] setIconForContact:[MLContact createContactFromJid:jid andAccountID:account.accountID] WithData:nil];
                [[DataLayer sharedInstance] setAvatarHash:@"" forContact:jid andAccount:account.accountID];
                //delete cache to make sure the image will be regenerated
                [[MLImageManager sharedInstance] purgeCacheForContact:jid andAccount:account.accountID];
                [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:account userInfo:@{
                    @"contact": [MLContact createContactFromJid:jid andAccountID:account.accountID]
                }];
            }
            else
            {
                NSString* currentHash = [[DataLayer sharedInstance] getAvatarHashForContact:jid andAccount:account.accountID];
                if(currentHash && [avatarHash isEqualToString:currentHash])
                {
                    DDLogInfo(@"Avatar hash of '%@' is the same, we don't need to update our avatar image data", jid);
                    break;
                }
                //only allow a maximum of 72KiB of image data when in appex due to appex memory limits
                //--> ignore metadata elements bigger than this size and only hande them once not in appex anymore
                NSUInteger avatarByteSize = [[metadata findFirst:@"/@bytes|int"] unsignedIntegerValue];
                if(![HelperTools isAppExtension] || avatarByteSize < 128 * 1024)
                    [account.pubsub fetchNode:@"urn:xmpp:avatar:data" from:jid withItemsList:@[avatarHash] andHandler:$newHandler(self, handleAvatarFetchResult, $ID(metadata))];
                else
                {
                    DDLogWarn(@"Not loading avatar image of '%@' because it is too big to be handled in appex (%lu bytes), rescheduling it to be fetched in mainapp", jid, (unsigned long)avatarByteSize);
                    [account addReconnectionHandler:$newHandler(self, fetchAvatarAgain, $ID(jid), $ID(avatarHash), $ID(metadata))];
                }
            }
            break;      //we only want to process the first item (this should also be the only item)
        }
        if([data count] > 1)
            DDLogWarn(@"Got more than one avatar metadata item!");
    }
    else
    {
        DDLogInfo(@"User %@ disabled his avatar", jid);
        [[MLImageManager sharedInstance] setIconForContact:[MLContact createContactFromJid:jid andAccountID:account.accountID] WithData:nil];
        [[DataLayer sharedInstance] setAvatarHash:@"" forContact:jid andAccount:account.accountID];
        //delete cache to make sure the image will be regenerated
        [[MLImageManager sharedInstance] purgeCacheForContact:jid andAccount:account.accountID];
        [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:account userInfo:@{
            @"contact": [MLContact createContactFromJid:jid andAccountID:account.accountID]
        }];
    }
$$

//this handler will simply retry the fetchNode for urn:xmpp:avatar:data if in mainapp
$$class_handler(fetchAvatarAgain, $$ID(xmpp*, account), $$ID(NSString*, jid), $$ID(NSString*, avatarHash), $$ID(MLXMLNode*, metadata))
    if([HelperTools isAppExtension])
    {
        DDLogWarn(@"Not loading avatar image of '%@' because we are still in appex, rescheduling it again!", jid);
        [account addReconnectionHandler:$newHandler(self, fetchAvatarAgain, $ID(jid), $ID(avatarHash), $ID(metadata))];
    }
    else
        [account.pubsub fetchNode:@"urn:xmpp:avatar:data" from:jid withItemsList:@[avatarHash] andHandler:$newHandler(self, handleAvatarFetchResult, $ID(metadata))];
$$

$$class_handler(handleAvatarFetchResult, $$ID(xmpp*, account), $$ID(NSString*, jid), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(XMPPIQ*, errorReason), $_ID((NSDictionary<NSString*, MLXMLNode*>*), data), $$ID(MLXMLNode*, metadata))
    //ignore errors here (e.g. simply don't update the avatar image)
    //(this should never happen if other clients and servers behave properly)
    if(!success)
    {
        DDLogWarn(@"Got avatar image fetch error from jid %@: errorIq=%@, errorReason=%@", jid, errorIq, errorReason);
        return;
    }
    
    for(NSString* avatarHash in data)
    {
        //this should be small enough to not crash the appex when loading the image from file later on but large enough to have excellent quality
        NSData* avatarData = [data[avatarHash] findFirst:@"{urn:xmpp:avatar:data}data#|base64"];
        UIImage* image = nil;
        if([[metadata findFirst:@"/@type"] hasPrefix:@"image/svg"])
            image = (UIImage*)nilExtractor(PMKHang([HelperTools renderUIImageFromSVGData:avatarData]));
        else
            image = [UIImage imageWithData:avatarData];
        if(image == nil)
        {
            DDLogWarn(@"Failed to load avatar of %@", jid);
            return;
        }
        //this upper limit is roughly 1.4MiB memory (600x600 with 4 byte per pixel)
        if(![HelperTools isAppExtension] || image.size.width * image.size.height < 600 * 600)
        {
            NSData* imageData = [HelperTools resizeAvatarImage:image withCircularMask:YES toMaxBase64Size:256000];
            [[MLImageManager sharedInstance] setIconForContact:[MLContact createContactFromJid:jid andAccountID:account.accountID] WithData:imageData];
            [[DataLayer sharedInstance] setAvatarHash:avatarHash forContact:jid andAccount:account.accountID];
            //delete cache to make sure the image will be regenerated
            [[MLImageManager sharedInstance] purgeCacheForContact:jid andAccount:account.accountID];
            [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:account userInfo:@{
                @"contact": [MLContact createContactFromJid:jid andAccountID:account.accountID]
            }];
            DDLogInfo(@"Avatar of '%@' fetched and updated successfully", jid);
        }
        else
        {
            DDLogWarn(@"Not loading avatar image of '%@' because it is too big to be processed in appex (%lux%lu pixels), rescheduling it to be fetched in mainapp", jid, (unsigned long)image.size.width, (unsigned long)image.size.height);
            [account addReconnectionHandler:$newHandler(self, fetchAvatarAgain, $ID(jid), $ID(avatarHash), $ID(metadata))];
        }
    }
$$

$$class_handler(rosterNameHandler, $$ID(xmpp*, account), $$ID(NSString*, jid), $$ID(NSString*, type), $_ID((NSDictionary<NSString*, MLXMLNode*>*), data))
    //new/updated nickname
    if([type isEqualToString:@"publish"])
    {
        for(NSString* itemId in data)
        {
            if([jid isEqualToString:account.connectionProperties.identity.jid])        //own roster name
            {
                DDLogInfo(@"Got own nickname: %@", [data[itemId] findFirst:@"{http://jabber.org/protocol/nick}nick#"]);
                NSMutableDictionary* accountDic = [[NSMutableDictionary alloc] initWithDictionary:[[DataLayer sharedInstance] detailsForAccount:account.accountID] copyItems:YES];
                accountDic[kRosterName] = [data[itemId] findFirst:@"{http://jabber.org/protocol/nick}nick#"];
                [[DataLayer sharedInstance] updateAccounWithDictionary:accountDic];
            }
            else                                                                    //roster name of contact
            {
                DDLogInfo(@"Got nickname of %@: %@", jid, [data[itemId] findFirst:@"{http://jabber.org/protocol/nick}nick#"]);
                [[DataLayer sharedInstance] setFullName:[data[itemId] findFirst:@"{http://jabber.org/protocol/nick}nick#"] forContact:jid andAccount:account.accountID];
                MLContact* contact = [MLContact createContactFromJid:jid andAccountID:account.accountID];
                if(contact)     //ignore updates for jids not in our roster
                {
                    //delete cache to make sure the image will be regenerated
                    [[MLImageManager sharedInstance] purgeCacheForContact:jid andAccount:account.accountID];
                    [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:account userInfo:@{
                        @"contact": contact
                    }];
                }
            }
            break;      //we only need the first item (there should be only one item in the first place)
        }
    }
    //deleted/purged node or retracted item
    else
    {
        if([jid isEqualToString:account.connectionProperties.identity.jid])        //own roster name
        {
            DDLogInfo(@"Own nickname got retracted");
            NSMutableDictionary* accountDic = [[NSMutableDictionary alloc] initWithDictionary:[[DataLayer sharedInstance] detailsForAccount:account.accountID] copyItems:NO];
            accountDic[kRosterName] = @"";
            [[DataLayer sharedInstance] updateAccounWithDictionary:accountDic];
        }
        else
        {
            DDLogInfo(@"Nickname of %@ got retracted", jid);
            [[DataLayer sharedInstance] setFullName:@"" forContact:jid andAccount:account.accountID];
            MLContact* contact = [MLContact createContactFromJid:jid andAccountID:account.accountID];
            if(contact)     //ignore updates for jids not in our roster
            {
                //delete cache to make sure the image will be regenerated
                [[MLImageManager sharedInstance] purgeCacheForContact:jid andAccount:account.accountID];
                [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:account userInfo:@{
                    @"contact": contact
                }];
            }
        }
    }
$$

$$class_handler(bookmarks2Handler, $$ID(xmpp*, account), $$ID(NSString*, jid), $$ID(NSString*, type), $_ID((NSDictionary<NSString*, MLXMLNode*>*), data))
    if(!account.connectionProperties.supportsBookmarksCompat)
    {
        DDLogWarn(@"Ignoring new XEP-0402 bookmarks, server does not support syncing between XEP-0048 and XEP-0402!");
        return;
    }
    
    //type will be "publish", "retract", "purge" or "delete". "publish" and "retract" will have the data dictionary filled with id --> data pairs
    //the data for "publish" is the item node with the given id, the data for "retract" is always @YES
    if(![jid isEqualToString:account.connectionProperties.identity.jid])
    {
        DDLogWarn(@"Ignoring bookmarks update not coming from our own jid");
        return;
    }
    
    NSSet* ownFavorites = [[DataLayer sharedInstance] listMucsForAccount:account.accountID];
    
    //new/updated bookmarks
    if([type isEqualToString:@"publish"])
    {
        //iterate through all conference elements provided
        for(NSString* itemId in data)
        {
            //we ignore the conference name (the name will be taken from the muc itself)
            //NSString* name = [data[itemId] findFirst:@"{urn:xmpp:bookmarks:1}conference@name"];
            NSString* room = [itemId lowercaseString];
            NSString* nick = [data[itemId] findFirst:@"{urn:xmpp:bookmarks:1}conference/nick#"];
            //ignore password protected mucs
            if([data[itemId] check:@"{urn:xmpp:bookmarks:1}conference/password"])
                continue;
            NSNumber* autojoin = [data[itemId] findFirst:@"{urn:xmpp:bookmarks:1}conference@autojoin|bool"];
            if(autojoin == nil)
                autojoin = @NO;     //default value specified in xep
            
            //check if this is a new entry with autojoin=true
            if(![ownFavorites containsObject:room] && [autojoin boolValue])
            {
                DDLogInfo(@"Entering muc '%@' on account %@ because it got added to bookmarks...", room, account.accountID);
                //make sure we update our favorites table right away, to counter any race conditions when joining multiple mucs with one bookmarks update
                if(nick == nil)
                    nick = [account.mucProcessor calculateNickForMuc:room];
                //this will record the desired nickname: the mucProcessor will pick that up and use it to join the muc
                [[DataLayer sharedInstance] addMucFavorite:room forAccountID:account.accountID andMucNick:nick];
                //try to join muc, but don't perform a bookmarks update (this muc came in through a bookmark already)
                [account.mucProcessor sendDiscoQueryFor:room withJoin:YES andBookmarksUpdate:NO];
            }
            //check if it is a known entry that changed autojoin to false
            else if([ownFavorites containsObject:room] && ![autojoin boolValue])
            {
                DDLogInfo(@"Leaving muc '%@' on account %@ because not listed as autojoin=true in bookmarks...", room, account.accountID);
                //delete local favorites entry and leave room afterwards, but keep buddylist entry because only the autojoin flag changed
                [account.mucProcessor leave:room withBookmarksUpdate:NO keepBuddylistEntry:YES];
            }
            //check for nickname changes
            else if([ownFavorites containsObject:room] && nick != nil)
            {
                NSString* oldNick = [[DataLayer sharedInstance] ownNickNameforMuc:room forAccount:account.accountID];
                if(![nick isEqualToString:oldNick])
                {
                    DDLogInfo(@"Updating muc '%@' nick on account %@ in database to nick provided by bookmarks: '%@'...", room, account.accountID, nick);
                    
                    //update muc nickname in database
                    [[DataLayer sharedInstance] updateOwnNickName:nick forMuc:room forAccount:account.accountID];
                    [[DataLayer sharedInstance] addMucFavorite:room forAccountID:account.accountID andMucNick:nick];        //this will upate the already existing favorites entry
                    
                    //rejoin the muc (e.g. change nick)
                    //we don't have to do a full disco because we are sure this is a real muc and we are joined already
                    //(only real mucs are part of our local favorites list and this list is joined automatically)
                    [account.mucProcessor sendJoinPresenceFor:room];
                }
            }
        }
    }
    else if([type isEqualToString:@"retract"])
    {
        for(NSString* itemId in data)
        {
            NSString* room = [itemId lowercaseString];
            if([ownFavorites containsObject:room])
            {
                DDLogInfo(@"Leaving muc '%@' on account %@ because not listed in bookmarks anymore...", room, account.accountID);
                //delete local favorites entry and leave room afterwards
                [account.mucProcessor leave:room withBookmarksUpdate:NO keepBuddylistEntry:NO];
            }
            else
                DDLogVerbose(@"Ignoring retracted bookmark because not listed in muc_favorites already...");
        }
    }
    else
    {
        //deleted/purged node (e.g. all bookmarks deleted)
        //--> remove and leave all mucs
        for(NSString* room in ownFavorites)
        {
            DDLogInfo(@"Leaving muc '%@' on account %@ because all bookmarks got deleted...", room, account.accountID);
            //delete local favorites entry and leave room afterwards
            [account.mucProcessor leave:room withBookmarksUpdate:NO keepBuddylistEntry:NO];
        }
    }
$$

$$class_handler(handleBookmarks2FetchResult, $$ID(xmpp*, account), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason), $_ID((NSDictionary<NSString*, MLXMLNode*>*), data))
    if(!account.connectionProperties.supportsBookmarksCompat)
    {
        DDLogWarn(@"Ignoring new XEP-0402 bookmarks, server does not support syncing between XEP-0048 and XEP-0402!");
        return;
    }
    
    if(!success)
    {
        //item-not-found means: no bookmarks in storage --> use an empty data dict
        if([errorIq check:@"error/{urn:ietf:params:xml:ns:xmpp-stanzas}item-not-found"])
            data = @{};
        else
        {
            DDLogWarn(@"Could not fetch bookmarks from pep prior to publishing!");
            [self handleErrorWithDescription:NSLocalizedString(@"Failed to save groupchat bookmarks", @"") andAccount:account andErrorIq:errorIq andErrorReason:errorReason andIsSevere:YES];
            return;
        }
    }
    
    NSString* max_items = @"255";       //fallback for servers not supporting "max"
    if(account.connectionProperties.supportsPubSubMax)
        max_items = @"max";
    NSDictionary* infoDict = [[NSBundle mainBundle] infoDictionary];
    
    NSSet* ownFavorites = [[DataLayer sharedInstance] listMucsForAccount:account.accountID];
    DDLogVerbose(@"Own favorites: %@", ownFavorites);
    
    //filter passwort protected mucs and make sure jids (the item ids) are always lowercase
    NSMutableDictionary* _data = [NSMutableDictionary new];
    for(NSString* itemId in data)
    {
        if([data[itemId] check:@"{urn:xmpp:bookmarks:1}conference/password"])
        {
            DDLogVerbose(@"Not copying muc %@ to bookmark data: password protected", itemId);
            continue;
        }
        _data[[itemId lowercaseString]] = data[itemId];
    }
    DDLogVerbose(@"Mucs listed in bookmarks2: %@", [_data allKeys]);
    
    //handle all changes of existing bookmarks
    for(NSString* room in _data)
    {
        MLXMLNode* item = _data[room];
        
        //we ignore the conference name (the name will be taken from the muc itself)
        //NSString* name = [_data[room] findFirst:@"{urn:xmpp:bookmarks:1}conference@name"];
        //NSString* nick = [_data[room] findFirst:@"{urn:xmpp:bookmarks:1}conference/nick#"];
        NSNumber* autojoin = [item findFirst:@"{urn:xmpp:bookmarks:1}conference@autojoin|bool"];
        if(autojoin == nil)
            autojoin = @NO;     //default value specified in xep
        
        //check if the bookmark exists with autojoin==false and only update the autojoin and nick values, if true
        if([ownFavorites containsObject:room] && ![autojoin boolValue])
        {
            DDLogInfo(@"Updating autojoin of bookmarked muc '%@' on account %@ to 'true'...", room, account.accountID);
            
            //add or update nickname
            NSString* nick = [[DataLayer sharedInstance] ownNickNameforMuc:room forAccount:account.accountID];
            if(nick != nil)
            {
                if(![item check:@"{urn:xmpp:bookmarks:1}conference/nick"])
                    [[item findFirst:@"{urn:xmpp:bookmarks:1}conference"] addChildNode:[[MLXMLNode alloc] initWithElement:@"nick"]];
                ((MLXMLNode*)[item findFirst:@"{urn:xmpp:bookmarks:1}conference/nick"]).data = nick;
            }
            
            //update autojoin value to true
            ((MLXMLNode*)[item findFirst:@"{urn:xmpp:bookmarks:1}conference"]).attributes[@"autojoin"] = @"true";
            
            //publish this bookmark item again
            [account.pubsub publishItem:item onNode:@"urn:xmpp:bookmarks:1" withConfigOptions:@{
                @"pubsub#persist_items": @"true",
                @"pubsub#access_model": @"whitelist",
                @"pubsub#max_items": max_items,
            } andHandler:$newHandler(self, bookmarks2Published, $ID(room))];
        }
    }
        
    //add all mucs not yet listed in bookmarks
    NSMutableSet* toAdd = [ownFavorites mutableCopy];
    [toAdd  minusSet:[NSSet setWithArray:[_data allKeys]]];
    for(NSString* room in toAdd)
    {
        DDLogInfo(@"Adding muc '%@' on account %@ to bookmarks...", room, account.accountID);
        NSString* nick = [[DataLayer sharedInstance] ownNickNameforMuc:room forAccount:account.accountID];
        [account.pubsub publishItem:
            [[MLXMLNode alloc] initWithElement:@"item" withAttributes:@{@"id": room} andChildren:@[
                [[MLXMLNode alloc] initWithElement:@"conference" andNamespace:@"urn:xmpp:bookmarks:1" withAttributes:@{
                    @"autojoin": @"true",
                } andChildren:@[
                    nilWrapper(nick != nil ? [[MLXMLNode alloc] initWithElement:@"nick" withAttributes:@{} andChildren:@[] andData:nick] : nil),
                    [[MLXMLNode alloc] initWithElement:@"extensions" withAttributes:@{} andChildren:@[
                        [[MLXMLNode alloc] initWithElement:@"added-by" andNamespace:@"urn:monal.im:bookmarks:info" withAttributes:@{
                            @"name": @"Monal",
                            @"version": infoDict[@"CFBundleShortVersionString"],
                            @"build": infoDict[@"CFBundleVersion"],
                        } andChildren:@[] andData:nil]
                    ] andData:nil]
                ]andData:nil]
            ] andData:nil]
        onNode:@"urn:xmpp:bookmarks:1" withConfigOptions:@{
            @"pubsub#persist_items": @"true",
            @"pubsub#access_model": @"whitelist",
            @"pubsub#max_items": max_items,
        } andHandler:$newHandler(self, bookmarks2Published, $ID(room))];
    }
    
    //remove all mucs not listed in local favorites table
    NSMutableSet* toRemove = [NSMutableSet setWithArray:[_data allKeys]];
    [toRemove  minusSet:ownFavorites];
    for(NSString* room in toRemove)
    {
        DDLogInfo(@"Removing muc '%@' on account %@ from bookmarks...", room, account.accountID);
        [account.pubsub retractItemWithId:room onNode:@"urn:xmpp:bookmarks:1" andHandler:$newHandler(self, bookmarks2Retracted, $ID(room))];
    }
$$

$$class_handler(bookmarks2Published, $$ID(xmpp*, account), $$ID(NSString*, room), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason))
    if(!account.connectionProperties.supportsBookmarksCompat)
    {
        DDLogWarn(@"Ignoring new XEP-0402 bookmarks, server does not support syncing between XEP-0048 and XEP-0402!");
        return;
    }
    
    if(!success)
    {
        DDLogWarn(@"Could not publish bookmark for muc '%@' to pep!", room);
        [self handleErrorWithDescription:[NSString stringWithFormat:NSLocalizedString(@"Failed to save bookmark for Group/Channel: %@", @""), room] andAccount:account andErrorIq:errorIq andErrorReason:errorReason andIsSevere:YES];
        return;
    }
    DDLogDebug(@"Published bookmark for muc '%@' to pep", room);
$$

$$class_handler(bookmarks2Retracted, $$ID(xmpp*, account), $$ID(NSString*, room), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason))
    if(!account.connectionProperties.supportsBookmarksCompat)
    {
        DDLogWarn(@"Ignoring new XEP-0402 bookmarks, server does not support syncing between XEP-0048 and XEP-0402!");
        return;
    }
    
    if(!success)
    {
        DDLogWarn(@"Could not retract bookmark for muc '%@' from pep!", room);
        [self handleErrorWithDescription:[NSString stringWithFormat:NSLocalizedString(@"Failed to remove bookmark for Group/Channel: %@", @""), room] andAccount:account andErrorIq:errorIq andErrorReason:errorReason andIsSevere:YES];
        return;
    }
    DDLogDebug(@"Retracted bookmark for muc '%@' from pep", room);
$$

$$class_handler(bookmarksHandler, $$ID(xmpp*, account), $$ID(NSString*, jid), $$ID(NSString*, type), $_ID((NSDictionary<NSString*, MLXMLNode*>*), data))
    if(account.connectionProperties.supportsBookmarksCompat)
    {
        DDLogInfo(@"Ignoring old XEP-0048 bookmarks, server supports syncing between XEP-0048 and XEP-0402...");
        return;
    }
    
    if(![jid isEqualToString:account.connectionProperties.identity.jid])
    {
        DDLogWarn(@"Ignoring bookmarks update not coming from our own jid");
        return;
    }
    
    NSSet* ownFavorites = [[DataLayer sharedInstance] listMucsForAccount:account.accountID];
    
    //new/updated bookmarks
    if([type isEqualToString:@"publish"])
    {
        for(NSString* itemId in data)
        {
            //iterate through all conference elements provided
            NSMutableSet* bookmarkedMucs = [NSMutableSet new];
            for(MLXMLNode* conference in [data[itemId] find:@"{storage:bookmarks}storage/conference"])
            {
                //we ignore the conference name (the name will be taken from the muc itself)
                //NSString* name = [conference findFirst:@"/@name"];
                NSString* room = [[conference findFirst:@"/@jid"] lowercaseString];
                //ignore non-xep-compliant entries
                if(!room)
                {
                    DDLogError(@"Received non-xep-compliant bookmarks entry, ignoring: %@", conference);
                    continue;
                }
                
                //ignore password protected mucs
                if([conference check:@"password"])
                    continue;
                
                [bookmarkedMucs addObject:room];
                NSString* nick = [conference findFirst:@"nick#"];
                NSNumber* autojoin = [conference findFirst:@"/@autojoin|bool"];
                if(autojoin == nil)
                    autojoin = @NO;     //default value specified in xep
                
                //check if this is a new entry with autojoin=true
                if(![ownFavorites containsObject:room] && [autojoin boolValue])
                {
                    DDLogInfo(@"Entering muc '%@' on account %@ because it got added to bookmarks...", room, account.accountID);
                    //make sure we update our favorites table right away, to counter any race conditions when joining multiple mucs with one bookmarks update
                    if(nick == nil)
                        nick = [account.mucProcessor calculateNickForMuc:room];
                    //this will record the desired nickname: the mucProcessor will pick that up and use it to join the muc
                    [[DataLayer sharedInstance] addMucFavorite:room forAccountID:account.accountID andMucNick:nick];
                    //try to join muc, but don't perform a bookmarks update (this muc came in through a bookmark already)
                    [account.mucProcessor sendDiscoQueryFor:room withJoin:YES andBookmarksUpdate:NO];
                }
                //check if it is a known entry that changed autojoin to false
                else if([ownFavorites containsObject:room] && ![autojoin boolValue])
                {
                    DDLogInfo(@"Leaving muc '%@' on account %@ because not listed as autojoin=true in bookmarks...", room, account.accountID);
                    //delete local favorites entry and leave room afterwards, but keep buddylist entry because only the autojoin flag changed
                    [account.mucProcessor leave:room withBookmarksUpdate:NO keepBuddylistEntry:YES];
                }
                //check for nickname changes
                else if([ownFavorites containsObject:room] && nick != nil)
                {
                    NSString* oldNick = [[DataLayer sharedInstance] ownNickNameforMuc:room forAccount:account.accountID];
                    if(![nick isEqualToString:oldNick])
                    {
                        DDLogInfo(@"Updating muc '%@' nick on account %@ in database to nick provided by bookmarks: '%@'...", room, account.accountID, nick);
                        
                        //update muc nickname in database
                        [[DataLayer sharedInstance] updateOwnNickName:nick forMuc:room forAccount:account.accountID];
                        [[DataLayer sharedInstance] addMucFavorite:room forAccountID:account.accountID andMucNick:nick];        //this will upate the already existing favorites entry
                        
                        //rejoin the muc (e.g. change nick)
                        //we don't have to do a full disco because we are sure this is a real muc and we are joined already
                        //(only real mucs are part of our local favorites list and this list is joined automatically)
                        [account.mucProcessor sendJoinPresenceFor:room];
                    }
                }
            }
            
            //remove and leave all mucs removed from bookmarks
            NSMutableSet* toLeave = [ownFavorites mutableCopy];
            [toLeave  minusSet:bookmarkedMucs];
            for(NSString* room in toLeave)
            {
                DDLogInfo(@"Leaving muc '%@' on account %@ because not listed in bookmarks anymore...", room, account.accountID);
                //delete local favorites entry and leave room afterwards
                [account.mucProcessor leave:room withBookmarksUpdate:NO keepBuddylistEntry:NO];
            }
            
            return;      //we only need the first pep item (there should be only one item in the first place)
        }
        //FALLTHROUGH to "delete all" if no item was found
    }
    //deleted/purged node or retracted item (e.g. all bookmarks deleted)
    //--> remove and leave all mucs
    for(NSString* room in ownFavorites)
    {
        DDLogInfo(@"Leaving muc '%@' on account %@ because all bookmarks got deleted...", room, account.accountID);
        //delete local favorites entry and leave room afterwards
        [account.mucProcessor leave:room withBookmarksUpdate:NO keepBuddylistEntry:NO];
    }
$$

$$class_handler(handleBookarksFetchResult, $$ID(xmpp*, account), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason), $_ID((NSDictionary<NSString*, MLXMLNode*>*), data))
    if(account.connectionProperties.supportsBookmarksCompat)
    {
        DDLogInfo(@"Ignoring old XEP-0048 bookmarks, server supports syncing between XEP-0048 and XEP-0402...");
        return;
    }
    
    if(!success)
    {
        //item-not-found means: no bookmarks in storage --> use an empty data dict
        if([errorIq check:@"error/{urn:ietf:params:xml:ns:xmpp-stanzas}item-not-found"])
            data = @{};
        else
        {
            DDLogWarn(@"Could not fetch bookmarks from pep prior to publishing!");
            [self handleErrorWithDescription:NSLocalizedString(@"Failed to save groupchat bookmarks", @"") andAccount:account andErrorIq:errorIq andErrorReason:errorReason andIsSevere:YES];
            return;
        }
    }
    
    BOOL changed = NO;
    NSSet* ownFavorites = [[DataLayer sharedInstance] listMucsForAccount:account.accountID];
    
    for(NSString* itemId in data)
    {
        //ignore non-xep-compliant data and continue as if no data was received at all
        if(![data[itemId] check:@"{storage:bookmarks}storage"])
        {
            DDLogError(@"Received non-xep-compliant bookmarks data: %@", data);
            break;
        }
        
        NSMutableSet* bookmarkedMucs = [NSMutableSet new];
        for(MLXMLNode* conference in [data[itemId] find:@"{storage:bookmarks}storage/conference"])
        {
            //we ignore the conference name (the name will be taken from the muc itself)
            //NSString* name = [conference findFirst:@"/@name"];
            NSString* room = [[conference findFirst:@"/@jid"] lowercaseString];
            //ignore non-xep-compliant entries
            if(!room)
            {
                DDLogError(@"Received non-xep-compliant bookmarks entry, ignoring: %@", conference);
                continue;
            }
            [bookmarkedMucs addObject:room];
            NSNumber* autojoin = [conference findFirst:@"/@autojoin|bool"];
            if(autojoin == nil)
                autojoin = @NO;     //default value specified in xep
            
            //check if the bookmark exists with autojoin==false and only update the autojoin and nick values, if true
            if([ownFavorites containsObject:room] && ![autojoin boolValue])
            {
                DDLogInfo(@"Updating autojoin of bookmarked muc '%@' on account %@ to 'true'...", room, account.accountID);
                
                //add or update nickname
                NSString* nick = [[DataLayer sharedInstance] ownNickNameforMuc:room forAccount:account.accountID];
                if(nick != nil)
                {
                    if(![conference check:@"nick"])
                        [conference addChildNode:[[MLXMLNode alloc] initWithElement:@"nick"]];
                    ((MLXMLNode*)[conference findFirst:@"nick"]).data = [[DataLayer sharedInstance] ownNickNameforMuc:room forAccount:account.accountID];
                }
                
                //update autojoin value to true
                conference.attributes[@"autojoin"] = @"true";
                changed = YES;
            }
        }
        
        //add all mucs not yet listed in bookmarks
        NSMutableSet* toAdd = [ownFavorites mutableCopy];
        [toAdd  minusSet:bookmarkedMucs];
        for(NSString* room in toAdd)
        {
            DDLogInfo(@"Adding muc '%@' on account %@ to bookmarks...", room, account.accountID);
            NSString* nick = [[DataLayer sharedInstance] ownNickNameforMuc:room forAccount:account.accountID];
            [[data[itemId] findFirst:@"{storage:bookmarks}storage"] addChildNode:[[MLXMLNode alloc] initWithElement:@"conference" withAttributes:@{
                @"jid": room,
                @"name": [[MLContact createContactFromJid:room andAccountID:account.accountID] contactDisplayName],
                @"autojoin": @"true",
            } andChildren:(nick != nil ? @[[[MLXMLNode alloc] initWithElement:@"nick" withAttributes:@{} andChildren:@[] andData:nick]] : @[]) andData:nil]];
            changed = YES;
        }
        
        //remove all mucs not listed in local favorites table
        NSMutableSet* toRemove = [bookmarkedMucs mutableCopy];
        [toRemove  minusSet:ownFavorites];
        for(NSString* room in toRemove)
        {
            DDLogInfo(@"Removing muc '%@' on account %@ from bookmarks...", room, account.accountID);
            [[data[itemId] findFirst:@"{storage:bookmarks}storage"] removeChildNode:[data[itemId] findFirst:@"{storage:bookmarks}storage/conference<jid=%@>", room]];
            changed = YES;
        }
        
        //publish new bookmarks if something was changed
        if(changed)
            [account.pubsub publishItem:data[itemId] onNode:@"storage:bookmarks" withConfigOptions:@{
                @"pubsub#persist_items": @"true",
                @"pubsub#access_model": @"whitelist"
            } andHandler:$newHandler(self, bookmarksPublished)];
        
        //we only need the first pep item (there should be only one item in the first place)
        return;
    }
    
    //don't publish an empty bookmarks node if there is nothing to publish at all
    if([ownFavorites count] == 0)
    {
        DDLogInfo(@"neither a pep item was found, nor do we have any local muc favorites: don't publish anything");
        return;
    }
    
    DDLogInfo(@"no pep item was found: publish our bookmarks the first time");
    NSMutableArray* conferences = [NSMutableArray new];
    for(NSString* room in ownFavorites)
    {
        DDLogInfo(@"Adding muc '%@' on account %@ to bookmarks...", room, account.accountID);
        NSString* nick = [[DataLayer sharedInstance] ownNickNameforMuc:room forAccount:account.accountID];
        [conferences addObject:[[MLXMLNode alloc] initWithElement:@"conference" withAttributes:@{
            @"jid": room,
            @"name": [[MLContact createContactFromJid:room andAccountID:account.accountID] contactDisplayName],
            @"autojoin": @"true",
        } andChildren:(nick != nil ? @[[[MLXMLNode alloc] initWithElement:@"nick" withAttributes:@{} andChildren:@[] andData:nick]] : @[]) andData:nil]];
    }
    [account.pubsub publishItem:
        [[MLXMLNode alloc] initWithElement:@"item" withAttributes:@{@"id": @"current"} andChildren:@[
            [[MLXMLNode alloc] initWithElement:@"storage" andNamespace:@"storage:bookmarks" withAttributes:@{} andChildren:conferences andData:nil]
        ] andData:nil]
    onNode:@"storage:bookmarks" withConfigOptions:@{
        @"pubsub#persist_items": @"true",
        @"pubsub#access_model": @"whitelist"
    } andHandler:$newHandler(self, bookmarksPublished)];
$$

$$class_handler(bookmarksPublished, $$ID(xmpp*, account), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason))
    if(account.connectionProperties.supportsBookmarksCompat)
    {
        DDLogInfo(@"Ignoring old XEP-0048 bookmarks, server supports syncing between XEP-0048 and XEP-0402...");
        return;
    }
    
    if(!success)
    {
        DDLogWarn(@"Could not publish bookmarks to pep!");
        [self handleErrorWithDescription:NSLocalizedString(@"Failed to save groupchat bookmarks", @"") andAccount:account andErrorIq:errorIq andErrorReason:errorReason andIsSevere:YES];
        return;
    }
    DDLogDebug(@"Published bookmarks to pep");
$$

$$class_handler(rosterNamePublished, $$ID(xmpp*, account), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason))
    if(!success)
    {
        DDLogWarn(@"Could not publish roster name to pep!");
        [self handleErrorWithDescription:NSLocalizedString(@"Failed to publish own nickname", @"") andAccount:account andErrorIq:errorIq andErrorReason:errorReason andIsSevere:NO];
        return;
    }
    DDLogDebug(@"Published roster name to pep");
$$

$$class_handler(rosterNameDeleted, $$ID(xmpp*, account), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason))
    if(!success)
    {
        //item-not-found means: nick already deleted --> ignore this error
        if([errorIq check:@"error/{urn:ietf:params:xml:ns:xmpp-stanzas}item-not-found"])
        {
            DDLogWarn(@"Roster name was already deleted from pep, ignoring error!");
            return;
        }
        DDLogWarn(@"Could not remove roster name from pep!");
        [self handleErrorWithDescription:NSLocalizedString(@"Failed to delete own nickname", @"") andAccount:account andErrorIq:errorIq andErrorReason:errorReason andIsSevere:NO];
        return;
    }
    DDLogDebug(@"Removed roster name from pep");
$$

$$class_handler(avatarDeleted, $$ID(xmpp*, account), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason))
    if(!success)
    {
        //item-not-found means: avatar already deleted --> ignore this error
        if([errorIq check:@"error/{urn:ietf:params:xml:ns:xmpp-stanzas}item-not-found"])
        {
            DDLogWarn(@"Avatar image was already deleted from pep, ignoring error!");
            return;
        }
        DDLogWarn(@"Could not delete avatar image from pep!");
        [self handleErrorWithDescription:NSLocalizedString(@"Failed to delete own avatar", @"") andAccount:account andErrorIq:errorIq andErrorReason:errorReason andIsSevere:NO];
        return;
    }
    DDLogDebug(@"Removed avatar from pep");
$$

$$class_handler(avatarMetadataPublished, $$ID(xmpp*, account), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason))
    if(!success)
    {
        DDLogWarn(@"Could not publish avatar metadata to pep!");
        [self handleErrorWithDescription:NSLocalizedString(@"Failed to publish own avatar", @"") andAccount:account andErrorIq:errorIq andErrorReason:errorReason andIsSevere:NO];
        return;
    }
    DDLogDebug(@"Published avatar metadata to pep");
$$

$$class_handler(avatarDataPublished, $$ID(xmpp*, account), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason), $$ID(NSString*, imageHash), $$UINTEGER(imageBytesLen))
    if(!success)
    {
        DDLogWarn(@"Could not publish avatar image data for hash %@!", imageHash);
        [self handleErrorWithDescription:NSLocalizedString(@"Failed to publish own avatar", @"") andAccount:account andErrorIq:errorIq andErrorReason:errorReason andIsSevere:NO];
        return;
    }
    
    DDLogInfo(@"Avatar image data for hash %@ published successfully, now publishing metadata", imageHash);
    
    //publish metadata node (must be done *after* publishing the new data node)
    [account.pubsub publishItem:
        [[MLXMLNode alloc] initWithElement:@"item" withAttributes:@{@"id": imageHash} andChildren:@[
            [[MLXMLNode alloc] initWithElement:@"metadata" andNamespace:@"urn:xmpp:avatar:metadata" withAttributes:@{} andChildren:@[
                [[MLXMLNode alloc] initWithElement:@"info" withAttributes:@{
                    @"id": imageHash,
                    @"type": @"image/jpeg",
                    @"bytes": [NSString stringWithFormat:@"%lu", (unsigned long)imageBytesLen]
                } andChildren:@[] andData:nil]
            ] andData:nil]
        ] andData:nil]
    onNode:@"urn:xmpp:avatar:metadata" withConfigOptions:@{
        @"pubsub#persist_items": @"true",
        @"pubsub#access_model": @"presence"
    } andHandler:$newHandler(self, avatarMetadataPublished)];
$$

+(void) handleErrorWithDescription:(NSString*) description andAccount:(xmpp*) account andErrorIq:(XMPPIQ*) errorIq andErrorReason:(NSString*) errorReason andIsSevere:(BOOL) isSevere
{
    MLAssert(errorIq || errorReason, @"at least one of errorIq or errorReason must be set when calling error handler!");
    if(errorIq)
        [HelperTools postError:description withNode:errorIq andAccount:account andIsSevere:isSevere];
    else if(errorReason)
        [HelperTools postError:[NSString stringWithFormat:@"%@: %@", description, errorReason] withNode:nil andAccount:account andIsSevere:isSevere];
}

@end