/*
 * Copyright 2017 Google
 *
 * 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 "FirebaseDatabase/Sources/Core/Utilities/FIRRetryHelper.h"
#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
#import "FirebaseDatabase/Sources/Utilities/FUtilities.h"

@interface FIRRetryHelperTask : NSObject

@property(nonatomic, strong) void (^block)(void);

@end

@implementation FIRRetryHelperTask

- (instancetype)initWithBlock:(void (^)(void))block {
    self = [super init];
    if (self != nil) {
        self->_block = [block copy];
    }
    return self;
}

- (BOOL)isCanceled {
    return self.block == nil;
}

- (void)cancel {
    self.block = nil;
}

- (void)execute {
    if (self.block) {
        self.block();
    }
}

@end

@interface FIRRetryHelper ()

@property(nonatomic, strong) dispatch_queue_t dispatchQueue;
@property(nonatomic) NSTimeInterval minRetryDelayAfterFailure;
@property(nonatomic) NSTimeInterval maxRetryDelay;
@property(nonatomic) double retryExponent;
@property(nonatomic) double jitterFactor;

@property(nonatomic) BOOL lastWasSuccess;
@property(nonatomic) NSTimeInterval currentRetryDelay;

@property(nonatomic, strong) FIRRetryHelperTask *scheduledRetry;

@end

@implementation FIRRetryHelper

- (instancetype)initWithDispatchQueue:(dispatch_queue_t)dispatchQueue
            minRetryDelayAfterFailure:(NSTimeInterval)minRetryDelayAfterFailure
                        maxRetryDelay:(NSTimeInterval)maxRetryDelay
                        retryExponent:(double)retryExponent
                         jitterFactor:(double)jitterFactor {
    self = [super init];
    if (self != nil) {
        self->_dispatchQueue = dispatchQueue;
        self->_minRetryDelayAfterFailure = minRetryDelayAfterFailure;
        self->_maxRetryDelay = maxRetryDelay;
        self->_retryExponent = retryExponent;
        self->_jitterFactor = jitterFactor;
        self->_lastWasSuccess = YES;
    }
    return self;
}

- (void)retry:(void (^)(void))block {
    if (self.scheduledRetry != nil) {
        FFLog(@"I-RDB054001", @"Canceling existing retry attempt");
        [self.scheduledRetry cancel];
        self.scheduledRetry = nil;
    }

    NSTimeInterval delay;
    if (self.lastWasSuccess) {
        delay = 0;
    } else {
        if (self.currentRetryDelay == 0) {
            self.currentRetryDelay = self.minRetryDelayAfterFailure;
        } else {
            NSTimeInterval newDelay =
                (self.currentRetryDelay * self.retryExponent);
            self.currentRetryDelay = MIN(newDelay, self.maxRetryDelay);
        }

        delay = ((1 - self.jitterFactor) * self.currentRetryDelay) +
                (self.jitterFactor * self.currentRetryDelay *
                 [FUtilities randomDouble]);
        FFLog(@"I-RDB054002", @"Scheduling retry in %fs", delay);
    }
    self.lastWasSuccess = NO;
    FIRRetryHelperTask *task = [[FIRRetryHelperTask alloc] initWithBlock:block];
    self.scheduledRetry = task;
    dispatch_time_t popTime =
        dispatch_time(DISPATCH_TIME_NOW, (long long)(delay * NSEC_PER_SEC));
    dispatch_after(popTime, self.dispatchQueue, ^{
      if (![task isCanceled]) {
          self.scheduledRetry = nil;
          [task execute];
      }
    });
}

- (void)signalSuccess {
    self.lastWasSuccess = YES;
    self.currentRetryDelay = 0;
}

- (void)cancel {
    if (self.scheduledRetry != nil) {
        FFLog(@"I-RDB054003", @"Canceling existing retry attempt");
        [self.scheduledRetry cancel];
        self.scheduledRetry = nil;
    } else {
        FFLog(@"I-RDB054004", @"No existing retry attempt to cancel");
    }
    self.currentRetryDelay = 0;
}

@end
