//
//  GMSChatViewController.m
//  GMS Sample
//
/*
 Version: 1.0
 
 Disclaimer: IMPORTANT:  This software is supplied to you by Genesys
 Telecommunications Laboratories Inc ("Genesys") in consideration of your agreement
 to the following terms, and your use, installation, modification or redistribution
 of this Genesys software constitutes acceptance of these terms.  If you do not
 agree with these terms, please do not use, install, modify or redistribute this
 Genesys software.
 
 In consideration of your agreement to abide by the following terms, and subject
 to these terms, Genesys grants you a personal, non-exclusive license, under
 Genesys's copyrights in this original Genesys software (the "Genesys Software"), to
 use, reproduce, modify and redistribute the Genesys Software, with or without
 modifications, in source and/or binary forms; provided that if you redistribute
 the Genesys Software in its entirety and without modifications, you must retain
 this notice and the following text and disclaimers in all such redistributions
 of the Genesys Software.
 
 Neither the name, trademarks, service marks or logos of Genesys Inc. may be used
 to endorse or promote products derived from the Genesys Software without specific
 prior written permission from Genesys.  Except as expressly stated in this notice,
 no other rights or licenses, express or implied, are granted by Genesys herein,
 including but not limited to any patent rights that may be infringed by your
 derivative works or by other works in which the Genesys Software may be
 incorporated.
 
 The Genesys Software is provided by Genesys on an "AS IS" basis.  GENESYS MAKES NO
 WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED
 WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 PURPOSE, REGARDING THE GENESYS SOFTWARE OR ITS USE AND OPERATION ALONE OR IN
 COMBINATION WITH YOUR PRODUCTS.
 
 IN NO EVENT SHALL GENESYS BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR
 CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
 GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR
 DISTRIBUTION OF THE GENESYS SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF
 CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF
 GENESYS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 Copyright (C) 2013 Genesys Inc. All Rights Reserved.
 */

#import "GMSChatViewController.h"
#import "DDCometMessage.h"
#import "UIBubbleTableView.h"
#import "GMSLogViewController.h"
#import "GMSRequestViewController.h"
#import "GMSAppDelegate.h"
#import "GMSUtil.h"


#define refreshURL      @"/refresh"
#define startTypingURL  @"/startTyping"
#define stopTypingURL   @"/stopTyping"
#define disconnectURL   @"/disconnect"

@interface GMSChatViewController ()
{
    BOOL _chatActive;
    GMSAppDelegate *_appDelegate;
}

@end

@implementation GMSChatViewController
// instance variables declared in implementation context
{
    NSString *startSessionURL;
    NSString *cometURL;
    NSString *userHeader;
    NSDictionary *chatParams;
    
    NSMutableArray *localBubbleData;            //displayed in UIBubbleTableView, synchronized with remoteBubbleData
    NSMutableArray *remoteBubbleData;           //received by CometD
    DDCometClient *cometClient;
    NSString *cometClientID;
    NSString *gmsSession;
    NSString *agentName;
    NSString *transcriptPosition;
    NSString *pushSubscriptionId;
    NSString *pushNotificationMaxSize;
    BOOL *_delayingBeforeCometHandshake;
}

- (id)initWithCoder:(NSCoder *)decoder
{
    _appDelegate = (GMSAppDelegate*)[[UIApplication sharedApplication] delegate];
    _delayingBeforeCometHandshake = NO;
    if ([[[UIDevice currentDevice] systemVersion] floatValue] < 8.0)
        pushNotificationMaxSize = @"256";
    else
        pushNotificationMaxSize = @"4096";

    return [super initWithCoder:decoder];
}

- (void)startWithURL:(NSString *)startURL cometURL:(NSString *)cURL userHeader:(NSString *)uHeader chatParams:(NSDictionary *)cParams
{    
    startSessionURL = startURL;
    cometURL = cURL;
    userHeader = uHeader;
    chatParams = cParams;
    
    if (_appDelegate.clearChatOnNewSession)
        [self clearChat:self];
    
    // Provide the appdelegate with a link so it can call applicationDidEnterBackground
    _appDelegate.chatViewController = self;
    
    // We support two ways for enabling push notificatios.
    // Uncomment this block to enable calling the notification API to create a push subscription
    //if (![_appDelegate.notifyToken isEqualToString:@"Error"]) {
         //[self subcribeForPushNotifications];
    //}
    // Uncomment this line to enable sending device details in the start chat request
    [self initCometd];
}

-(void) subcribeForPushNotifications
{
    NSString *id = [[NSUUID UUID] UUIDString];
    
    NSDictionary *params = @{
         @"subscriberId": id,
         @"notificationDetails": @{
             @"deviceId": _appDelegate.notifyToken,
             @"type": @"ios",
             @"properties":  @{
                 @"debug": _appDelegate.serverAPNDebug ? @"true": @"false",
                 @"maxsize": pushNotificationMaxSize
             },
         },
         @"expire": @3600,
         @"filter": @"chat.newagentmessage"
    };
    
    GMSChatViewController *myself = self;
    
    [GMSUtil submitRequestWithPath:@"/1/notification/subscription" method:@"POSTJSON" params:params headers:nil
                   completionBlock:^(NSDictionary *responseDict) {
                       myself->pushSubscriptionId = [responseDict objectForKey:@"id"];
                       [myself initCometd];
                   }
    ];
}

- (void)initCometd
{
    // Start cometd by calling handshake on the client.
    // From this point on cometd works through callbacks by cometClient to drive the long-polling loop
    cometClient = [[DDCometClient alloc] initWithURL:[NSURL URLWithString:cometURL] gmsHeader:userHeader];
    cometClient.delegate = self;
    [cometClient scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [cometClient handshake];
}

- (void)chatApiRequest:(NSString *)urlStr body:(NSDictionary *)bodyDict 
{
    [GMSUtil submitRequestWithURL:urlStr method:@"POST" params:bodyDict headers:@{@"gms_user" : userHeader} completionBlock:nil];
}

- (void) logRequest:(NSString *)txt2Log direction:(int)dir
{
    //Update Log View
    [[NSNotificationCenter defaultCenter] postNotificationName:kUpdateNotification
                                                        object:self
                                                      userInfo:@{@"direction":@(dir), @"text":txt2Log}];
}

- (void)showError:(NSString *)errorStr
{
    UIAlertView *alert = [[UIAlertView alloc]
                          initWithTitle: @"GMS Chat Server Error"
                          message: errorStr
                          delegate: nil
                          cancelButtonTitle:@"OK"
                          otherButtonTitles:nil];
    dispatch_async(dispatch_get_main_queue(), ^{
        [alert show];
    });
}

#pragma mark Application State Handling

-(void) applicationDidEnterBackground
{
    if (cometClient != nil) {
        // Send /meta/disconnect to inform GMS we are going to background
        // Include the last transcriptPosition received in the "ext" field
        [cometClient disconnectSynchronousWithExt:@{
            @"transcriptPosition": transcriptPosition == nil ? @"0": transcriptPosition
        }];
    }
}

-(void) applicationDidBecomeActive
{
    // If chat is active, start a new cometd session.
    if (_chatActive && cometClient != nil) {
        [cometClient handshake];
    }
}

#pragma mark Getters and Setters

- (void) setChatActive:(BOOL)chatActive
{
    _chatActive = chatActive;
    if (chatActive) {
        _doneButton.enabled = YES;
        _textField.enabled = YES;
    }
    else {
        _doneButton.enabled = NO;
        _textField.enabled = NO;
    }
}

#pragma mark - View life cycle

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    if (localBubbleData == nil) {
        localBubbleData = [[NSMutableArray alloc] init];
    }

    if (remoteBubbleData == nil) {
        remoteBubbleData = [[NSMutableArray alloc] init];
    }

    _tableView.bubbleDataSource = self;
    
    // The line below sets the snap interval in seconds. This defines how the bubbles will be grouped in time.
    // Interval of 120 means that if the next messages comes in 2 minutes since the last message, it will be added into the same group.
    // Groups are delimited with header which contains date and time for the first message in the group.
    
    _tableView.snapInterval = 120;
    
    // The line below disables avatar support. Avatar can be specified for each bubble with .avatar property of NSBubbleData.
    // Avatars are enabled for the whole table at once. If particular NSBubbleData misses the avatar, a default placeholder will be set (missingAvatar.png)
    
    _tableView.showAvatars = NO;
    
    // Uncomment the line below to add "Now typing" bubble
    // Possible values are
    //    - NSBubbleTypingTypeSomebody - shows "now typing" bubble on the left
    //    - NSBubbleTypingTypeMe - shows "now typing" bubble on the right
    //    - NSBubbleTypingTypeNobody - no "now typing" bubble
    
    _tableView.typingBubble = NSBubbleTypingTypeNobody;
    
    // Keyboard events
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWasShown:) name:UIKeyboardWillShowNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillBeHidden:) name:UIKeyboardWillHideNotification object:nil];

    [self setChatActive:NO];
    
    // Move button lower in case of iPhone 5 (4-inch display)
    CGRect screenBounds = [[UIScreen mainScreen] bounds];
    if (screenBounds.size.height == 568) {
        // code for 4-inch screen
        CGRect frame = [_textField frame];
        frame.origin.y += 88;
        [_textField setFrame:frame];
    }

}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (void)viewDidUnload {
    [[NSNotificationCenter defaultCenter] removeObserver:self];

    _tableView.bubbleDataSource = nil;
    localBubbleData = nil;
    remoteBubbleData = nil;
    [self setTableView:nil];
    [self setTextField:nil];
    [self setStatusLabel:nil];
    [self setDoneButton:nil];
    [super viewDidUnload];
}

#pragma mark - UIBubbleTableViewDataSource implementation

- (NSInteger)rowsForBubbleTable:(UIBubbleTableView *)tableView
{
    return [localBubbleData count];
}

- (NSBubbleData *)bubbleTableView:(UIBubbleTableView *)tableView dataForRow:(NSInteger)row
{
    return [localBubbleData objectAtIndex:row];
}

#pragma mark - DDComet Client Delegate methods

- (void)cometClientHandshakeDidSucceed:(DDCometClient *)client
{
	[self logRequest:@"Handshake succeeded" direction:fromGMS];
    
    cometClientID = client.clientID;
	
	[client subscribeToChannel:@"/_genesys" target:self selector:@selector(chatMessageReceived:)];
 
    if (!_chatActive) {
        _statusLabel.text = @"Waiting for an agent...";
        [self setChatActive:YES];
        //Start Chat
        NSMutableDictionary *chatDict = [NSMutableDictionary dictionaryWithDictionary:chatParams];
        [chatDict setValue:@"true" forKey:@"_verbose"];
        [chatDict setValue:@"comet" forKey:@"notify_by"];
        
        // If ID for Apple Push Notification is set, then provide push notification info to server
        // by the subscriptionId (set if we have subscribed) or by providing device details in request
        if (![_appDelegate.notifyToken isEqualToString:@"Error"]) {
            if (self->pushSubscriptionId != nil) {
                [chatDict setValue:self->pushSubscriptionId forKey:@"subscriptionID"];
            }
            else {
                [chatDict setValue:@"ios" forKey:@"push_notification_type"];
                [chatDict setValue:_appDelegate.notifyToken forKey:@"push_notification_deviceid"];
                [chatDict setValue:_appDelegate.serverAPNDebug ? @"true": @"false" forKey:@"push_notification_debug"];
                [chatDict setValue:pushNotificationMaxSize forKey:@"push_notification_maxsize"];
            }
        }
        [self chatApiRequest:startSessionURL body:chatDict];
    }
}

- (void) requestCometHandshakeAfterDelay
{
    // Should be called after an error is returned to restart comet
    // Delay this to avoid spinning for errors types that return immediately
    @synchronized(cometClient) {
        if (_delayingBeforeCometHandshake)
            return; // Ignore subsequent errors when we are already going to handshake
        _delayingBeforeCometHandshake = YES;
        
        GMSChatViewController *myself = self;
        double delayInSeconds = 2.0; //
        dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
        dispatch_after(delayTime, dispatch_get_main_queue(), ^(void){
            myself->_delayingBeforeCometHandshake = NO;
            // Provide the current received transcriptionPosition to be sent with first connect after handshake
            [myself->cometClient handshakeWithExtData:@{@"transcriptPosition": myself->transcriptPosition == nil ? @"0": myself->transcriptPosition}];
        });
    }
}

- (void)cometClient:(DDCometClient *)client handshakeDidFailWithError:(NSError *)error
{
	[self logRequest:@"CometD Handshake failed" direction:fromGMS];
    [self requestCometHandshakeAfterDelay];
}

- (void)cometClientConnectDidSucceed:(DDCometClient *)client
{
	[self logRequest:@"CometD Connect succeeded" direction:fromGMS];
}

- (void)cometClient:(DDCometClient *)client connectDidFailWithError:(NSError *)error
{
	[self logRequest:[NSString stringWithFormat:@"CometD Connect failed, error: %@", error] direction:fromGMS];
    [self requestCometHandshakeAfterDelay];
}

- (void)cometClient:(DDCometClient *)client didFailWithTransportError:(NSError *)error
{
    [self logRequest:[NSString stringWithFormat:@"CometD Transport Error, error: %@", error] direction:fromGMS];
    [self requestCometHandshakeAfterDelay];
}

- (void)cometClient:(DDCometClient *)client subscriptionDidSucceed:(DDCometSubscription *)subscription
{
	[self logRequest:@"CometD Subsrciption succeeded" direction:fromGMS];
}

- (void)cometClient:(DDCometClient *)client subscription:(DDCometSubscription *)subscription didFailWithError:(NSError *)error
{
	[self logRequest:[NSString stringWithFormat:@"CometD Subsrciption failed, error: %@", error] direction:fromGMS];
    [self requestCometHandshakeAfterDelay];
}

- (void)chatMessageReceived:(DDCometMessage *)message
{    
	if (message.successful == nil) {
        NSDictionary *jsonResponse = [message.data objectForKey:@"message"];
        
        if (jsonResponse != nil) {
            // Keep track of the highest transcriptPosition received, which we include in /meta/disconnect requests
            transcriptPosition = (NSString *) [jsonResponse objectForKey:@"transcriptPosition"];
            
            NSArray *transcript = (NSArray *) [jsonResponse objectForKey:@"transcriptToShow"];
            
            if (transcript != nil) {
                for (int i=0; i < transcript.count; i++) {
                    NSArray *row = (NSArray *) [transcript objectAtIndex:i];
                    for (int j=0; j < row.count; j++) {
                        NSString *object = [row objectAtIndex:j];
                        if([object isEqualToString:@"Notice.Joined"]) {
                            agentName = [row objectAtIndex:j + 1];                            
                            _statusLabel.text = [NSString stringWithFormat:@"%@ %@", [row objectAtIndex:j+1], [row objectAtIndex:j+2]];
                            break;
                        }
                        if([object isEqualToString:@"Notice.TypingStarted"]) {
                            //React only to messages from agent
                            if ([[row objectAtIndex:j + 1] isEqualToString:agentName]) {
                                _tableView.typingBubble = NSBubbleTypingTypeSomebody;
                                [_tableView reloadData];
                                [self scrollToBottom];
                            }
                             break;
                        }
                        if([object isEqualToString:@"Notice.TypingStopped"]) {
                            _tableView.typingBubble = NSBubbleTypingTypeNobody;
                            [_tableView reloadData];
                            break;
                        }
                        if([object isEqualToString:@"Message.Text"]) {
                            if (agentName != nil) { //Ignore messages before the agent joins in
                                if ([agentName isEqualToString:(NSString *) [row objectAtIndex:j + 1]]) { //Do not display messages apart from the agent's
                                    // Display new message from AGENT
                                    NSBubbleData *rcvBubble = [NSBubbleData dataWithText:[row objectAtIndex:j + 2]
                                                                                    date:[NSDate dateWithTimeIntervalSinceNow:0]
                                                                                    type:BubbleTypeSomeoneElse];
                                    [localBubbleData addObject:rcvBubble];
                                    [remoteBubbleData addObject:rcvBubble];
                                    _tableView.typingBubble = NSBubbleTypingTypeNobody;
                                    [_tableView reloadData];
                                    [self scrollToBottom];
                                 }
                                if ([(NSString *) [row objectAtIndex:j + 4] isEqualToString:@"CLIENT"]) {
                                    // Display new message from CLIENT
                                    NSBubbleData *rcvBubble = [NSBubbleData dataWithText:[row objectAtIndex:j + 2]
                                                                                    date:[NSDate dateWithTimeIntervalSinceNow:0]
                                                                                    type:BubbleTypeMine];
                                    [remoteBubbleData addObject:rcvBubble];
                                    if (![localBubbleData isEqual:remoteBubbleData]) {
                                        //Sync
                                        [self logRequest:@"Synchronized remote chat messages order..." direction:fromGMS];
                                        localBubbleData = [NSMutableArray arrayWithArray:remoteBubbleData];
                                        [_tableView reloadData];
                                        [self scrollToBottom];
                                    }
                                }
                            }
                            break;
                        }
                        if([object isEqualToString:@"Notice.Left"]) {
                            if (agentName != nil) { //Ignore messages before the agent joins in
                                if ([agentName isEqualToString:(NSString *) [row objectAtIndex:j + 1]]) { //Do not display messages apart from the agent's
                                    _statusLabel.text = [NSString stringWithFormat:@"%@ %@", [row objectAtIndex:j+1], [row objectAtIndex:j+2]];
                                    [self performSelector:@selector(endChat:) withObject:nil afterDelay:5.0];
                                }
                            }
                            break;
                        }
                    }
                }
            }
        }
        
        NSString *state = (NSString *) [jsonResponse objectForKey:@"chatIxnState"];
        if (state != nil && [state isEqualToString:@"DISCONNECTED"]) {
            [self performSelector:@selector(endChat:) withObject:nil afterDelay:5.0];
        }
    }
	else if (![message.successful boolValue])
		_statusLabel.text = @"Unable to send message";
}

//- (void)membershipMessageReceived:(DDCometMessage *)message
//{
//}

-(void) scrollToBottom
{
    int lastSection=[_tableView numberOfSections]-1;
    int lastRowNumber = [_tableView numberOfRowsInSection:lastSection]-1;
    NSIndexPath* ip = [NSIndexPath indexPathForRow:lastRowNumber inSection:lastSection];
    [_tableView scrollToRowAtIndexPath:ip atScrollPosition:UITableViewScrollPositionTop animated:YES];
}

#pragma mark - Keyboard events

- (void)keyboardWasShown:(NSNotification*)aNotification
{
    NSDictionary* info = [aNotification userInfo];
    CGSize kbSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
    
    [UIView animateWithDuration:0.2f animations:^{
        
        CGRect frame = _textField.frame;
        frame.origin.y -= kbSize.height - [[[super tabBarController] tabBar] frame].size.height;
        _textField.frame = frame;
        
        frame = _tableView.frame;
        frame.size.height -= kbSize.height - [[[super tabBarController] tabBar] frame].size.height;
        _tableView.frame = frame;
    }];
}

- (void)keyboardWillBeHidden:(NSNotification*)aNotification
{
    NSDictionary* info = [aNotification userInfo];
    CGSize kbSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
    
    [UIView animateWithDuration:0.2f animations:^{
        
        CGRect frame = _textField.frame;
        frame.origin.y += kbSize.height - [[[super tabBarController] tabBar] frame].size.height;
        _textField.frame = frame;
        
        frame = _tableView.frame;
        frame.size.height += kbSize.height - [[[super tabBarController] tabBar] frame].size.height;
        _tableView.frame = frame;
    }];
}



#pragma mark - TextField delegates

- (void)textFieldDidBeginEditing:(UITextField *)textField
{
    [self chatApiRequest:[startSessionURL stringByAppendingString:startTypingURL] body:@{@"_verbose": @"true"}];
}

- (void)textFieldDidEndEditing:(UITextField *)textField
{
    [self chatApiRequest:[startSessionURL stringByAppendingString:stopTypingURL] body:@{@"_verbose": @"true"}];
}

//Dismiss keyboard when Return key is pressed
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
    
    _tableView.typingBubble = NSBubbleTypingTypeNobody;
    
    if ([_textField.text length] > 0) {
        
        [self chatApiRequest:[startSessionURL stringByAppendingString:refreshURL] body:@{@"_verbose": @"true", @"message" : _textField.text}];
        
        NSBubbleData *sayBubble = [NSBubbleData dataWithText:_textField.text date:[NSDate dateWithTimeIntervalSinceNow:0] type:BubbleTypeMine];
        [localBubbleData addObject:sayBubble];
        [_tableView reloadData];
        [self scrollToBottom];
    }
    
    _textField.text = @"";
    [_textField resignFirstResponder];
    
    return YES;
}


#pragma mark - Action Clear

- (IBAction)clearChat:(id)sender {
    [localBubbleData removeAllObjects];
    [remoteBubbleData removeAllObjects];
    [_tableView reloadData];
}

- (IBAction)endChat:(id)sender {
    _statusLabel.text = @"Chat Ended";
    [self setChatActive:NO];
    [self chatApiRequest:[startSessionURL stringByAppendingString:disconnectURL] body:@{@"_verbose": @"true"}];
    [cometClient unsubsubscribeFromChannel:@"/_genesys" target:self selector:@selector(chatMessageReceived:)];
    [cometClient disconnect];
    cometClient.delegate = nil;
    cometClient = nil;
    
    if (pushSubscriptionId != nil) {
        [GMSUtil submitRequestWithPath:[@"/1/notification/subscription/" stringByAppendingString:pushSubscriptionId]
            method:@"DELETE" params:nil headers:nil completionBlock:nil];
    }
}

@end
