/*
 * Copyright (c) 2016 Apple Inc. All Rights Reserved.
 *
 * @APPLE_LICENSE_HEADER_START@
 *
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Apple Public Source 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://www.opensource.apple.com/apsl/ 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.
 *
 * @APPLE_LICENSE_HEADER_END@
 */

#import "keychain/ckks/CKKSResultOperation.h"
#import "keychain/ckks/CKKSCondition.h"
#include <utilities/debugging.h>

@interface CKKSResultOperation()
@property NSMutableArray<CKKSResultOperation*>* successDependencies;
@property bool timeoutCanOccur;
@property dispatch_queue_t timeoutQueue;
@property void (^finishingBlock)(void);
@end

@implementation CKKSResultOperation
- (instancetype)init {
    if(self = [super init]) {
        _error = nil;
        _successDependencies = [[NSMutableArray alloc] init];
        _timeoutCanOccur = true;
        _timeoutQueue = dispatch_queue_create("result-operation-timeout", DISPATCH_QUEUE_SERIAL);
        _completionHandlerDidRunCondition = [[CKKSCondition alloc] init];

        __weak __typeof(self) weakSelf = self;
        _finishingBlock = ^(void) {
            weakSelf.finishDate = [NSDate dateWithTimeIntervalSinceNow:0];
        };
        self.completionBlock = ^{}; // our _finishing block gets added in the method override
    }
    return self;
}

- (NSString*)description {
    NSString* state = ([self isFinished] ? [NSString stringWithFormat:@"finished %@", self.finishDate] :
                       [self isCancelled] ? @"cancelled" :
                       [self isExecuting] ? @"executing" :
                       [self isReady] ? @"ready" :
                       @"pending");

    if(self.error) {
        return [NSString stringWithFormat: @"<%@: %@ error:%@>", [self selfname], state, self.error];
    } else {
        return [NSString stringWithFormat: @"<%@: %@%@>", [self selfname], state, [self pendingDependenciesString:@" dep:"]];
    }
}

- (NSString*)debugDescription {
    return [self description];
}

- (void)setCompletionBlock:(void (^)(void))completionBlock
{
    __weak __typeof(self) weakSelf = self;
    [super setCompletionBlock:^(void) {
        __strong __typeof(self) strongSelf = weakSelf;
        if (!strongSelf) {
            secerror("ckksresultoperation: completion handler called on deallocated operation instance");
            completionBlock(); // go ahead and still behave as things would if this method override were not here
            return;
        }

        strongSelf.finishingBlock();
        completionBlock();
        [strongSelf.completionHandlerDidRunCondition fulfill];
    }];
}

- (void)start {
    if(![self allDependentsSuccessful]) {
        secdebug("ckksresultoperation", "Not running due to some failed dependent: %@", self.error);
        [self cancel];
    } else {
        [self invalidateTimeout];

    }

    [super start];
}

- (void)invalidateTimeout {
    dispatch_sync(self.timeoutQueue, ^{
        if(![self isCancelled]) {
            self.timeoutCanOccur = false;
        };
    });
}

- (instancetype)timeout:(dispatch_time_t)timeout {
    __weak __typeof(self) weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, timeout), self.timeoutQueue, ^{
        __strong __typeof(self) strongSelf = weakSelf;
        if(strongSelf.timeoutCanOccur) {
            strongSelf.error = [NSError errorWithDomain:CKKSResultErrorDomain code: CKKSResultTimedOut userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat:@"Operation timed out waiting to start for [%@]", [self pendingDependenciesString:@""]]}];
            strongSelf.timeoutCanOccur = false;
            [strongSelf cancel];
        }
    });

    return self;
}

- (void)addSuccessDependency:(CKKSResultOperation *)operation {
    [self addNullableSuccessDependency:operation];
}

- (void)addNullableSuccessDependency:(CKKSResultOperation *)operation {
    if(!operation) {
        return;
    }
    @synchronized(self) {
        [self.successDependencies addObject: operation];
        [self addDependency: operation];
    }
}

- (bool)allDependentsSuccessful {
    return [self allSuccessful: self.successDependencies];
}

- (bool)allSuccessful: (NSArray<CKKSResultOperation*>*) operations {
    @synchronized(self) {
        bool result = false;

        bool finished = true;   // all dependents must be finished
        bool cancelled = false; // no dependents can be cancelled
        bool failed = false;    // no dependents can have failed

        for(CKKSResultOperation* op in operations) {
            finished  &= !!([op isFinished]);
            cancelled |= !!([op isCancelled]);
            failed    |= (op.error != nil);

            // TODO: combine suberrors
            if(op.error != nil) {
                if([op.error.domain isEqual: CKKSResultErrorDomain] && op.error.code == CKKSResultSubresultError) {
                    // Already a subresult, just copy it on in
                    self.error = op.error;
                } else {
                    self.error = [NSError errorWithDomain:CKKSResultErrorDomain code: CKKSResultSubresultError userInfo:@{ NSUnderlyingErrorKey: op.error}];
                }
            }
        }

        result = finished && !( cancelled || failed );

        if(!result && self.error == nil) {
            self.error = [NSError errorWithDomain:CKKSResultErrorDomain code: CKKSResultSubresultCancelled userInfo:nil];
        }
        return result;
    }
}

+ (CKKSResultOperation*)operationWithBlock:(void (^)(void))block {
    CKKSResultOperation* op = [[CKKSResultOperation alloc] init];
    [op addExecutionBlock: block];
    return op;
}

+(instancetype)named:(NSString*)name withBlock:(void(^)(void)) block {
    CKKSResultOperation* blockOp = [CKKSResultOperation operationWithBlock: block];
    blockOp.name = name;
    return blockOp;
}
@end
