/*
 * Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#import "FirebaseInstallations/Source/Library/InstallationsIDController/FIRInstallationsBackoffController.h"

static const NSTimeInterval k24Hours = 24 * 60 * 60;
static const NSTimeInterval k30Minutes = 30 * 60;

/** The class represents `FIRInstallationsBackoffController` state required to calculate next
 allowed request time. The properties of the class are intentionally immutable because changing them
 separately leads to an inconsistent state. */
@interface FIRInstallationsBackoffEventData : NSObject

@property(nonatomic, readonly) FIRInstallationsBackoffEvent eventType;
@property(nonatomic, readonly) NSDate *lastEventDate;
@property(nonatomic, readonly) NSInteger eventCount;

@property(nonatomic, readonly) NSTimeInterval backoffTimeInterval;

@end

@implementation FIRInstallationsBackoffEventData

- (instancetype)initWithEvent:(FIRInstallationsBackoffEvent)eventType
                lastEventDate:(NSDate *)lastEventDate
                   eventCount:(NSInteger)eventCount {
  self = [super init];
  if (self) {
    _eventType = eventType;
    _lastEventDate = lastEventDate;
    _eventCount = eventCount;

    _backoffTimeInterval = [[self class] backoffTimeIntervalWithEvent:eventType
                                                           eventCount:eventCount];
  }
  return self;
}

+ (NSTimeInterval)backoffTimeIntervalWithEvent:(FIRInstallationsBackoffEvent)eventType
                                    eventCount:(NSInteger)eventCount {
  switch (eventType) {
    case FIRInstallationsBackoffEventSuccess:
      return 0;
      break;

    case FIRInstallationsBackoffEventRecoverableFailure:
      return [self recoverableErrorBackoffTimeForAttemptNumber:eventCount];
      break;

    case FIRInstallationsBackoffEventUnrecoverableFailure:
      return k24Hours;
      break;
  }
}

+ (NSTimeInterval)recoverableErrorBackoffTimeForAttemptNumber:(NSInteger)attemptNumber {
  NSTimeInterval exponentialInterval = pow(2, attemptNumber) + [self randomMilliseconds];
  return MIN(exponentialInterval, k30Minutes);
}

+ (NSTimeInterval)randomMilliseconds {
  int32_t random_millis = ABS(arc4random() % 1000);
  return (double)random_millis * 0.001;
}

@end

@interface FIRInstallationsBackoffController ()

@property(nonatomic, readonly) FIRCurrentDateProvider currentDateProvider;

@property(nonatomic, nullable) FIRInstallationsBackoffEventData *lastEventData;

@end

@implementation FIRInstallationsBackoffController

- (instancetype)init {
  return [self initWithCurrentDateProvider:FIRRealCurrentDateProvider()];
}

- (instancetype)initWithCurrentDateProvider:(FIRCurrentDateProvider)currentDateProvider {
  self = [super init];
  if (self) {
    _currentDateProvider = [currentDateProvider copy];
  }
  return self;
}

- (BOOL)isNextRequestAllowed {
  @synchronized(self) {
    if (self.lastEventData == nil) {
      return YES;
    }

    NSTimeInterval timeSinceLastEvent =
        [self.currentDateProvider() timeIntervalSinceDate:self.lastEventData.lastEventDate];
    return timeSinceLastEvent >= self.lastEventData.backoffTimeInterval;
  }
}

- (void)registerEvent:(FIRInstallationsBackoffEvent)event {
  @synchronized(self) {
    // Event of the same type as was registered before.
    if (self.lastEventData && self.lastEventData.eventType == event) {
      self.lastEventData = [[FIRInstallationsBackoffEventData alloc]
          initWithEvent:event
          lastEventDate:self.currentDateProvider()
             eventCount:self.lastEventData.eventCount + 1];
    } else {  // A different event.
      self.lastEventData =
          [[FIRInstallationsBackoffEventData alloc] initWithEvent:event
                                                    lastEventDate:self.currentDateProvider()
                                                       eventCount:1];
    }
  }
}

@end
