//
//  ActiveChatsViewController.m
//  Monal
//
//  Created by Anurodh Pokharel on 6/14/13.
//
//

#include "metamacros.h"

#import <Contacts/Contacts.h>
#import "ActiveChatsViewController.h"
#import "DataLayer.h"
#import "xmpp.h"
#import "MLContactCell.h"
#import "chatViewController.h"
#import "MonalAppDelegate.h"
#import "MLImageManager.h"
#import "MLXEPSlashMeHandler.h"
#import "MLNotificationQueue.h"
#import "MLSettingsAboutViewController.h"
#import "MLVoIPProcessor.h"
#import "MLCall.h"      //for MLCallType
#import "XMPPIQ.h"
#import "MLIQProcessor.h"
#import "Quicksy_Country.h"
#import <Monal-Swift.h>

#define prependToViewQueue(firstArg, ...)                           metamacro_if_eq(0, metamacro_argcount(__VA_ARGS__))([self prependToViewQueue:firstArg withId:MLViewIDUnspecified andFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__])(_prependToViewQueue(firstArg, __VA_ARGS__))
#define _prependToViewQueue(ownId, block)                           [self prependToViewQueue:block withId:ownId andFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__]
#define appendToViewQueue(firstArg, ...)                            metamacro_if_eq(0, metamacro_argcount(__VA_ARGS__))([self appendToViewQueue:firstArg withId:MLViewIDUnspecified andFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__])(_appendToViewQueue(firstArg, __VA_ARGS__))
#define _appendToViewQueue(ownId, block)                            [self prependToViewQueue:block withId:ownId andFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__]
#define appendingReplaceOnViewQueue(firstArg, secondArg, ...)       metamacro_if_eq(0, metamacro_argcount(__VA_ARGS__))([self replaceIdOnViewQueue:firstArg withBlock:secondArg havingId:MLViewIDUnspecified andAppendOnUnknown:YES withFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__])(_appendingReplaceOnViewQueue(firstArg, secondArg, __VA_ARGS__))
#define prependingReplaceOnViewQueue(firstArg, secondArg, ...)      metamacro_if_eq(0, metamacro_argcount(__VA_ARGS__))([self replaceIdOnViewQueue:firstArg withBlock:secondArg havingId:MLViewIDUnspecified andAppendOnUnknown:NO withFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__])(_prependingReplaceOnViewQueue(firstArg, secondArg, __VA_ARGS__))
#define _appendingReplaceOnViewQueue(replaceId, ownId, block)       [self replaceIdOnViewQueue:replaceId withBlock:block havingId:ownId andAppendOnUnknown:YES withFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__]
#define _prependingReplaceOnViewQueue(replaceId, ownId, block)      [self replaceIdOnViewQueue:replaceId withBlock:block havingId:ownId andAppendOnUnknown:NO withFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__]
typedef void (^view_queue_block_t)(PMKResolver _Nonnull);

@import QuartzCore.CATransaction;

@interface DZNEmptyDataSetView
@property (atomic, strong) UIView* contentView;
@property (atomic, strong) UIImageView* imageView;
@property (atomic, strong) UILabel* titleLabel;
@property (atomic, strong) UILabel* detailLabel;
@end

@interface UIScrollView () <UIGestureRecognizerDelegate>
@property (nonatomic, readonly) DZNEmptyDataSetView* emptyDataSetView;
@end

@interface ActiveChatsViewController() {
    int _startedOrientation;
    double _portraitTop;
    double _landscapeTop;
    BOOL _loginAlreadyAutodisplayed;
    NSMutableArray* _blockQueue;
    dispatch_semaphore_t _blockQueueSemaphore;
}
@property (atomic, strong) NSMutableArray* unpinnedContacts;
@property (atomic, strong) NSMutableArray* pinnedContacts;
@end

@implementation SizeClassWrapper
@end

@implementation ActiveChatsViewController

enum activeChatsControllerSections {
    pinnedChats,
    unpinnedChats,
    activeChatsViewControllerSectionCnt
};

typedef NS_ENUM(NSUInteger, MLViewID) {
    MLViewIDUnspecified,
    MLViewIDRegisterView,
    MLViewIDWelcomeLoginView,
};

static NSMutableSet* _mamWarningDisplayed;
static NSMutableSet* _smacksWarningDisplayed;
static NSMutableSet* _pushWarningDisplayed;

+(void) initialize
{
    DDLogDebug(@"initializing active chats class");
    _mamWarningDisplayed = [NSMutableSet new];
    _smacksWarningDisplayed = [NSMutableSet new];
    _pushWarningDisplayed = [NSMutableSet new];
}

-(instancetype)initWithNibName:(NSString*) nibNameOrNil bundle:(NSBundle*) nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    [self commonInit];
    return self;
}

-(instancetype) initWithStyle:(UITableViewStyle) style
{
    self = [super initWithStyle:style];
    [self commonInit];
    return self;
}

-(instancetype) initWithCoder:(NSCoder*) coder
{
    self = [super initWithCoder:coder];
    [self commonInit];
    return self;
}

-(void) commonInit
{
    _blockQueue = [NSMutableArray new];
    _blockQueueSemaphore = dispatch_semaphore_create(1);
}

-(void) resetViewQueue
{
    [_blockQueue removeAllObjects];
}

-(void) prependToViewQueue:(view_queue_block_t) block withId:(MLViewID) viewId andFile:(char*) file andLine:(int) line andFunc:(char*) func
{
    @synchronized(_blockQueue) {
        DDLogDebug(@"Prepending block with id %lu defined in %s at %@:%d to queue...", (unsigned long)viewId, func, [HelperTools sanitizeFilePath:file], line);
        [_blockQueue insertObject:@{@"id":@(viewId), @"block":^(PMKResolver resolve) {
            DDLogDebug(@"Calling block with id %lu defined in %s at %@:%d...", (unsigned long)viewId, func, [HelperTools sanitizeFilePath:file], line);
            block(resolve);
            DDLogDebug(@"Block with id %lu defined in %s at %@:%d finished...", (unsigned long)viewId, func, [HelperTools sanitizeFilePath:file], line);
        }} atIndex:0];
    }
    [self processViewQueue];
}

-(void) appendToViewQueue:(view_queue_block_t) block withId:(MLViewID) viewId andFile:(char*) file andLine:(int) line andFunc:(char*) func
{
    @synchronized(_blockQueue) {
        DDLogDebug(@"Appending block with id %lu defined in %s at %@:%d to queue...", (unsigned long)viewId, func, [HelperTools sanitizeFilePath:file], line);
        [_blockQueue addObject:@{@"id":@(viewId), @"block":^(PMKResolver resolve) {
            DDLogDebug(@"Calling block with id %lu defined in %s at %@:%d...", (unsigned long)viewId, func, [HelperTools sanitizeFilePath:file], line);
            block(resolve);
            DDLogDebug(@"Block with id %lu defined in %s at %@:%d finished...", (unsigned long)viewId, func, [HelperTools sanitizeFilePath:file], line);
        }}];
    }
    [self processViewQueue];
}

-(void) replaceIdOnViewQueue:(MLViewID) previousId withBlock:(view_queue_block_t) block havingId:(MLViewID) viewId andAppendOnUnknown:(BOOL) appendOnUnknown withFile:(char*) file andLine:(int) line andFunc:(char*) func
{
    @synchronized(_blockQueue) {
        DDLogDebug(@"Replacing block with id %lu with new block having id %lu defined in %s at %@:%d to queue...", (unsigned long)previousId, (unsigned long)viewId, func, [HelperTools sanitizeFilePath:file], line);
        
        //search for old block to replace and remove it
        NSInteger index = -1;
        for(NSDictionary* blockInfo in _blockQueue)
        {
            index++;
            if(((NSNumber*)blockInfo[@"id"]).unsignedIntegerValue == previousId)
            {
                DDLogDebug(@"Found blockInfo at index %d: %@", (int)index, blockInfo);
                [self->_blockQueue removeObjectAtIndex:index];
                break;
            }
        }
        if(index == -1)
        {
            if(appendOnUnknown)
            {
                DDLogDebug(@"Did not find block with id %lu on queue, appending block instead...", (unsigned long)previousId);
                [self appendToViewQueue:block withId:viewId andFile:file andLine:line andFunc:func];
            }
            else
            {
                DDLogDebug(@"Did not find block with id %lu on queue, prepending block instead...", (unsigned long)previousId);
                [self prependToViewQueue:block withId:viewId andFile:file andLine:line andFunc:func];
            }
            return;
        }
        
        //add replaement block at right position
        [_blockQueue insertObject:@{@"id":@(viewId), @"block":^(PMKResolver resolve) {
            DDLogDebug(@"Calling block with id %lu defined in %s at %@:%d...", (unsigned long)viewId, func, [HelperTools sanitizeFilePath:file], line);
            block(resolve);
            DDLogDebug(@"Block with id %lu defined in %s at %@:%d finished...", (unsigned long)viewId, func, [HelperTools sanitizeFilePath:file], line);
        }} atIndex:index];
    }
    [self processViewQueue];
}

-(void) processViewQueue
{
    //we are using uikit api all over the place: make sure we always run in the main queue
    [HelperTools dispatchAsync:YES reentrantOnQueue:dispatch_get_main_queue() withBlock:^{
        NSMutableArray* viewControllerHierarchy = [self getCurrentViewControllerHierarchy];
        
        //don't show the next entry if there is still the previous one
        //if(self.splitViewController.collapsed)
        if([viewControllerHierarchy count] > 0)
        {
            DDLogDebug(@"Ignoring call to processViewQueue, already showing: %@", viewControllerHierarchy);
            return;
        }
        
        //don't run the next block if the previous one did not yet complete
        if(dispatch_semaphore_wait(self->_blockQueueSemaphore, DISPATCH_TIME_NOW) != 0)
        {
            DDLogDebug(@"Ignoring call to processViewQueue, block still running, showing: %@", viewControllerHierarchy);
            return;
        }
        
        NSDictionary* blockInfo = nil;
        @synchronized(self->_blockQueue) {
            if(self->_blockQueue.count > 0)
            {
                blockInfo = [self->_blockQueue objectAtIndex:0];
                [self->_blockQueue removeObjectAtIndex:0];
            }
            else
                DDLogDebug(@"Queue is empty...");
        }
        if(blockInfo)
        {
            //DDLogDebug(@"Calling next block, stacktrace: %@", [NSThread callStackSymbols]);
            monal_void_block_t looper = ^{
                dispatch_semaphore_signal(self->_blockQueueSemaphore);
                DDLogDebug(@"Looping to next block...");
                [self processViewQueue];
            };
            [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
                ((view_queue_block_t)blockInfo[@"block"])(resolve);
            }].ensure(^{
                looper();
            });
        }
        else
        {
            DDLogDebug(@"Not calling next block: there is none...");
            dispatch_semaphore_signal(self->_blockQueueSemaphore);
        }
    }];
}

#pragma mark view lifecycle

-(void) configureComposeButton
{
    UIImage* image = [[UIImage systemImageNamed:@"person.2.fill"] imageWithTintColor:UIColor.tintColor];
    UITapGestureRecognizer* tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(showContacts:)];
    self.composeButton.customView = [HelperTools
        buttonWithNotificationBadgeForImage:image
        hasNotification:[[DataLayer sharedInstance] allContactRequests].count > 0
        withTapHandler:tapRecognizer];
    [self.composeButton setIsAccessibilityElement:YES];
    if([[DataLayer sharedInstance] allContactRequests].count > 0)
        [self.composeButton setAccessibilityLabel:NSLocalizedString(@"Open contact list (contact requests pending)", @"")];
    else
        [self.composeButton setAccessibilityLabel:NSLocalizedString(@"Open contact list", @"")];
    [self.composeButton setAccessibilityTraits:UIAccessibilityTraitButton];
}

-(void) viewDidLoad
{
    DDLogDebug(@"active chats view did load");
    [super viewDidLoad];
    
    _loginAlreadyAutodisplayed = NO;
    _startedOrientation = 0;
    
    self.spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium];
    self.spinner.hidesWhenStopped = YES;
    
    self.view.backgroundColor = [UIColor lightGrayColor];
    self.view.autoresizingMask = UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth;
    
    MonalAppDelegate* appDelegate = (MonalAppDelegate*)[[UIApplication sharedApplication] delegate];
    appDelegate.activeChats = self;
    
    self.chatListTable = [UITableView new];
    self.chatListTable.delegate = self;
    self.chatListTable.dataSource = self;
    
    self.view = self.chatListTable;
    
    self.sizeClass = [SizeClassWrapper new];
    [self updateSizeClass];

    NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
    [nc addObserver:self selector:@selector(handleRefreshDisplayNotification:) name:kMonalRefresh object:nil];
    [nc addObserver:self selector:@selector(handleContactRemoved:) name:kMonalContactRemoved object:nil];
    [nc addObserver:self selector:@selector(handleRefreshDisplayNotification:) name:kMonalMessageFiletransferUpdateNotice object:nil];
    [nc addObserver:self selector:@selector(refreshContact:) name:kMonalContactRefresh object:nil];
    [nc addObserver:self selector:@selector(handleNewMessage:) name:kMonalNewMessageNotice object:nil];
    [nc addObserver:self selector:@selector(handleNewMessage:) name:kMonalDeletedMessageNotice object:nil];
    [nc addObserver:self selector:@selector(messageSent:) name:kMLMessageSentToContact object:nil];
    [nc addObserver:self selector:@selector(handleDeviceRotation) name:UIDeviceOrientationDidChangeNotification object:nil];
    [nc addObserver:self selector:@selector(showWarningsIfNeeded) name:kMonalFinishedCatchup object:nil];
    
    [_chatListTable registerNib:[UINib nibWithNibName:@"MLContactCell" bundle:[NSBundle mainBundle]] forCellReuseIdentifier:@"ContactCell"];
    
    self.splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModeOneBesideSecondary;
#if !TARGET_OS_MACCATALYST
    self.splitViewController.primaryBackgroundStyle = UISplitViewControllerBackgroundStyleSidebar;
#endif
    self.settingsButton.image = [UIImage systemImageNamed:@"gearshape.fill"];
    [self configureComposeButton];

    self.spinnerButton.customView = self.spinner;
    
    self.chatListTable.emptyDataSetSource = self;
    self.chatListTable.emptyDataSetDelegate = self;
    
    //has to be done here to not always prepend intro screens onto our view queue
    //once a fullscreen view is dismissed (or the app is switched to foreground)
    [self segueToIntroScreensIfNeeded];
}

-(void) dealloc
{
    DDLogDebug(@"active chats dealloc");
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

-(void) handleDeviceRotation
{
    dispatch_async(dispatch_get_main_queue(), ^{
        [self imageForEmptyDataSet:nil];
        [self.chatListTable setNeedsDisplay];
    });
}

-(void) refreshDisplay
{
    size_t unpinnedConCntBefore = self.unpinnedContacts.count;
    size_t pinnedConCntBefore = self.pinnedContacts.count;
    NSMutableArray<MLContact*>* newUnpinnedContacts = [[DataLayer sharedInstance] activeContactsWithPinned:NO];
    NSMutableArray<MLContact*>* newPinnedContacts = [[DataLayer sharedInstance] activeContactsWithPinned:YES];
    if(!newUnpinnedContacts || ! newPinnedContacts)
        return;

    int unpinnedCntDiff = (int)unpinnedConCntBefore - (int)newUnpinnedContacts.count;
    int pinnedCntDiff = (int)pinnedConCntBefore - (int)newPinnedContacts.count;

    void (^resizeSections)(UITableView*, size_t, int) = ^void(UITableView* table, size_t section, int diff){
        if(diff > 0)
        {
            // remove rows
            for(int i = 0; i < diff; i++)
            {
                NSIndexPath* posInSection = [NSIndexPath indexPathForRow:i inSection:section];
                [table deleteRowsAtIndexPaths:@[posInSection] withRowAnimation:UITableViewRowAnimationNone];
            }
        }
        else if(diff < 0)
        {
            // add rows
            for(size_t i = (-1) * diff; i > 0; i--)
            {
                NSIndexPath* posInSectin = [NSIndexPath indexPathForRow:(i - 1) inSection:section];
                [table insertRowsAtIndexPaths:@[posInSectin] withRowAnimation:UITableViewRowAnimationNone];
            }
        }
    };

    dispatch_async(dispatch_get_main_queue(), ^{
        //make sure we don't display a chat view for a disabled account
        if([MLNotificationManager sharedInstance].currentContact != nil)
        {
            BOOL found = NO;
            for(NSDictionary* accountDict in [[DataLayer sharedInstance] enabledAccountList])
            {
                NSNumber* accountID = accountDict[kAccountID];
                if([MLNotificationManager sharedInstance].currentContact.accountID.intValue == accountID.intValue)
                    found = YES;
            }
            if(!found)
                [self presentChatWithContact:nil];
        }
        
        if(self.chatListTable.hasUncommittedUpdates)
            return;
        [CATransaction begin];
        [UIView performWithoutAnimation:^{
            [self.chatListTable beginUpdates];
            resizeSections(self.chatListTable, unpinnedChats, unpinnedCntDiff);
            resizeSections(self.chatListTable, pinnedChats, pinnedCntDiff);
            self.unpinnedContacts = newUnpinnedContacts;
            self.pinnedContacts = newPinnedContacts;
            [self.chatListTable reloadSections:[NSIndexSet indexSetWithIndex:pinnedChats] withRowAnimation:UITableViewRowAnimationNone];
            [self.chatListTable reloadSections:[NSIndexSet indexSetWithIndex:unpinnedChats] withRowAnimation:UITableViewRowAnimationNone];
            [self.chatListTable endUpdates];
        }];
        [CATransaction commit];
        [self.chatListTable reloadEmptyDataSet];

        MonalAppDelegate* appDelegate = (MonalAppDelegate*)[UIApplication sharedApplication].delegate;
        [appDelegate updateUnread];
    });
}

-(void) refreshContact:(NSNotification*) notification
{
    MLContact* contact = [notification.userInfo objectForKey:@"contact"];
    DDLogInfo(@"Refreshing contact %@ at %@: unread=%lu", contact.contactJid, contact.accountID, (unsigned long)contact.unreadCount);
    
    //update red dot
    dispatch_async(dispatch_get_main_queue(), ^{
        [self configureComposeButton];
    });
    
    // if pinning changed we have to move the user to a other section
    if([notification.userInfo objectForKey:@"pinningChanged"])
        [self insertOrMoveContact:contact completion:nil];
    else
    {
        dispatch_async(dispatch_get_main_queue(), ^{
            NSIndexPath* indexPath = nil;
            for(size_t section = pinnedChats; section < activeChatsViewControllerSectionCnt && !indexPath; section++)
            {
                NSMutableArray* curContactArray = [self getChatArrayForSection:section];
                // check if contact is already displayed -> get coresponding indexPath
                NSUInteger rowIdx = 0;
                for(MLContact* rowContact in curContactArray)
                {
                    if([rowContact isEqualToContact:contact])
                    {
                        //this MLContact instance is used in various ui parts, not just this file --> update all properties but keep the instance intact
                        [rowContact updateWithContact:contact];
                        indexPath = [NSIndexPath indexPathForRow:rowIdx inSection:section];
                        break;
                    }
                    rowIdx++;
                }
            }
            // reload contact entry if we found it
            if(indexPath)
            {
                DDLogDebug(@"Reloading row at %@", indexPath);
                [self.chatListTable reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
            }
            [self.chatListTable reloadEmptyDataSet];
        });
    }
}

-(void) handleRefreshDisplayNotification:(NSNotification*) notification
{
    // filter notifcations from within this class
    if([notification.object isKindOfClass:[ActiveChatsViewController class]])
        return;
    [self refresh];
}

-(void) handleContactRemoved:(NSNotification*) notification
{
    MLContact* removedContact = [notification.userInfo objectForKey:@"contact"];
    if(removedContact == nil)
        unreachable();
    
    dispatch_async(dispatch_get_main_queue(), ^{
        DDLogInfo(@"Contact removed, refreshing active chats...");
        
        //update red dot
        [self configureComposeButton];
        
        // remove contact from activechats table
        [self refreshDisplay];
        
        // open placeholder if the removed contact was "in foreground"
        if([removedContact isEqualToContact:[MLNotificationManager sharedInstance].currentContact])
        {
            DDLogInfo(@"Contact removed, closing chat view...");
            [self presentChatWithContact:nil];
        }
    });
}


-(void) messageSent:(NSNotification*) notification
{
    MLContact* contact = [notification.userInfo objectForKey:@"contact"];
    if(!contact)
        unreachable();
    [self insertOrMoveContact:contact completion:nil];
}

-(void) handleNewMessage:(NSNotification*) notification
{
    MLMessage* newMessage = notification.userInfo[@"message"];
    MLContact* contact = notification.userInfo[@"contact"];
    xmpp* msgAccount = (xmpp*)notification.object;
    if(!newMessage || !contact || !msgAccount)
    {
        unreachable();
        return;
    }
    if([newMessage.messageType isEqualToString:kMessageTypeStatus])
        return;

    // contact.statusMessage = newMessage;
    [self insertOrMoveContact:contact completion:nil];
}

-(void) insertOrMoveContact:(MLContact*) contact completion:(void (^ _Nullable)(BOOL finished)) completion
{
    dispatch_async(dispatch_get_main_queue(), ^{
        [self.chatListTable performBatchUpdates:^{
            __block NSIndexPath* indexPath = nil;
            for(size_t section = pinnedChats; section < activeChatsViewControllerSectionCnt && !indexPath; section++) {
                NSMutableArray* curContactArray = [self getChatArrayForSection:section];

                // check if contact is already displayed -> get coresponding indexPath
                [curContactArray enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
                    MLContact* rowContact = (MLContact *) obj;
                    if([rowContact isEqualToContact:contact])
                    {
                        indexPath = [NSIndexPath indexPathForRow:idx inSection:section];
                        *stop = YES;
                    }
                }];
            }

            size_t insertInSection = unpinnedChats;
            if(contact.isPinned) {
                insertInSection = pinnedChats;
            }
            NSMutableArray* insertContactToArray = [self getChatArrayForSection:insertInSection];
            NSIndexPath* insertAtPath = [NSIndexPath indexPathForRow:0 inSection:insertInSection];

            if(indexPath && insertAtPath.section == indexPath.section && insertAtPath.row == indexPath.row)
            {
                [insertContactToArray replaceObjectAtIndex:insertAtPath.row  withObject:contact];
                [self.chatListTable reloadRowsAtIndexPaths:@[insertAtPath] withRowAnimation:UITableViewRowAnimationNone];
                return;
            }
            else if(indexPath)
            {
                // Contact is already in our active chats list
                NSMutableArray* removeContactFromArray = [self getChatArrayForSection:indexPath.section];
                [self.chatListTable deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
                [removeContactFromArray removeObjectAtIndex:indexPath.row];
                [insertContactToArray insertObject:contact atIndex:0];
                [self.chatListTable insertRowsAtIndexPaths:@[insertAtPath] withRowAnimation:UITableViewRowAnimationNone];
            }
            else
            {
                // Chats does not exists in active Chats yet
                NSUInteger oldCount = [insertContactToArray count];
                [insertContactToArray insertObject:contact atIndex:0];
                [self.chatListTable insertRowsAtIndexPaths:@[insertAtPath] withRowAnimation:UITableViewRowAnimationRight];
                //make sure to fully refresh to remove the empty dataset (yes this will trigger on first chat pinning, too, but that does no harm)
                if(oldCount == 0)
                    [self refreshDisplay];
            }
        } completion:^(BOOL finished) {
            if(completion) completion(finished);
        }];
    });
}

-(void) viewWillAppear:(BOOL) animated
{
    DDLogDebug(@"active chats view will appear");
    [super viewWillAppear:animated];
    
    [self presentSplitPlaceholder];
}

-(void) viewWillDisappear:(BOOL) animated
{
    DDLogDebug(@"active chats view will disappear");
    [super viewWillDisappear:animated];
}

-(void) viewDidAppear:(BOOL) animated
{
    DDLogDebug(@"active chats view did appear");
    [super viewDidAppear:animated];
    
    [self refresh];
}

-(void) sheetDismissed
{
    [self refresh];
}

-(void) refresh
{
    dispatch_async(dispatch_get_main_queue(), ^{
        [self refreshDisplay];      // load contacts
        [self processViewQueue];
    });
}

-(void) didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
}

-(void) showAddContactWithJid:(NSString*) jid preauthToken:(NSString* _Nullable) preauthToken prefillAccount:(xmpp* _Nullable) account andOmemoFingerprints:(NSDictionary* _Nullable) fingerprints
{
    //check if contact is already known in any of our accounts and open a chat with the first contact we can find
    for(xmpp* checkAccount in [MLXMPPManager sharedInstance].connectedXMPP)
    {
        MLContact* checkContact = [MLContact createContactFromJid:jid andAccountID:checkAccount.accountID];
        if(checkContact.isInRoster)
        {
            [self presentChatWithContact:checkContact];
            return;
        }
    }
    
    appendToViewQueue((^(PMKResolver resolve) {
        UIViewController* addContactMenuView = [[SwiftuiInterface new] makeAddContactViewForJid:jid preauthToken:preauthToken prefillAccount:account andOmemoFingerprints:fingerprints withDismisser:^(MLContact* _Nonnull newContact) {
            [self presentChatWithContact:newContact];
        }];
        addContactMenuView.ml_disposeCallback = ^{
            [self sheetDismissed];
        };
        [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{
            [self presentViewController:addContactMenuView animated:NO completion:^{resolve(nil);}];
        }];
    }));
}

-(void) showAddContact
{
    appendToViewQueue((^(PMKResolver resolve) {
        UIViewController* addContactMenuView = [[SwiftuiInterface new] makeAddContactViewWithDismisser:^(MLContact* _Nonnull newContact) {
            [self presentChatWithContact:newContact];
        }];
        addContactMenuView.ml_disposeCallback = ^{
            [self sheetDismissed];
        };
        [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{
            [self presentViewController:addContactMenuView animated:NO completion:^{resolve(nil);}];
        }];
    }));
}

-(void) segueToIntroScreensIfNeeded
{
    DDLogDebug(@"segueToIntroScreensIfNeeded got called...");
    //prepend in a prepend block to make sure we have prepended everything in order before showing the first view
    //(if we would not do this, the first view prepended would be shown regardless of other views prepended after it)
    //every entry in here is flipped, because we want to prepend all intro screens to our queue
    prependToViewQueue((^(PMKResolver resolve) {
#ifdef IS_QUICKSY
        prependToViewQueue((^(PMKResolver resolve) {
            [self syncContacts];
            resolve(nil);
        }));
#else
        [self showWarningsIfNeeded];
#endif
        
        prependToViewQueue(MLViewIDWelcomeLoginView, (^(PMKResolver resolve) {
#ifdef IS_QUICKSY
            if([[[DataLayer sharedInstance] accountList] count] == 0)
            {
                DDLogDebug(@"Showing account registration view...");
                UIViewController* view = [[SwiftuiInterface new] makeAccountRegistration:@{}];
                if(UIDevice.currentDevice.userInterfaceIdiom != UIUserInterfaceIdiomPad)
                    view.modalPresentationStyle = UIModalPresentationFullScreen;
                else
                    view.ml_disposeCallback = ^{
                        [self sheetDismissed];
                    };
                [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{
                    [self presentViewController:view animated:NO completion:^{resolve(nil);}];
                }];
            }
            else
                resolve(nil);
#else
            // display quick start if the user never seen it or if there are 0 enabled accounts
            if([[DataLayer sharedInstance] enabledAccountCnts].intValue == 0 && !self->_loginAlreadyAutodisplayed)
            {
                DDLogDebug(@"Showing WelcomeLogIn view...");
                UIViewController* loginViewController = [[SwiftuiInterface new] makeViewWithName:@"WelcomeLogIn"];
                loginViewController.ml_disposeCallback = ^{
                    self->_loginAlreadyAutodisplayed = YES;
                    [self sheetDismissed];
                };
                [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{
                    [self presentViewController:loginViewController animated:YES completion:^{resolve(nil);}];
                }];
            }
            else
                resolve(nil);
#endif
        }));
    
        prependToViewQueue((^(PMKResolver resolve) {
            if(![[HelperTools defaultsDB] boolForKey:@"hasCompletedOnboarding"])
            {
                DDLogDebug(@"Showing onboarding view...");
                UIViewController* view = [[SwiftuiInterface new] makeViewWithName:@"OnboardingView"];
                if(UIDevice.currentDevice.userInterfaceIdiom != UIUserInterfaceIdiomPad)
                    view.modalPresentationStyle = UIModalPresentationFullScreen;
                else
                    view.ml_disposeCallback = ^{
                        [self sheetDismissed];
                    };
                [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{
                    [self presentViewController:view animated:NO completion:^{resolve(nil);}];
                }];
            }
            else
                resolve(nil);
        }));
        
        prependToViewQueue((^(PMKResolver resolve) {
            //open password migration if needed
            NSArray* needingMigration = [[DataLayer sharedInstance] accountListNeedingPasswordMigration];
            if(needingMigration.count > 0)
            {
#ifdef IS_QUICKSY
                DDLogDebug(@"Showing account registration view to do password migration...");
                UIViewController* view = [[SwiftuiInterface new] makeAccountRegistration:@{}];
                if(UIDevice.currentDevice.userInterfaceIdiom != UIUserInterfaceIdiomPad)
                    view.modalPresentationStyle = UIModalPresentationFullScreen;
                else
                    view.ml_disposeCallback = ^{
                        [self sheetDismissed];
                    };
                [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{
                    [self presentViewController:view animated:NO completion:^{resolve(nil);}];
                }];
#else
                DDLogDebug(@"Showing password migration view...");
                UIViewController* passwordMigration = [[SwiftuiInterface new] makePasswordMigration:needingMigration];
                passwordMigration.ml_disposeCallback = ^{
                    [self sheetDismissed];
                };
                [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{
                    [self presentViewController:passwordMigration animated:YES completion:^{resolve(nil);}];
                }];
#endif
            }
            else
                resolve(nil);
        }));
        
        resolve(nil);
    }));
}

#ifdef IS_QUICKSY
-(void) syncContacts
{
    CNContactStore* store = [[CNContactStore alloc] init];
    [store requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError* _Nullable error) {
        if(granted)
        {
            Quicksy_Country* country = [[HelperTools defaultsDB] objectForKey:@"Quicksy_country"];
            NSString* countryCode = country.code;
            NSCharacterSet* allowedCharacters = [[NSCharacterSet characterSetWithCharactersInString:@"+0123456789"] invertedSet];
            NSMutableDictionary* numbers = [NSMutableDictionary new];
            
            CNContactFetchRequest* request = [[CNContactFetchRequest alloc] initWithKeysToFetch:@[CNContactPhoneNumbersKey, CNContactNicknameKey, CNContactGivenNameKey, CNContactFamilyNameKey]];
            NSError* error;
            [store enumerateContactsWithFetchRequest:request error:&error usingBlock:^(CNContact* _Nonnull contact, BOOL* _Nonnull stop) {
                if(!error)
                {
                    NSString* name = [[NSString stringWithFormat:@"%@ %@", contact.givenName, contact.familyName] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
                    for(CNLabeledValue<CNPhoneNumber*>* phone in contact.phoneNumbers)
                    {
                        //add country code if missing
                        NSString* number = [[phone.value.stringValue componentsSeparatedByCharactersInSet:allowedCharacters] componentsJoinedByString:@""];
                        if(countryCode != nil && ![number hasPrefix:@"+"] && ![number hasPrefix:@"00"])
                        {
                            DDLogVerbose(@"Adding country code '%@' to number: %@", countryCode, number);
                            number = [NSString stringWithFormat:@"%@%@", countryCode, [number hasPrefix:@"0"] ? [number substringFromIndex:1] : number];
                        }
                        numbers[number] = name;
                    }
                }
                else
                    DDLogWarn(@"Error fetching contacts: %@", error);
            }];
            
            DDLogDebug(@"Got list of contact phone numbers: %@", numbers);
            
            NSArray<xmpp*>* enabledAccounts = [MLXMPPManager sharedInstance].connectedXMPP;
            if(enabledAccounts.count == 0)
            {
                DDLogError(@"No connected account while trying to send quicksy phonebook!");
                return;
            }
            else if(enabledAccounts.count > 1)
                DDLogWarn(@"More than 1 connected account while trying to send quicksy phonebook, using first one!");
            
            XMPPIQ* iqNode = [[XMPPIQ alloc] initWithType:kiqGetType to:@"api.quicksy.im"];
            [iqNode setQuicksyPhoneBook:numbers.allKeys];
            [enabledAccounts[0] sendIq:iqNode withHandler:$newHandler(MLIQProcessor, handleQuicksyPhoneBook, $ID(numbers))];
        }
        else
            DDLogError(@"Access to contacts not granted!");
    }];
}
#endif

-(void) showWarningsIfNeeded
{
    for(NSDictionary* accountDict in [[DataLayer sharedInstance] enabledAccountList])
    {
        NSNumber* accountID = accountDict[kAccountID];
        xmpp* account = [[MLXMPPManager sharedInstance] getEnabledAccountForID:accountID];
        if(!account)
            @throw [NSException exceptionWithName:@"RuntimeException" reason:@"Connected xmpp* object for accountID is nil!" userInfo:accountDict];
        
        prependToViewQueue((^(PMKResolver resolve) {
            if(![_mamWarningDisplayed containsObject:accountID] && account.accountState >= kStateBound && account.connectionProperties.accountDiscoDone)
            {
                if(![account.connectionProperties.accountDiscoFeatures containsObject:@"urn:xmpp:mam:2"])
                {
                    DDLogDebug(@"Showing MAM not supported warning...");
                    UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:NSLocalizedString(@"Account %@", @""), account.connectionProperties.identity.jid] message:NSLocalizedString(@"Your server does not support MAM (XEP-0313). That means you could frequently miss incoming messages!! You should switch your server or talk to the server admin to enable this!", @"") preferredStyle:UIAlertControllerStyleAlert];
                    [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) {
                        [_mamWarningDisplayed addObject:accountID];
                        resolve(nil);
                    }]];
                    [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{
                        [self presentViewController:messageAlert animated:YES completion:nil];
                    }];
                }
                else
                {
                    [_mamWarningDisplayed addObject:accountID];
                    resolve(nil);
                }
            }
            else
                resolve(nil);
        }));
        
        prependToViewQueue((^(PMKResolver resolve) {
            if(![_smacksWarningDisplayed containsObject:accountID] && account.accountState >= kStateBound)
            {
                if(!account.connectionProperties.supportsSM3)
                {
                    DDLogDebug(@"Showing smacks not supported warning...");
                    UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:NSLocalizedString(@"Account %@", @""), account.connectionProperties.identity.jid] message:NSLocalizedString(@"Your server does not support Stream Management (XEP-0198). That means your outgoing messages can get lost frequently!! You should switch your server or talk to the server admin to enable this!", @"") preferredStyle:UIAlertControllerStyleAlert];
                    [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) {
                        [_smacksWarningDisplayed addObject:accountID];
                        resolve(nil);
                    }]];
                    [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{
                        [self presentViewController:messageAlert animated:YES completion:nil];
                    }];
                }
                else
                {
                    [_smacksWarningDisplayed addObject:accountID];
                    resolve(nil);
                }
            }
            else
                resolve(nil);
        }));
        
        prependToViewQueue((^(PMKResolver resolve) {
            if(![_pushWarningDisplayed containsObject:accountID] && account.accountState >= kStateBound && account.connectionProperties.accountDiscoDone)
            {
                if(![account.connectionProperties.accountDiscoFeatures containsObject:@"urn:xmpp:push:0"])
                {
                    DDLogDebug(@"Showing push not supported warning...");
                    UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:NSLocalizedString(@"Account %@", @""), account.connectionProperties.identity.jid] message:NSLocalizedString(@"Your server does not support PUSH (XEP-0357). That means you have to manually open the app to retrieve new incoming messages!! You should switch your server or talk to the server admin to enable this!", @"") preferredStyle:UIAlertControllerStyleAlert];
                    [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) {
                        [_pushWarningDisplayed addObject:accountID];
                        resolve(nil);
                    }]];
                    [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{
                        [self presentViewController:messageAlert animated:YES completion:nil];
                    }];
                }
                else
                {
                    [_pushWarningDisplayed addObject:accountID];
                    resolve(nil);
                }
            }
            else
                resolve(nil);
        }));
    }
}

-(void) presentSplitPlaceholder
{
    // only show placeholder if we use a split view
    if(!self.splitViewController.collapsed)
    {
        DDLogVerbose(@"Presenting Chat Placeholder...");
        UIViewController* detailsViewController = [[SwiftuiInterface new] makeViewWithName:@"ChatPlaceholder"];
        [self showDetailViewController:detailsViewController sender:self];
    }
    [MLNotificationManager sharedInstance].currentContact = nil;
}

-(void) showNotificationSettings
{
    [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{
        UIViewController* view = [[SwiftuiInterface new] makeViewWithName:@"ActiveChatsNotificationSettings"];
        view.ml_disposeCallback = ^{
            [self sheetDismissed];
        };
        [self presentViewController:view animated:YES completion:nil];
    }];
}

-(void) prependGeneralSettings
{
    prependToViewQueue((^(PMKResolver resolve) {
        UIViewController* view = [[SwiftuiInterface new] makeViewWithName:@"ActiveChatsGeneralSettings"];
        view.ml_disposeCallback = ^{
            [self sheetDismissed];
        };
        [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{
            [self presentViewController:view animated:YES completion:^{resolve(nil);}];
        }];
    }));
}

-(void) showGeneralSettings
{
    appendToViewQueue((^(PMKResolver resolve) {
        UIViewController* view = [[SwiftuiInterface new] makeViewWithName:@"ActiveChatsGeneralSettings"];
        view.ml_disposeCallback = ^{
            [self sheetDismissed];
        };
        [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{
            [self presentViewController:view animated:YES completion:^{resolve(nil);}];
        }];
    }));
}

-(void) showSettings
{
    appendToViewQueue((^(PMKResolver resolve) {
        [self performSegueWithIdentifier:@"showSettings" sender:self];
        resolve(nil);
    }));
}

-(void) showCallContactNotFoundAlert:(NSString*) jid
{
    UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Contact not found", @"") message:[NSString stringWithFormat:NSLocalizedString(@"You tried to call contact '%@' but this contact could not be found in your contact list.", @""), jid] preferredStyle:UIAlertControllerStyleAlert];
    [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) {}]];
    [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{
        [self presentViewController:messageAlert animated:NO completion:nil];
    }];
}

-(void) callContact:(MLContact*) contact withCallType:(MLCallType) callType
{
    MonalAppDelegate* appDelegate = (MonalAppDelegate *)[[UIApplication sharedApplication] delegate];
    MLCall* activeCall = [appDelegate.voipProcessor getActiveCallWithContact:contact];
    if(activeCall != nil)
        [self presentCall:activeCall];
    else
        [self presentCall:[appDelegate.voipProcessor initiateCallWithType:callType toContact:contact]];
}

-(void) callContact:(MLContact*) contact withUIKitSender:(_Nullable id) sender
{
    MonalAppDelegate* appDelegate = (MonalAppDelegate *)[[UIApplication sharedApplication] delegate];
    MLCall* activeCall = [appDelegate.voipProcessor getActiveCallWithContact:contact];
    if(activeCall != nil)
        [self presentCall:activeCall];
    else
    {
        UIAlertController* alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Call Type", @"") message:NSLocalizedString(@"What call do you want to place?", @"") preferredStyle:UIAlertControllerStyleActionSheet];
        [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"🎵 Audio", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
            [self dismissViewControllerAnimated:YES completion:nil];
            [self presentCall:[appDelegate.voipProcessor initiateCallWithType:MLCallTypeAudio toContact:contact]];
        }]];
        [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"🎥 Video", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
            [self dismissViewControllerAnimated:YES completion:nil];
            [self presentCall:[appDelegate.voipProcessor initiateCallWithType:MLCallTypeVideo toContact:contact]];
        }]];
        [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
            [self dismissViewControllerAnimated:YES completion:nil];
        }]];
        UIPopoverPresentationController* popPresenter = [alert popoverPresentationController];
        if(sender != nil)
            popPresenter.sourceItem = sender;
        else
            popPresenter.sourceView = self.view;
        [self presentViewController:alert animated:YES completion:nil];
    }
}

-(void) presentAccountPickerForContacts:(NSArray<MLContact*>*) contacts andCallType:(MLCallType) callType
{
    [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{
        UIViewController* accountPickerController = [[SwiftuiInterface new] makeAccountPickerForContacts:contacts andCallType:callType];
        accountPickerController.ml_disposeCallback = ^{
            [self sheetDismissed];
        };
        [self presentViewController:accountPickerController animated:YES completion:^{}];
    }];
}

-(void) presentCall:(MLCall*) call
{
    [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{
        UIViewController* callViewController = [[SwiftuiInterface new] makeCallScreenForCall:call];
        callViewController.modalPresentationStyle = UIModalPresentationFullScreen;
        [self presentViewController:callViewController animated:NO completion:^{}];
    }];
}

-(void) presentChatWithContact:(MLContact*) contact
{
    return [self presentChatWithContact:contact andCompletion:nil];
}

-(void) presentChatWithContact:(MLContact*) contact andCompletion:(monal_id_block_t _Nullable) completion
{
    DDLogVerbose(@"presenting chat with contact: %@, stacktrace: %@", contact, [NSThread callStackSymbols]);
    [HelperTools dispatchAsync:YES reentrantOnQueue:dispatch_get_main_queue() withBlock:^{
        [self dismissCompleteViewChainWithAnimation:YES andCompletion:^{
            // only open contact chat when it is not opened yet (needed for opening via notifications and for macOS)
            if([contact isEqualToContact:[MLNotificationManager sharedInstance].currentContact])
            {
                // make sure the already open chat is reloaded and return
                [[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil];
                if(completion != nil)
                    completion(@YES);
                return;
            }
            
            // clear old chat before opening a new one (but not for splitView == YES)
            if(self.splitViewController.collapsed)
                [self.navigationController popViewControllerAnimated:NO];
            
            // show placeholder if contact is nil, open chat otherwise
            if(contact == nil)
            {
                [self presentSplitPlaceholder];
                if(completion != nil)
                    completion(@NO);
                return;
            }

            //open chat (make sure we have an active buddy for it and add it to our ui, if needed)
            //but don't animate this if the contact is already present in our list
            [[DataLayer sharedInstance] addActiveBuddies:contact.contactJid forAccount:contact.accountID];
            if([[self getChatArrayForSection:pinnedChats] containsObject:contact] || [[self getChatArrayForSection:unpinnedChats] containsObject:contact])
            {
                [self scrollToContact:contact];
                [self performSegueWithIdentifier:@"showConversation" sender:contact];
                if(completion != nil)
                    completion(@YES);
            }
            else
            {
                [self insertOrMoveContact:contact completion:^(BOOL finished __unused) {
                    [self scrollToContact:contact];
                    [self performSegueWithIdentifier:@"showConversation" sender:contact];
                    if(completion != nil)
                        completion(@YES);
                }];
            }
        }];
    }];
}

/*
 * return YES if no enabled account was found && a alert will open
 */
-(BOOL) showAccountNumberWarningIfNeeded
{
    // Only open contacts list / roster if at least one account is enabled
    if([[DataLayer sharedInstance] enabledAccountCnts].intValue == 0) {
        // Show warning
        UIAlertController* alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"No enabled account found", @"") message:NSLocalizedString(@"Please add a new account under settings first. If you already added your account you may need to enable it under settings", @"") preferredStyle:UIAlertControllerStyleAlert];
        [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction* action __unused) {
            [alert dismissViewControllerAnimated:YES completion:nil];
        }]];
        [self presentViewController:alert animated:YES completion:nil];
        return YES;
    }
    return NO;
}

-(BOOL) shouldPerformSegueWithIdentifier:(NSString*) identifier sender:(id) sender
{
    return YES;
}

//this is needed to prevent segues invoked programmatically
-(void) performSegueWithIdentifier:(NSString*) identifier sender:(id) sender
{
    [super performSegueWithIdentifier:identifier sender:sender];
}

-(void) prepareForSegue:(UIStoryboardSegue*) segue sender:(id) sender
{
    DDLogInfo(@"Got segue identifier '%@'", segue.identifier);
    if([segue.identifier isEqualToString:@"showConversation"])
    {
        UINavigationController* nav = segue.destinationViewController;
        chatViewController* chatVC = (chatViewController*)nav.topViewController;
        UIBarButtonItem* barButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil];
        self.navigationItem.backBarButtonItem = barButtonItem;
        [chatVC setupWithContact:sender];
    }
    else if([segue.identifier isEqualToString:@"showDetails"])
    {
        UIViewController* detailsViewController = [[SwiftuiInterface new] makeContactDetails:sender];
        detailsViewController.ml_disposeCallback = ^{
            [self sheetDismissed];
        };
        [self presentViewController:detailsViewController animated:YES completion:^{}];
    }
}

-(void) updateSizeClass {
    self.sizeClass.horizontal = self.view.traitCollection.horizontalSizeClass;
}

-(NSMutableArray*) getChatArrayForSection:(size_t) section
{
    NSMutableArray* chatArray = nil;
    if(section == pinnedChats) {
        chatArray = self.pinnedContacts;
    } else if(section == unpinnedChats) {
        chatArray = self.unpinnedContacts;
    }
    return chatArray;
}
#pragma mark - tableview datasource

-(NSInteger) numberOfSectionsInTableView:(UITableView *)tableView
{
    return activeChatsViewControllerSectionCnt;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    if(section == pinnedChats) {
        return [self.pinnedContacts count];
    } else if(section == unpinnedChats) {
        return [self.unpinnedContacts count];
    } else {
        return 0;
    }
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    MLContactCell* cell = (MLContactCell*)[tableView dequeueReusableCellWithIdentifier:@"ContactCell" forIndexPath:indexPath];

    MLContact* chatContact = nil;
    // Select correct contact array
    if(indexPath.section == pinnedChats)
        chatContact = [self.pinnedContacts objectAtIndex:indexPath.row];
    else
        chatContact = [self.unpinnedContacts objectAtIndex:indexPath.row];
    
    // Display msg draft or last msg
    MLMessage* messageRow = [[DataLayer sharedInstance] lastMessageForContact:chatContact.contactJid forAccount:chatContact.accountID];

    [cell initCell:chatContact withLastMessage:messageRow];

    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    
    // Highlight the selected chat
    if([MLNotificationManager sharedInstance].currentContact != nil && [chatContact isEqual:[MLNotificationManager sharedInstance].currentContact])
    {
        cell.backgroundColor = [UIColor lightGrayColor];
        cell.statusText.textColor = [UIColor whiteColor];
    }
    else
    {
        cell.backgroundColor = [UIColor clearColor];
        cell.statusText.textColor = [UIColor lightGrayColor];
    }

    return cell;
}


#pragma mark - tableview delegate

-(CGFloat) tableView:(UITableView*) tableView heightForRowAtIndexPath:(NSIndexPath*) indexPath
{
    return 60.0f;
}

-(NSString*) tableView:(UITableView*) tableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath*) indexPath
{
    return NSLocalizedString(@"Archive chat", @"");
}

-(BOOL) tableView:(UITableView*) tableView canEditRowAtIndexPath:(NSIndexPath*) indexPath
{
    return YES;
}

-(BOOL)tableView:(UITableView*) tableView shouldIndentWhileEditingRowAtIndexPath:(NSIndexPath*) indexPath
{
    return YES;
}

-(void)tableView:(UITableView*) tableView commitEditingStyle:(UITableViewCellEditingStyle) editingStyle forRowAtIndexPath:(NSIndexPath*) indexPath
{
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        MLContact* contact = nil;
        // Delete contact from view
        if(indexPath.section == pinnedChats) {
            contact = [self.pinnedContacts objectAtIndex:indexPath.row];
            [self.pinnedContacts removeObjectAtIndex:indexPath.row];
        } else {
            contact = [self.unpinnedContacts objectAtIndex:indexPath.row];
            [self.unpinnedContacts removeObjectAtIndex:indexPath.row];
        }
        [self.chatListTable deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
        // removeActiveBuddy in db
        [[DataLayer sharedInstance] removeActiveBuddy:contact.contactJid forAccount:contact.accountID];
        // remove contact from activechats table
        [self refreshDisplay];
        // open placeholder
        [self presentChatWithContact:nil];
    }
}

-(void) tableView:(UITableView*) tableView didSelectRowAtIndexPath:(NSIndexPath*) indexPath
{
    MLContact* selected = nil;
    if(indexPath.section == pinnedChats) {
        selected = self.pinnedContacts[indexPath.row];
    } else {
        selected = self.unpinnedContacts[indexPath.row];
    }
    [self presentChatWithContact:selected];
}

-(void) tableView:(UITableView*) tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath*) indexPath
{
    MLContact* selected = nil;
    if(indexPath.section == pinnedChats) {
        selected = self.pinnedContacts[indexPath.row];
    } else {
        selected = self.unpinnedContacts[indexPath.row];
    }
    UIViewController* detailsViewController = [[SwiftuiInterface new] makeContactDetails:selected];
    detailsViewController.ml_disposeCallback = ^{
        [self sheetDismissed];
    };
    [self presentViewController:detailsViewController animated:YES completion:^{}];
}


#pragma mark - empty data set

-(void) viewWillTransitionToSize:(CGSize) size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>) coordinator
{
    //DDLogError(@"Transitioning to size: %@", NSStringFromCGSize(size));
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
}


-(UIImage*) imageForEmptyDataSet:(UIScrollView*) scrollView
{
    int orientation;
    if(self.tableView.frame.size.height > self.tableView.frame.size.width)
    {
        orientation = 1;        //portrait
        _portraitTop = self.navigationController.navigationBar.frame.size.height;
    }
    else
    {
        orientation = 2;        //landscape
        _landscapeTop = self.navigationController.navigationBar.frame.size.height;
    }
    if(_startedOrientation == 0)
        _startedOrientation = orientation;

    //DDLogError(@"started orientation: %@", _startedOrientation == 1 ? @"portrait" : @"landscape");
    //DDLogError(@"current orientation: %@", orientation == 1 ? @"portrait" : @"landscape");
    
    DZNEmptyDataSetView* emptyDataSetView = self.tableView.emptyDataSetView;
    CGRect headerFrame = self.navigationController.navigationBar.frame;
    CGRect tableFrame = self.tableView.frame;
    //CGRect contentFrame = emptyDataSetView.contentView.frame;
    //DDLogError(@"headerFrame: %@", NSStringFromCGRect(headerFrame));
    //DDLogError(@"tableFrame: %@", NSStringFromCGRect(tableFrame));
    //DDLogError(@"contentFrame: %@", NSStringFromCGRect(contentFrame));
    tableFrame.size.height *= 0.5;
    
    //started in landscape, moved to portrait
    if(_startedOrientation == 2 && orientation == 1)
    {
        tableFrame.origin.y += headerFrame.size.height - _landscapeTop - _portraitTop;
    }
    //started in portrait, moved to landscape
    else if(_startedOrientation == 1 && orientation == 2)
    {
        tableFrame.origin.y += (_portraitTop + _landscapeTop * 2);
        tableFrame.size.height -= _portraitTop;
    }
    //started in any orientation, moved to same orientation (or just started)
    else
    {
        tableFrame.origin.y += headerFrame.size.height;
    }
    
    emptyDataSetView.contentView.frame = tableFrame;
    emptyDataSetView.imageView.frame = tableFrame;
    [emptyDataSetView.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[imageView]-(32@750)-[titleLabel]-(16@750)-[detailLabel]|" options:0 metrics:nil views:@{
        @"imageView": emptyDataSetView.imageView,
        @"titleLabel": emptyDataSetView.titleLabel,
        @"detailLabel": emptyDataSetView.detailLabel,
    }]];
    emptyDataSetView.imageView.translatesAutoresizingMaskIntoConstraints = YES;
    if(self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark)
        return [UIImage imageNamed:@"chat_dark"];
    return [UIImage imageNamed:@"chat"];
    
    /*
    DZNEmptyDataSetView* emptyDataSetView = self.chatListTable.emptyDataSetView;
    CGRect headerFrame = self.navigationController.navigationBar.frame;
    CGRect tableFrame = self.chatListTable.frame;
    CGRect contentFrame = emptyDataSetView.contentView.frame;
    DDLogError(@"headerFrame: %@", NSStringFromCGRect(headerFrame));
    DDLogError(@"tableFrame: %@", NSStringFromCGRect(tableFrame));
    DDLogError(@"contentFrame: %@", NSStringFromCGRect(contentFrame));
    if(tableFrame.size.height > tableFrame.size.width)
    {
        DDLogError(@"height is bigger");
        tableFrame.size.height *= 0.5;
        tableFrame.origin.y += headerFrame.size.height;
    }
    else
    {
        DDLogError(@"width is bigger");
        tableFrame.size.height *= 2.0;
    }
    //tableFrame.size.height *= (tableFrame.size.width / tableFrame.size.height);
    emptyDataSetView.imageView.frame = tableFrame;
    emptyDataSetView.contentView.frame = tableFrame;
    [emptyDataSetView.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[imageView]-(48@750)-[titleLabel]-(16@750)-[detailLabel]|" options:0 metrics:nil views:@{
        @"imageView": emptyDataSetView.imageView,
        @"titleLabel": emptyDataSetView.titleLabel,
        @"detailLabel": emptyDataSetView.detailLabel,
    }]];
    emptyDataSetView.imageView.translatesAutoresizingMaskIntoConstraints = YES;
    return [UIImage imageNamed:@"chat"];
    */
}

-(CGFloat) spaceHeightForEmptyDataSet:(UIScrollView*) scrollView
{
    return 480.0f;
}

-(NSAttributedString*) titleForEmptyDataSet:(UIScrollView*) scrollView
{
    NSString* text = NSLocalizedString(@"No active conversations", @"");
    
    NSDictionary* attributes = @{NSFontAttributeName: [UIFont boldSystemFontOfSize:18.0f],
                                 NSForegroundColorAttributeName: (self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark ? [UIColor whiteColor] : [UIColor blackColor])};
    
    return [[NSAttributedString alloc] initWithString:text attributes:attributes];
}

- (NSAttributedString*)descriptionForEmptyDataSet:(UIScrollView*) scrollView
{
    NSString* text = NSLocalizedString(@"When you start a conversation\nwith someone, they will\nshow up here.", @"");
    
    NSMutableParagraphStyle* paragraph = [NSMutableParagraphStyle new];
    paragraph.lineBreakMode = NSLineBreakByWordWrapping;
    paragraph.alignment = NSTextAlignmentCenter;
    
    NSDictionary* attributes = @{NSFontAttributeName: [UIFont systemFontOfSize:14.0f],
                                 NSForegroundColorAttributeName: (self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark ? [UIColor whiteColor] : [UIColor blackColor]),
                                 NSParagraphStyleAttributeName: paragraph};
    
    return [[NSAttributedString alloc] initWithString:text attributes:attributes];
}

-(UIColor*) backgroundColorForEmptyDataSet:(UIScrollView*) scrollView
{
    return [UIColor colorNamed:@"chats"];
}

-(BOOL) emptyDataSetShouldDisplay:(UIScrollView*) scrollView
{
    BOOL toreturn = (self.unpinnedContacts.count == 0 && self.pinnedContacts.count == 0) ? YES : NO;
    if(toreturn)
    {
        // A little trick for removing the cell separators
        self.tableView.tableFooterView = [UIView new];
    }
    return toreturn;
}

#pragma mark - mac menu

-(void) showContacts:(id) sender { // function definition for @selector
    [self showContacts];
}

-(void) showContacts
{
    if([self showAccountNumberWarningIfNeeded]) {
        return;
    }

    appendToViewQueue((^(PMKResolver resolve) {
        contactCompletion callback = ^(MLContact* selectedContact) {
            DDLogVerbose(@"Got selected contact from contactlist ui: %@", selectedContact);
            [self presentChatWithContact:selectedContact];
        };

        UIViewController* contactsView = [[SwiftuiInterface new] makeContactsViewWithDismisser: callback onButton: self.composeButton];
        [self presentViewController:contactsView animated:YES completion:^{resolve(nil);}];
    }));
}

//we can not call this var "completion" because then some dumb comiler check kicks in and tells us "completion handler is never called"
//which is plainly wrong. "callback" on the other hand doesn't seem to be a word in the objc compiler's "bad words" dictionary,
//so this makes it compile again
-(void) showRegisterWithUsername:(NSString*) username onHost:(NSString*) host withToken:(NSString*) token usingCompletion:(monal_id_block_t) callback
{
    prependingReplaceOnViewQueue(MLViewIDWelcomeLoginView, MLViewIDRegisterView, (^(PMKResolver resolve) {
        UIViewController* registerViewController = [[SwiftuiInterface new] makeAccountRegistration:@{
            @"host": nilWrapper(host),
            @"username": nilWrapper(username),
            @"token": nilWrapper(token),
            @"completion": nilDefault(callback, (^(id accountID) {
                DDLogWarn(@"Dummy reg completion called for accountID: %@", accountID);
            })),
        }];
        registerViewController.ml_disposeCallback = ^{
            [self sheetDismissed];
        };
        [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{
            [self presentViewController:registerViewController animated:YES completion:^{resolve(nil);}];
        }];
    }));
}

-(void) showDetails
{
    appendToViewQueue((^(PMKResolver resolve) {
        if([MLNotificationManager sharedInstance].currentContact != nil)
        {
            UIViewController* detailsViewController = [[SwiftuiInterface new] makeContactDetails:[MLNotificationManager sharedInstance].currentContact];
            detailsViewController.ml_disposeCallback = ^{
                [self sheetDismissed];
            };
            [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{
                [self presentViewController:detailsViewController animated:YES completion:^{resolve(nil);}];
            }];
        }
        else
            resolve(nil);
    }));
}

-(void) deleteConversation
{
    for(size_t section = pinnedChats; section < activeChatsViewControllerSectionCnt; section++)
    {
        NSMutableArray* curContactArray = [self getChatArrayForSection:section];
        // check if contact is already displayed -> get coresponding indexPath
        [curContactArray enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop __unused) {
            MLContact* rowContact = (MLContact*)obj;
            if([rowContact isEqualToContact:[MLNotificationManager sharedInstance].currentContact])
            {
                [self tableView:self.chatListTable commitEditingStyle:UITableViewCellEditingStyleDelete forRowAtIndexPath:[NSIndexPath indexPathForRow:idx inSection:section]];
                // remove contact from activechats table
                [self refreshDisplay];
                // open placeholder
                [self presentChatWithContact:nil];
                return;
            }
        }];
    }
}

-(NSMutableArray*) getCurrentViewControllerHierarchy
{
    MonalAppDelegate* appDelegate = (MonalAppDelegate *)[[UIApplication sharedApplication] delegate];
    UIViewController* rootViewController = appDelegate.window.rootViewController;
    NSMutableArray* viewControllers = [NSMutableArray new];
    while(rootViewController.presentedViewController)
    {
        [viewControllers addObject:rootViewController.presentedViewController];
        rootViewController = rootViewController.presentedViewController;
    }
    return [[[viewControllers reverseObjectEnumerator] allObjects] mutableCopy];
}

-(void) dismissCompleteViewChainWithAnimation:(BOOL) animation andCompletion:(monal_void_block_t _Nullable) completion
{
    NSMutableArray* viewControllers = [self getCurrentViewControllerHierarchy];
    DDLogVerbose(@"Dismissing view controller hierarchy: %@", viewControllers);
    [self dismissRecursorWithViewControllers:viewControllers animation:animation andCompletion:completion];
}

-(void) dismissRecursorWithViewControllers:(NSMutableArray*) viewControllers animation:(BOOL) animation andCompletion:(monal_void_block_t _Nullable) completion
{
    if([viewControllers count] > 0)
    {
        UIViewController* viewController = viewControllers[0];
        [viewControllers removeObjectAtIndex:0];
        DDLogVerbose(@"Dismissing: %@", viewController);
        [viewController dismissViewControllerAnimated:animation completion:^{
            [self dismissRecursorWithViewControllers:viewControllers animation:animation andCompletion:completion];
        }];
    }
    else
    {
        DDLogVerbose(@"View chain completely dismissed...");
        completion();
    }
}

-(chatViewController* _Nullable) currentChatView
{
    NSArray* controllers = ((UINavigationController*)self.splitViewController.viewControllers[0]).viewControllers;
    chatViewController* chatView = nil;
    if(controllers.count > 1)
        chatView = [((UINavigationController*)controllers[1]).viewControllers firstObject];
    if(![chatView isKindOfClass:NSClassFromString(@"chatViewController")])
        chatView = nil;
    return chatView;
}

-(void) scrollToContact:(MLContact*) contact
{
    __block NSIndexPath* indexPath = nil;
    for(size_t section = pinnedChats; section < activeChatsViewControllerSectionCnt && !indexPath; section++) {
        NSMutableArray* curContactArray = [self getChatArrayForSection:section];

        // get indexPath
        [curContactArray enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            MLContact* rowContact = (MLContact*)obj;
            if([rowContact isEqualToContact:contact])
            {
                indexPath = [NSIndexPath indexPathForRow:idx inSection:section];
                *stop = YES;
            }
        }];
    }
    [self.chatListTable selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
}

@end