// Copyright 2015 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/web/public/crw_browsing_data_store.h"

#import <Foundation/Foundation.h>

#include "base/ios/ios_util.h"
#import "base/ios/weak_nsobject.h"
#include "base/logging.h"
#import "base/mac/scoped_nsobject.h"
#import "ios/web/browsing_data_managers/crw_cookie_browsing_data_manager.h"
#include "ios/web/public/active_state_manager.h"
#include "ios/web/public/browser_state.h"

namespace {
// Represents the type of operations that a CRWBrowsingDataStore can perform.
enum OperationType {
  // Represents NOP.
  NONE = 1,
  // Stash operation involves stashing browsing data that web views create to
  // the associated BrowserState's state path.
  STASH,
  // Restore operation involves restoring browsing data from the
  // associated BrowserState's state path so that web views can read from them.
  RESTORE,
  // Remove operation involves removing the data that web views create.
  REMOVE,
};

// The name of the NSOperation that performs a |RESTORE| operation.
NSString* const kRestoreOperationName = @"CRWBrowsingDataStore.RESTORE";
// The name of the NSOperation that performs a |STASH| operation.
NSString* const kStashOperationName = @"CRWBrowsingDataStore.STASH";
}  // namespace

// CRWBrowsingDataStore is implemented using 2 queues.
// 1) operationQueueForStashAndRestoreOperations:
// - This queue is used to perform |STASH| and |RESTORE| operations.
// - |STASH|, |RESTORE| operations are operations that have been explicitly
//   requested by the user. And the performance of these operations block
//   further user interaction. Hence this queue has a QoS of
//   NSQualityOfServiceUserInitiated.
// - |STASH|, |RESTORE| operations from 2 different CRWBrowsingDataStores are
//    not re-entrant. Hence this is a serial queue.
// 2) operationQueueForRemoveOperations:
// - This queue is used to perform |REMOVE| operations.
// - |REMOVE| operations is an operation that has been explicitly requested by
//   the user. And the performance of this operation blocks further user
//   interaction. Hence this queue has a QoS of NSQualityOfServiceUserInitiated.
// - |REMOVE| operations from 2 different CRWBrowingDataStores can be run in
//   parallel. Hence this is a concurrent queue.
//
// |STASH|, |RESTORE|, |REMOVE| operations of a particular CRWBrowsingDataStore
// are not re-entrant. Hence these operations are serialized.

@interface CRWBrowsingDataStore ()
// Redefined to be read-write. Must be called from the main thread.
@property(nonatomic, assign) web::BrowsingDataStoreMode mode;
// The array of all browsing data managers. Must be called from the main
// thread.
@property(nonatomic, readonly) NSArray* allBrowsingDataManagers;
// The number of |STASH| or |RESTORE| operations that are still pending. Must be
// called from the main thread.
@property(nonatomic, assign) NSUInteger numberOfPendingStashOrRestoreOperations;

// Returns a serial queue on which |STASH| and |RESTORE| operations can be
// scheduled to be run. All |STASH|/|RESTORE| operations need to be run on the
// same queue hence it is shared with all CRWBrowsingDataStores.
+ (NSOperationQueue*)operationQueueForStashAndRestoreOperations;
// Returns a concurrent queue on which remove operations can be scheduled to be
// run. All |REMOVE| operations need to be run on the same queue hence it is
// shared with all CRWBrowsingDataStores.
+ (NSOperationQueue*)operationQueueForRemoveOperations;

// Returns an array of CRWBrowsingDataManagers for the given
// |browsingDataTypes|.
- (NSArray*)browsingDataManagersForBrowsingDataTypes:
    (web::BrowsingDataTypes)browsingDataTypes;
// Returns the selector that needs to be performed on the
// CRWBrowsingDataManagers for the |operationType|. |operationType| cannot be
// |NONE|.
- (SEL)browsingDataManagerSelectorForOperationType:(OperationType)operationType;
// Returns the selector that needs to be performed on the
// CRWBrowsingDataManagers for a |REMOVE| operation.
- (SEL)browsingDataManagerSelectorForRemoveOperationType;

// Sets the mode iff there are no more |STASH| or |RESTORE| operations that are
// still pending. |mode| can only be either |ACTIVE| or |INACTIVE|.
// Returns YES if the mode was successfully changed to |mode|.
- (BOOL)finalizeChangeToMode:(web::BrowsingDataStoreMode)mode;
// Changes the mode of the CRWBrowsingDataStore to |mode|. This is an
// asynchronous operation and the mode is not changed immediately.
// |completionHandler| can be nil.
// |completionHandler| is called on the main thread. This block has no return
// value and takes a single BOOL argument that indicates whether or not the
// mode change was successfully changed to |mode|.
- (void)changeMode:(web::BrowsingDataStoreMode)mode
    completionHandler:(void (^)(BOOL modeChangeWasSuccessful))completionHandler;

// Returns the OperationType (that needs to be performed) in order to change the
// mode to |mode|. Consults the delegate if one is present. |mode| cannot be
// |CHANGING|.
- (OperationType)operationTypeToChangeMode:(web::BrowsingDataStoreMode)mode;
// Performs an operation of type |operationType| on each of the
// |browsingDataManagers|. |operationType| cannot be |NONE|.
// Precondition: There must be no web views associated with the BrowserState.
// |completionHandler| is called on the main thread and cannot be nil.
- (void)performOperationWithType:(OperationType)operationType
            browsingDataManagers:(NSArray*)browsingDataManagers
               completionHandler:(ProceduralBlock)completionHandler;
// Returns an NSOperation that calls |selector| on all the
// |browsingDataManagers|. |selector| needs to be one of the methods in
// CRWBrowsingDataManager.
- (NSOperation*)operationWithBrowsingDataManagers:(NSArray*)browsingDataManagers
                                         selector:(SEL)selector;
// Enqueues |operation| to be run on |queue|. All operations are serialized to
// be run one after another.
- (void)addOperation:(NSOperation*)operation toQueue:(NSOperationQueue*)queue;
@end

@implementation CRWBrowsingDataStore {
  web::BrowserState* _browserState;  // Weak, owns this object.
  // The delegate.
  base::WeakNSProtocol<id<CRWBrowsingDataStoreDelegate>> _delegate;
  // The mode of the CRWBrowsingDataStore.
  web::BrowsingDataStoreMode _mode;
  // The dictionary that maps a browsing data type to its
  // CRWBrowsingDataManager.
  base::scoped_nsobject<NSDictionary> _browsingDataTypeMap;
  // The last operation that was enqueued to be run. Can be a |STASH|, |RESTORE|
  // or a |REMOVE| operation.
  base::scoped_nsobject<NSOperation> _lastDispatchedOperation;
  // The last dispatched |STASH| or |RESTORE| operation that was enqueued to be
  // run.
  base::scoped_nsobject<NSOperation> _lastDispatchedStashOrRestoreOperation;
  // The number of |STASH| or |RESTORE| operations that are still pending. If
  // the number of |STASH| or |RESTORE| operations is greater than 0U, the mode
  // of the CRWBrowsingDataStore is |CHANGING|. The mode can be made ACTIVE or
  // INACTIVE only be set when this value is 0U.
  NSUInteger _numberOfPendingStashOrRestoreOperations;
}

#pragma mark -
#pragma mark NSObject Methods

- (instancetype)initWithBrowserState:(web::BrowserState*)browserState {
  self = [super init];
  if (self) {
    DCHECK([NSThread isMainThread]);
    DCHECK(browserState);
    _browserState = browserState;
    web::ActiveStateManager* activeStateManager =
        web::BrowserState::GetActiveStateManager(browserState);
    DCHECK(activeStateManager);
    _mode = activeStateManager->IsActive() ? web::ACTIVE : web::INACTIVE;
    base::scoped_nsobject<CRWCookieBrowsingDataManager>
        cookieBrowsingDataManager([[CRWCookieBrowsingDataManager alloc]
            initWithBrowserState:browserState]);
    _browsingDataTypeMap.reset([@{
      @(web::BROWSING_DATA_TYPE_COOKIES) : cookieBrowsingDataManager,
    } retain]);
  }
  return self;
}

- (instancetype)init {
  NOTREACHED();
  return nil;
}

- (NSString*)description {
  NSString* format = @"<%@: %p; hasPendingOperations = { %@ }; mode = { %@ }>";
  NSString* hasPendingOperationsString =
      [self hasPendingOperations] ? @"YES" : @"NO";
  NSString* modeString = nil;
  switch (self.mode) {
    case web::ACTIVE:
      modeString = @"ACTIVE";
      break;
    case web::CHANGING:
      modeString = @"CHANGING";
      break;
    case web::INACTIVE:
      modeString = @"INACTIVE";
      break;
  }
  NSString* result = [NSString stringWithFormat:format,
      NSStringFromClass(self.class), self, hasPendingOperationsString,
      modeString];
  return result;
}

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString*)key {
  // It is necessary to override this for |mode| because the default KVO
  // behavior in NSObject is to fire a notification irrespective of if an actual
  // change was made to the ivar or not. The |mode| property needs fine grained
  // control over the actual notifications being fired since observers need to
  // be notified iff the |mode| actually changed.
  if ([key isEqual:@"mode"]) {
    return NO;
  }
  return [super automaticallyNotifiesObserversForKey:key];
}

#pragma mark -
#pragma mark Public Properties

- (id<CRWBrowsingDataStoreDelegate>)delegate {
  return _delegate;
}

- (void)setDelegate:(id<CRWBrowsingDataStoreDelegate>)delegate {
  _delegate.reset(delegate);
}

- (web::BrowsingDataStoreMode)mode {
  DCHECK([NSThread isMainThread]);
  return _mode;
}

- (BOOL)hasPendingOperations {
  if (!_lastDispatchedOperation) {
    return NO;
  }
  return ![_lastDispatchedOperation isFinished];
}

#pragma mark -
#pragma mark Public Methods

- (void)makeActiveWithCompletionHandler:
    (void (^)(BOOL success))completionHandler {
  DCHECK([NSThread isMainThread]);

  [self changeMode:web::ACTIVE completionHandler:completionHandler];
}

- (void)makeInactiveWithCompletionHandler:
    (void (^)(BOOL success))completionHandler {
  DCHECK([NSThread isMainThread]);

  [self changeMode:web::INACTIVE completionHandler:completionHandler];
}

- (void)removeDataOfTypes:(web::BrowsingDataTypes)browsingDataTypes
        completionHandler:(ProceduralBlock)completionHandler {
  DCHECK([NSThread isMainThread]);

  NSArray* browsingDataManagers =
      [self browsingDataManagersForBrowsingDataTypes:browsingDataTypes];
  [self performOperationWithType:REMOVE
            browsingDataManagers:browsingDataManagers
               completionHandler:completionHandler];
}

#pragma mark -
#pragma mark Private Properties

- (void)setMode:(web::BrowsingDataStoreMode)mode {
  DCHECK([NSThread isMainThread]);

  if (_mode == mode) {
    return;
  }
  if (mode == web::ACTIVE || mode == web::INACTIVE) {
    DCHECK(!self.numberOfPendingStashOrRestoreOperations);
  }
  [self willChangeValueForKey:@"mode"];
  _mode = mode;
  [self didChangeValueForKey:@"mode"];
}

- (NSArray*)allBrowsingDataManagers {
  DCHECK([NSThread isMainThread]);
  return [_browsingDataTypeMap allValues];
}

- (NSUInteger)numberOfPendingStashOrRestoreOperations {
  DCHECK([NSThread isMainThread]);
  return _numberOfPendingStashOrRestoreOperations;
}

- (void)setNumberOfPendingStashOrRestoreOperations:
    (NSUInteger)numberOfPendingStashOrRestoreOperations {
  DCHECK([NSThread isMainThread]);
  _numberOfPendingStashOrRestoreOperations =
      numberOfPendingStashOrRestoreOperations;
}

#pragma mark -
#pragma mark Private Class Methods

+ (NSOperationQueue*)operationQueueForStashAndRestoreOperations {
  static dispatch_once_t onceToken = 0;
  static NSOperationQueue* operationQueueForStashAndRestoreOperations = nil;
  dispatch_once(&onceToken, ^{
    operationQueueForStashAndRestoreOperations =
        [[NSOperationQueue alloc] init];
    [operationQueueForStashAndRestoreOperations
        setMaxConcurrentOperationCount:1U];
    if (base::ios::IsRunningOnIOS8OrLater()) {
      [operationQueueForStashAndRestoreOperations
          setQualityOfService:NSQualityOfServiceUserInitiated];
    }
  });
  return operationQueueForStashAndRestoreOperations;
}

+ (NSOperationQueue*)operationQueueForRemoveOperations {
  static dispatch_once_t onceToken = 0;
  static NSOperationQueue* operationQueueForRemoveOperations = nil;
  dispatch_once(&onceToken, ^{
    operationQueueForRemoveOperations = [[NSOperationQueue alloc] init];
    [operationQueueForRemoveOperations
        setMaxConcurrentOperationCount:NSUIntegerMax];
    if (base::ios::IsRunningOnIOS8OrLater()) {
      [operationQueueForRemoveOperations
          setQualityOfService:NSQualityOfServiceUserInitiated];
    }
  });
  return operationQueueForRemoveOperations;
}

#pragma mark -
#pragma mark Private Methods

- (NSArray*)browsingDataManagersForBrowsingDataTypes:
    (web::BrowsingDataTypes)browsingDataTypes {
  __block NSMutableArray* result = [NSMutableArray array];
  [_browsingDataTypeMap
      enumerateKeysAndObjectsUsingBlock:^(NSNumber* dataType,
                                          id<CRWBrowsingDataManager> manager,
                                          BOOL*) {
        if ([dataType unsignedIntegerValue] & browsingDataTypes) {
          [result addObject:manager];
        }
      }];
  return result;
}

- (SEL)browsingDataManagerSelectorForOperationType:
    (OperationType)operationType {
  switch (operationType) {
    case NONE:
      NOTREACHED();
      return nullptr;
    case STASH:
      return @selector(stashData);
    case RESTORE:
      return @selector(restoreData);
    case REMOVE:
      return [self browsingDataManagerSelectorForRemoveOperationType];
  };
}

- (SEL)browsingDataManagerSelectorForRemoveOperationType {
  if (self.mode == web::ACTIVE) {
    return @selector(removeDataAtCanonicalPath);
  }
  if (self.mode == web::INACTIVE) {
    return @selector(removeDataAtStashPath);
  }
  // Since the mode is |CHANGING|, find the last |STASH| or |RESTORE| operation
  // that was enqueued in order to find out the eventual mode that the
  // CRWBrowsingDataStore will be in when this |REMOVE| operation is run.
  DCHECK(_lastDispatchedStashOrRestoreOperation);
  NSString* lastDispatchedStashOrRestoreOperationName =
      [_lastDispatchedStashOrRestoreOperation name];
  if ([lastDispatchedStashOrRestoreOperationName
          isEqual:kRestoreOperationName]) {
    return @selector(removeDataAtCanonicalPath);
  }
  if ([lastDispatchedStashOrRestoreOperationName isEqual:kStashOperationName]) {
    return @selector(removeDataAtStashPath);
  }
  NOTREACHED();
  return nullptr;
}

- (BOOL)finalizeChangeToMode:(web::BrowsingDataStoreMode)mode {
  DCHECK([NSThread isMainThread]);
  DCHECK_NE(web::CHANGING, mode);

  BOOL modeChangeWasSuccessful = NO;
  if (!self.numberOfPendingStashOrRestoreOperations) {
    [self setMode:mode];
    modeChangeWasSuccessful = YES;
  }
  return modeChangeWasSuccessful;
}

- (void)changeMode:(web::BrowsingDataStoreMode)mode
    completionHandler:
        (void (^)(BOOL modeChangeWasSuccessful))completionHandler {
  DCHECK([NSThread isMainThread]);

  ProceduralBlock completionHandlerAfterPerformingOperation = ^{
    BOOL modeChangeWasSuccessful = [self finalizeChangeToMode:mode];
    if (completionHandler) {
      completionHandler(modeChangeWasSuccessful);
    }
  };

  OperationType operationType = [self operationTypeToChangeMode:mode];
  if (operationType == NONE) {
    // As a caller of this API, it is awkward to get the callback before the
    // method call has completed, hence defer it.
    dispatch_async(dispatch_get_main_queue(),
                   completionHandlerAfterPerformingOperation);
  } else {
    [self performOperationWithType:operationType
              browsingDataManagers:[self allBrowsingDataManagers]
                 completionHandler:completionHandlerAfterPerformingOperation];
  }
}

- (OperationType)operationTypeToChangeMode:(web::BrowsingDataStoreMode)mode {
  DCHECK_NE(web::CHANGING, mode);

  OperationType operationType = NONE;
  if (mode == self.mode) {
    // Already in the desired mode.
    operationType = NONE;
  } else if (mode == web::ACTIVE) {
    // By default a |RESTORE| operation is performed when the mode is changed
    // to |ACTIVE|.
    operationType = RESTORE;
    web::BrowsingDataStoreMakeActivePolicy makeActivePolicy =
        [_delegate decideMakeActiveOperationPolicyForBrowsingDataStore:self];
    operationType = (makeActivePolicy == web::ADOPT) ? REMOVE : RESTORE;
  } else {
    // By default a |STASH| operation is performed when the mode is changed to
    // |INACTIVE|.
    operationType = STASH;
    web::BrowsingDataStoreMakeInactivePolicy makeInactivePolicy =
        [_delegate decideMakeInactiveOperationPolicyForBrowsingDataStore:self];
    operationType = (makeInactivePolicy == web::DELETE) ? REMOVE : STASH;
  }
  return operationType;
}

- (void)performOperationWithType:(OperationType)operationType
            browsingDataManagers:(NSArray*)browsingDataManagers
               completionHandler:(ProceduralBlock)completionHandler {
  DCHECK([NSThread isMainThread]);
  DCHECK(completionHandler);
  DCHECK_NE(NONE, operationType);

  SEL selector =
      [self browsingDataManagerSelectorForOperationType:operationType];
  DCHECK(selector);

  if (operationType == RESTORE || operationType == STASH) {
    [self setMode:web::CHANGING];
    ++self.numberOfPendingStashOrRestoreOperations;
    completionHandler = ^{
      --self.numberOfPendingStashOrRestoreOperations;
      // It is safe to this and does not lead to the block (|completionHandler|)
      // retaining itself.
      completionHandler();
    };
  }

  ProceduralBlock callCompletionHandlerOnMainThread = ^{
    // This is called on a background thread, hence the need to bounce to the
    // main thread.
    dispatch_async(dispatch_get_main_queue(), completionHandler);
  };
  NSOperation* operation =
      [self operationWithBrowsingDataManagers:browsingDataManagers
                                     selector:selector];

  if (operationType == RESTORE || operationType == STASH) {
    [operation setName:(RESTORE ? kRestoreOperationName : kStashOperationName)];
    _lastDispatchedStashOrRestoreOperation.reset([operation retain]);
  }

  NSOperationQueue* queue = nil;
  switch (operationType) {
    case NONE:
      NOTREACHED();
      break;
    case STASH:
    case RESTORE:
      queue = [CRWBrowsingDataStore operationQueueForStashAndRestoreOperations];
      break;
    case REMOVE:
      queue = [CRWBrowsingDataStore operationQueueForRemoveOperations];
      break;
  }

  NSOperation* completionHandlerOperation = [NSBlockOperation
      blockOperationWithBlock:callCompletionHandlerOnMainThread];

  [self addOperation:operation toQueue:queue];
  [self addOperation:completionHandlerOperation toQueue:queue];
}

- (NSOperation*)operationWithBrowsingDataManagers:(NSArray*)browsingDataManagers
                                         selector:(SEL)selector {
  NSBlockOperation* operation = [[[NSBlockOperation alloc] init] autorelease];
  for (id<CRWBrowsingDataManager> manager : browsingDataManagers) {
    // |addExecutionBlock| farms out the different blocks added to it. hence the
    // operations are implicitly parallelized.
    [operation addExecutionBlock:^{
      [manager performSelector:selector];
    }];
  }
  return operation;
}

- (void)addOperation:(NSOperation*)operation toQueue:(NSOperationQueue*)queue {
  DCHECK([NSThread isMainThread]);
  DCHECK(operation);
  DCHECK(queue);

  if (_lastDispatchedOperation) {
    [operation addDependency:_lastDispatchedOperation];
  }
  _lastDispatchedOperation.reset([operation retain]);
  [queue addOperation:operation];
}

@end