/*
* Apache 2.0 License
*
* Copyright (c) Sebastian Katzer 2017
*
* This file contains Original Code and/or Modifications of Original Code
* as defined in and that are subject to the Apache License
* Version 2.0 (the 'License'). You may not use this file except in
* compliance with the License. Please obtain a copy of the License at
* http://opensource.org/licenses/Apache-2.0/ and read it before using this
* file.
*
* The Original Code and all software distributed under the License are
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
* Please see the License for the specific language governing rights and
* limitations under the License.
*/

// codebeat:disable[TOO_MANY_FUNCTIONS]

#import "APPLocalNotification.h"
#import "APPNotificationCategory.h"
#import "APPNotificationContent.h"
#import "APPNotificationOptions.h"
#import "UNNotificationRequest+APPLocalNotification.h"
#import "UNUserNotificationCenter+APPLocalNotification.h"

@interface APPLocalNotification ()

@property(strong, nonatomic) UNUserNotificationCenter *center;
@property(NS_NONATOMIC_IOSONLY, nullable, weak)
id<UNUserNotificationCenterDelegate>
delegate;
@property(readwrite, assign) BOOL deviceready;
@property(readwrite, assign) BOOL isActive;
@property(readonly, nonatomic, retain) NSArray *launchDetails;
@property(readonly, nonatomic, retain) NSMutableArray *eventQueue;

@end

@implementation APPLocalNotification

UNNotificationPresentationOptions const OptionNone =
UNNotificationPresentationOptionNone;
UNNotificationPresentationOptions const OptionBadge =
UNNotificationPresentationOptionBadge;
UNNotificationPresentationOptions const OptionSound =
UNNotificationPresentationOptionSound;
UNNotificationPresentationOptions const OptionAlert =
UNNotificationPresentationOptionAlert;

@synthesize deviceready, isActive, eventQueue;

#pragma mark -
#pragma mark Interface

/**
* Set launchDetails object.
*
* @return [ Void ]
*/
- (void)launch:(CDVInvokedUrlCommand *)command {
NSString *js;

if (!_launchDetails)
return;

js = [NSString stringWithFormat:@"cordova.plugins.notification.local."
@"launchDetails = {id:%@, action:'%@'}",
_launchDetails[0], _launchDetails[1]];

[self.commandDelegate evalJs:js];

_launchDetails = NULL;
}

/**
* Execute all queued events.
*
* @return [ Void ]
*/
- (void)ready:(CDVInvokedUrlCommand *)command {
deviceready = YES;

[self.commandDelegate runInBackground:^{
for (NSString *js in eventQueue) {
[self.commandDelegate evalJs:js];
}
[eventQueue removeAllObjects];
}];
}

/**
* Schedule notifications.
*
* @param [Array<Hash>] properties A list of key-value properties.
*
* @return [ Void ]
*/
- (void)schedule:(CDVInvokedUrlCommand *)command {
NSArray *notifications = command.arguments;

[self.commandDelegate runInBackground:^{
for (NSDictionary *options in notifications) {
APPNotificationContent *notification;

notification = [[APPNotificationContent alloc] initWithOptions:options];

[self scheduleNotification:notification];
}

[self check:command];
}];
}

/**
* Update notifications.
*
* @param [Array<Hash>] properties A list of key-value properties.
*
* @return [ Void ]
*/
- (void)update:(CDVInvokedUrlCommand *)command {
NSArray *notifications = command.arguments;

[self.commandDelegate runInBackground:^{
for (NSDictionary *options in notifications) {
NSNumber *id = [options objectForKey:@"id"];
UNNotificationRequest *notification;

notification = [_center getNotificationWithId:id];

if (!notification)
continue;

[self updateNotification:[notification copy] withOptions:options];

[self fireEvent:@"update" notification:notification];
}

[self check:command];
}];
}

/**
* Clear notifications by id.
*
* @param [ Array<Int> ] The IDs of the notifications to clear.
*
* @return [ Void ]
*/
- (void)clear:(CDVInvokedUrlCommand *)command {
[self.commandDelegate runInBackground:^{
for (NSNumber *id in command.arguments) {
UNNotificationRequest *notification;

notification = [_center getNotificationWithId:id];

if (!notification)
continue;

[_center clearNotification:notification];
[self fireEvent:@"clear" notification:notification];
}

[self execCallback:command];
}];
}

/**
* Clear all local notifications.
*
* @return [ Void ]
*/
- (void)clearAll:(CDVInvokedUrlCommand *)command {
[self.commandDelegate runInBackground:^{
[_center clearNotifications];
[self clearApplicationIconBadgeNumber];
[self fireEvent:@"clearall"];
[self execCallback:command];
}];
}

/**
* Cancel notifications by id.
*
* @param [ Array<Int> ] The IDs of the notifications to clear.
*
* @return [ Void ]
*/
- (void)cancel:(CDVInvokedUrlCommand *)command {
[self.commandDelegate runInBackground:^{
for (NSNumber *id in command.arguments) {
UNNotificationRequest *notification;

notification = [_center getNotificationWithId:id];

if (!notification)
continue;

[_center cancelNotification:notification];
[self fireEvent:@"cancel" notification:notification];
}

[self execCallback:command];
}];
}

/**
* Cancel all local notifications.
*
* @return [ Void ]
*/
- (void)cancelAll:(CDVInvokedUrlCommand *)command {
[self.commandDelegate runInBackground:^{
[_center cancelNotifications];
[self clearApplicationIconBadgeNumber];
[self fireEvent:@"cancelall"];
[self execCallback:command];
}];
}

/**
* Get type of notification.
*
* @param [ Int ] id The ID of the notification.
*
* @return [ Void ]
*/
- (void)type:(CDVInvokedUrlCommand *)command {
[self.commandDelegate runInBackground:^{
NSNumber *id = [command argumentAtIndex:0];
NSString *type;

switch ([_center getTypeOfNotificationWithId:id]) {
case NotifcationTypeScheduled:
type = @"scheduled";
break;
case NotifcationTypeTriggered:
type = @"triggered";
break;
default:
type = @"unknown";
}

CDVPluginResult *result;
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK
messageAsString:type];

[self.commandDelegate sendPluginResult:result
callbackId:command.callbackId];
}];
}

/**
* List of notification IDs by type.
*
* @return [ Void ]
*/
- (void)ids:(CDVInvokedUrlCommand *)command {
[self.commandDelegate runInBackground:^{
int code = [command.arguments[0] intValue];
APPNotificationType type = NotifcationTypeUnknown;

switch (code) {
case 0:
type = NotifcationTypeAll;
break;
case 1:
type = NotifcationTypeScheduled;
break;
case 2:
type = NotifcationTypeTriggered;
break;
}

NSArray *ids = [_center getNotificationIdsByType:type];

CDVPluginResult *result;
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK
messageAsArray:ids];

[self.commandDelegate sendPluginResult:result
callbackId:command.callbackId];
}];
}

/**
* Notification by id.
*
* @param [ Number ] id The id of the notification to return.
*
* @return [ Void ]
*/
- (void)notification:(CDVInvokedUrlCommand *)command {
[self.commandDelegate runInBackground:^{
NSArray *ids = command.arguments;

NSArray *notifications;
notifications = [_center getNotificationOptionsById:ids];

CDVPluginResult *result;
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK
messageAsDictionary:[notifications firstObject]];

[self.commandDelegate sendPluginResult:result
callbackId:command.callbackId];
}];
}

/**
* List of notifications by id.
*
* @param [ Array<Number> ] ids The ids of the notifications to return.
*
* @return [ Void ]
*/
- (void)notifications:(CDVInvokedUrlCommand *)command {
[self.commandDelegate runInBackground:^{
int code = [command.arguments[0] intValue];
APPNotificationType type = NotifcationTypeUnknown;
NSArray *toasts;
NSArray *ids;

switch (code) {
case 0:
type = NotifcationTypeAll;
break;
case 1:
type = NotifcationTypeScheduled;
break;
case 2:
type = NotifcationTypeTriggered;
break;
case 3:
ids = command.arguments[1];
toasts = [_center getNotificationOptionsById:ids];
break;
}

if (toasts == nil) {
toasts = [_center getNotificationOptionsByType:type];
}

CDVPluginResult *result;
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK
messageAsArray:toasts];

[self.commandDelegate sendPluginResult:result
callbackId:command.callbackId];
}];
}

/**
* Check for permission to show notifications.
*
* @return [ Void ]
*/
- (void)check:(CDVInvokedUrlCommand *)command {
[_center getNotificationSettingsWithCompletionHandler:^(
UNNotificationSettings *settings) {
BOOL authorized =
settings.authorizationStatus == UNAuthorizationStatusAuthorized;
BOOL enabled =
settings.notificationCenterSetting == UNNotificationSettingEnabled;
BOOL permitted = authorized && enabled;

[self execCallback:command arg:permitted];
}];
}

/**
* Request for permission to show notifcations.
*
* @return [ Void ]
*/
- (void)request:(CDVInvokedUrlCommand *)command {
UNAuthorizationOptions options =
(UNAuthorizationOptionBadge | UNAuthorizationOptionSound |
UNAuthorizationOptionAlert);

[_center requestAuthorizationWithOptions:options
completionHandler:^(BOOL granted, NSError *e) {
[self check:command];
}];
}

/**
* Register/update an action group.
*
* @return [ Void ]
*/
- (void)actions:(CDVInvokedUrlCommand *)command {
[self.commandDelegate runInBackground:^{
int code = [command.arguments[0] intValue];
NSString *identifier = [command argumentAtIndex:1];
NSArray *actions = [command argumentAtIndex:2];
UNNotificationCategory *group;
BOOL found;

switch (code) {
case 0:
group = [APPNotificationCategory parse:actions withId:identifier];
[_center addActionGroup:group];
[self execCallback:command];
break;
case 1:
[_center removeActionGroup:identifier];
[self execCallback:command];
break;
case 2:
found = [_center hasActionGroup:identifier];
[self execCallback:command arg:found];
break;
}
}];
}

#pragma mark -
#pragma mark Private

/**
* Schedule the local notification.
*
* @param [ APPNotificationContent* ] notification The notification to schedule.
*
* @return [ Void ]
*/
- (void)scheduleNotification:(APPNotificationContent *)notification {
__weak APPLocalNotification *weakSelf = self;
UNNotificationRequest *request = notification.request;
NSString *event = [request wasUpdated] ? @"update" : @"add";

[_center addNotificationRequest:request
withCompletionHandler:^(NSError *e) {
__strong APPLocalNotification *strongSelf = weakSelf;
[strongSelf fireEvent:event notification:request];
}];
}

/**
* Update the local notification.
*
* @param [ UNNotificationRequest* ] notification The notification to update.
* @param [ NSDictionary* ] options The options to update.
*
* @return [ Void ]
*/
- (void)updateNotification:(UNNotificationRequest *)notification
withOptions:(NSDictionary *)newOptions {
NSMutableDictionary *options = [notification.content.userInfo mutableCopy];

[options addEntriesFromDictionary:newOptions];
[options setObject:[NSDate date] forKey:@"updatedAt"];

APPNotificationContent *newNotification =
[[APPNotificationContent alloc] initWithOptions:options];

[self scheduleNotification:newNotification];
}

#pragma mark -
#pragma mark UNUserNotificationCenterDelegate

/**
* Called when a notification is delivered to the app while being in foreground.
*/
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
willPresentNotification:(UNNotification *)notification
withCompletionHandler:
(void (^)(UNNotificationPresentationOptions))handler {
UNNotificationRequest *toast = notification.request;

[_delegate userNotificationCenter:center
willPresentNotification:notification
withCompletionHandler:handler];

if ([toast.trigger isKindOfClass:UNPushNotificationTrigger.class])
return;

APPNotificationOptions *options = toast.options;

if (![notification.request wasUpdated]) {
[self fireEvent:@"trigger" notification:toast];
}

if (options.silent) {
handler(OptionNone);
} else if (!isActive || options.priority > 0) {
handler(OptionBadge | OptionSound | OptionAlert);
} else {
handler(OptionBadge | OptionSound);
}
}

/**
* Called to let your app know which action was selected by the user for a given
* notification.
*/
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
withCompletionHandler:(void (^)(void))handler {
UNNotificationRequest *toast = response.notification.request;

[_delegate userNotificationCenter:center
didReceiveNotificationResponse:response
withCompletionHandler:handler];

handler();

if ([toast.trigger isKindOfClass:UNPushNotificationTrigger.class])
return;

NSMutableDictionary *data = [[NSMutableDictionary alloc] init];
NSString *action = response.actionIdentifier;
NSString *event = action;

if ([action isEqualToString:UNNotificationDefaultActionIdentifier]) {
event = @"click";
} else if ([action isEqualToString:UNNotificationDismissActionIdentifier]) {
event = @"clear";
}

if (!deviceready && [event isEqualToString:@"click"]) {
_launchDetails = @[ toast.options.id, event ];
}

if (![event isEqualToString:@"clear"]) {
[self fireEvent:@"clear" notification:toast];
}

if ([response isKindOfClass:UNTextInputNotificationResponse.class]) {
[data setObject:((UNTextInputNotificationResponse *)response).userText
forKey:@"text"];
}

[self fireEvent:event notification:toast data:data];
}

#pragma mark -
#pragma mark Life Cycle

/**
* Registers obervers after plugin was initialized.
*/
- (void)pluginInitialize {
eventQueue = [[NSMutableArray alloc] init];
_center = [UNUserNotificationCenter currentNotificationCenter];
_delegate = _center.delegate;

_center.delegate = self;
[_center registerGeneralNotificationCategory];

[self monitorAppStateChanges];
}

/**
* Monitor changes of the app state and update the _isActive flag.
*/
- (void)monitorAppStateChanges {
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];

[center addObserverForName:UIApplicationDidBecomeActiveNotification
object:NULL
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *e) {
isActive = YES;
}];

[center addObserverForName:UIApplicationDidEnterBackgroundNotification
object:NULL
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *e) {
isActive = NO;
}];
}

#pragma mark -
#pragma mark Helper

/**
* Removes the badge number from the app icon.
*/
- (void)clearApplicationIconBadgeNumber {
dispatch_async(dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] setApplicationIconBadgeNumber:0];
});
}

/**
* Invokes the callback without any parameter.
*
* @return [ Void ]
*/
- (void)execCallback:(CDVInvokedUrlCommand *)command {
CDVPluginResult *result =
[CDVPluginResult resultWithStatus:CDVCommandStatus_OK];

[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
}

/**
* Invokes the callback with a single boolean parameter.
*
* @return [ Void ]
*/
- (void)execCallback:(CDVInvokedUrlCommand *)command arg:(BOOL)arg {
CDVPluginResult *result =
[CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsBool:arg];

[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
}

/**
* Fire general event.
*
* @param [ NSString* ] event The name of the event to fire.
*
* @return [ Void ]
*/
- (void)fireEvent:(NSString *)event {
NSMutableDictionary *data = [[NSMutableDictionary alloc] init];

[self fireEvent:event notification:NULL data:data];
}

/**
* Fire event for about a local notification.
*
* @param [ NSString* ] event The name of the event to fire.
* @param [ APPNotificationRequest* ] notification The local notification.
*
* @return [ Void ]
*/
- (void)fireEvent:(NSString *)event
notification:(UNNotificationRequest *)notitification {
NSMutableDictionary *data = [[NSMutableDictionary alloc] init];

[self fireEvent:event notification:notitification data:data];
}

/**
* Fire event for about a local notification.
*
* @param [ NSString* ] event The name of the event to fire.
* @param [ APPNotificationRequest* ] notification The local notification.
* @param [ NSMutableDictionary* ] data Event object with additional data.
*
* @return [ Void ]
*/
- (void)fireEvent:(NSString *)event
notification:(UNNotificationRequest *)request
data:(NSMutableDictionary *)data {
NSString *js, *params, *notiAsJSON, *dataAsJSON;
NSData *dataAsData;

[data setObject:event forKey:@"event"];
[data setObject:@(isActive) forKey:@"foreground"];
[data setObject:@(!deviceready) forKey:@"queued"];

if (request) {
notiAsJSON = [request encodeToJSON];
[data setObject:request.options.id forKey:@"notification"];
}

dataAsData = [NSJSONSerialization dataWithJSONObject:data
options:0
error:NULL];

dataAsJSON = [[NSString alloc] initWithData:dataAsData
encoding:NSUTF8StringEncoding];

if (request) {
params = [NSString stringWithFormat:@"%@,%@", notiAsJSON, dataAsJSON];
} else {
params = [NSString stringWithFormat:@"%@", dataAsJSON];
}

js = [NSString stringWithFormat:
@"cordova.plugins.notification.local.fireEvent('%@', %@)",
event, params];

if (deviceready) {
[self.commandDelegate evalJs:js];
} else {
[self.eventQueue addObject:js];
}
}

@end

// codebeat:enable[TOO_MANY_FUNCTIONS]