// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/installation_notifier.h"

#import <UIKit/UIKit.h>

#include "base/ios/weak_nsobject.h"
#include "base/logging.h"
#include "base/mac/scoped_nsobject.h"
#include "base/memory/scoped_ptr.h"
#include "base/metrics/histogram.h"
#include "ios/web/public/web_thread.h"
#include "net/base/backoff_entry.h"
#include "url/gurl.h"

namespace {
const net::BackoffEntry::Policy kPollingBackoffPolicy = {
    0,          // Number of errors to ignore.
    1 * 1000,   // Initial delay in milliseconds.
    1.5,        // Multiply factor.
    0.1,        // Jitter factor.
    60 * 1000,  // Maximum backoff in milliseconds.
    -1,         // Entry lifetime.
    false       // Always use initial delay.
};
}  // namespace

@interface DefaultDispatcher : NSObject<DispatcherProtocol>
@end

@implementation DefaultDispatcher
- (void)dispatchAfter:(int64_t)delayInNSec withBlock:(dispatch_block_t)block {
  dispatch_time_t dispatchTime = dispatch_time(DISPATCH_TIME_NOW, delayInNSec);
  dispatch_after(dispatchTime, dispatch_get_main_queue(), block);
}
@end

@interface InstallationNotifier ()
// Registers for a notification and gives the option to not immediately start
// polling. |scheme| must not be nil nor an empty string.
- (void)registerForInstallationNotifications:(id)observer
                                withSelector:(SEL)notificationSelector
                                   forScheme:(NSString*)scheme
                                startPolling:(BOOL)poll;
// Dispatches a block with an exponentially increasing delay.
- (void)dispatchInstallationNotifierBlock;
// Dispatched blocks cannot be cancelled. Instead, each block has a |blockId|.
// If |blockId| is different from |lastCreatedBlockId_|, then the block does
// not execute anything.
@property(nonatomic, readonly) int lastCreatedBlockId;
@end

@interface InstallationNotifier (Testing)
// Sets the dispatcher.
- (void)setDispatcher:(id<DispatcherProtocol>)dispatcher;
// Sets the UIApplication used to determine if a scheme can be opened by an
// application.
- (void)setSharedApplication:(UIApplication*)sharedApplication;
@end

@implementation InstallationNotifier {
  scoped_ptr<net::BackoffEntry> _backoffEntry;
  base::scoped_nsprotocol<id<DispatcherProtocol>> _dispatcher;
  // Dictionary mapping URL schemes to mutable sets of observers.
  base::scoped_nsobject<NSMutableDictionary> _installedAppObservers;
  NSNotificationCenter* _notificationCenter;  // Weak.

  // This object can be a fake application in unittests.
  UIApplication* sharedApplication_;  // Weak.
}

@synthesize lastCreatedBlockId = lastCreatedBlockId_;

+ (InstallationNotifier*)sharedInstance {
  static InstallationNotifier* instance = [[InstallationNotifier alloc] init];
  return instance;
}

- (instancetype)init {
  self = [super init];
  if (self) {
    lastCreatedBlockId_ = 0;
    _dispatcher.reset([[DefaultDispatcher alloc] init]);
    _installedAppObservers.reset([[NSMutableDictionary alloc] init]);
    _notificationCenter = [NSNotificationCenter defaultCenter];
    sharedApplication_ = [UIApplication sharedApplication];
    _backoffEntry.reset(new net::BackoffEntry([self backOffPolicy]));
  }
  return self;
}

- (void)registerForInstallationNotifications:(id)observer
                                withSelector:(SEL)notificationSelector
                                   forScheme:(NSString*)scheme {
  [self registerForInstallationNotifications:observer
                                withSelector:notificationSelector
                                   forScheme:scheme
                                startPolling:YES];
}

- (void)registerForInstallationNotifications:(id)observer
                                withSelector:(SEL)notificationSelector
                                   forScheme:(NSString*)scheme
                                startPolling:(BOOL)poll {
  // Workaround a crash caused by calls to this function with a nil |scheme|.
  if (![scheme length])
    return;
  DCHECK([observer respondsToSelector:notificationSelector]);
  DCHECK([scheme rangeOfString:@":"].location == NSNotFound);
  // A strong reference would prevent the observer from unregistering itself
  // from its dealloc method, because the dealloc itself would never be called.
  NSValue* weakReferenceToObserver =
      [NSValue valueWithNonretainedObject:observer];
  NSMutableSet* observers = [_installedAppObservers objectForKey:scheme];
  if (!observers)
    observers = [[[NSMutableSet alloc] init] autorelease];
  if ([observers containsObject:weakReferenceToObserver])
    return;
  [observers addObject:weakReferenceToObserver];
  [_installedAppObservers setObject:observers forKey:scheme];
  [_notificationCenter addObserver:observer
                          selector:notificationSelector
                              name:scheme
                            object:self];
  _backoffEntry->Reset();
  if (poll)
    [self dispatchInstallationNotifierBlock];
}

- (void)unregisterForNotifications:(id)observer {
  DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
  NSValue* weakReferenceToObserver =
      [NSValue valueWithNonretainedObject:observer];
  [_notificationCenter removeObserver:observer];
  for (NSString* scheme in [_installedAppObservers allKeys]) {
    DCHECK([scheme isKindOfClass:[NSString class]]);
    NSMutableSet* observers = [_installedAppObservers objectForKey:scheme];
    if ([observers containsObject:weakReferenceToObserver]) {
      [observers removeObject:weakReferenceToObserver];
      if ([observers count] == 0) {
        [_installedAppObservers removeObjectForKey:scheme];
        UMA_HISTOGRAM_BOOLEAN("NativeAppLauncher.InstallationDetected", NO);
      }
    }
  }
}

- (void)checkNow {
  // Reset the back off polling.
  _backoffEntry->Reset();
  [self pollForTheInstallationOfApps];
}

- (void)dispatchInstallationNotifierBlock {
  DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
  int blockId = ++lastCreatedBlockId_;
  _backoffEntry->InformOfRequest(false);
  int64_t delayInNSec =
      _backoffEntry->GetTimeUntilRelease().InMicroseconds() * NSEC_PER_USEC;
  base::WeakNSObject<InstallationNotifier> weakSelf(self);
  [_dispatcher dispatchAfter:delayInNSec
                   withBlock:^{
                     DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
                     base::scoped_nsobject<InstallationNotifier> strongSelf(
                         [weakSelf retain]);
                     if (blockId == [strongSelf lastCreatedBlockId]) {
                       [strongSelf pollForTheInstallationOfApps];
                     }
                   }];
}

- (void)pollForTheInstallationOfApps {
  DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
  __block BOOL keepPolling = NO;
  NSMutableSet* keysToDelete = [NSMutableSet set];
  [_installedAppObservers enumerateKeysAndObjectsUsingBlock:^(id scheme,
                                                              id observers,
                                                              BOOL* stop) {
    DCHECK([scheme isKindOfClass:[NSString class]]);
    DCHECK([observers isKindOfClass:[NSMutableSet class]]);
    DCHECK([observers count] > 0);
    NSURL* testSchemeURL =
        [NSURL URLWithString:[NSString stringWithFormat:@"%@:", scheme]];
    if ([sharedApplication_ canOpenURL:testSchemeURL]) {
      [_notificationCenter postNotificationName:scheme object:self];
      for (id weakReferenceToObserver in observers) {
        id observer = [weakReferenceToObserver nonretainedObjectValue];
        [_notificationCenter removeObserver:observer name:scheme object:self];
      }
      if (![keysToDelete containsObject:scheme]) {
        [keysToDelete addObject:scheme];
        UMA_HISTOGRAM_BOOLEAN("NativeAppLauncher.InstallationDetected", YES);
      }
    } else {
      keepPolling = YES;
    }
  }];
  [_installedAppObservers removeObjectsForKeys:[keysToDelete allObjects]];
  if (keepPolling)
    [self dispatchInstallationNotifierBlock];
}

- (net::BackoffEntry::Policy const*)backOffPolicy {
  return &kPollingBackoffPolicy;
}

#pragma mark -
#pragma mark Testing setters

- (void)setDispatcher:(id<DispatcherProtocol>)dispatcher {
  _dispatcher.reset(dispatcher);
}

- (void)setSharedApplication:(id)sharedApplication {
  // Verify that the test application object responds to all the selectors that
  // will be called on it.
  CHECK([sharedApplication respondsToSelector:@selector(canOpenURL:)]);
  sharedApplication_ = (UIApplication*)sharedApplication;
}

@end