//
//  Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//

#import "OWSDevice.h"
#import "OWSError.h"
#import "OWSPrimaryStorage.h"
#import "ProfileManagerProtocol.h"
#import "SSKEnvironment.h"
#import "TSAccountManager.h"
#import "YapDatabaseConnection+OWS.h"
#import "YapDatabaseConnection.h"
#import "YapDatabaseTransaction.h"
#import <Mantle/MTLValueTransformer.h>
#import <SessionCoreKit/NSDate+OWS.h>
#import <SessionServiceKit/OWSIdentityManager.h>
#import <SessionServiceKit/SessionServiceKit-Swift.h>

NS_ASSUME_NONNULL_BEGIN

uint32_t const OWSDevicePrimaryDeviceId = 1;
NSString *const kOWSPrimaryStorage_OWSDeviceCollection = @"kTSStorageManager_OWSDeviceCollection";
NSString *const kOWSPrimaryStorage_MayHaveLinkedDevices = @"kTSStorageManager_MayHaveLinkedDevices";

@interface OWSDeviceManager ()

@property (atomic) NSDate *lastReceivedSyncMessage;

@end

#pragma mark -

@implementation OWSDeviceManager

+ (instancetype)sharedManager
{
    static OWSDeviceManager *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] initDefault];
    });
    return instance;
}

- (instancetype)initDefault
{
    return [super init];
}

- (BOOL)mayHaveLinkedDevices:(YapDatabaseConnection *)dbConnection
{
    OWSAssertDebug(dbConnection);

    return [dbConnection boolForKey:kOWSPrimaryStorage_MayHaveLinkedDevices
                       inCollection:kOWSPrimaryStorage_OWSDeviceCollection
                       defaultValue:YES];
}

// In order to avoid skipping necessary sync messages, the default value
// for mayHaveLinkedDevices is YES.  Once we've successfully sent a
// sync message with no device messages (e.g. the service has confirmed
// that we have no linked devices), we can set mayHaveLinkedDevices to NO
// to avoid unnecessary message sends for sync messages until we learn
// of a linked device (e.g. through the device linking UI or by receiving
// a sync message, etc.).
- (void)clearMayHaveLinkedDevices
{
    // Note that we write async to avoid opening transactions within transactions.
    [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
        [transaction setObject:@(NO)
                        forKey:kOWSPrimaryStorage_MayHaveLinkedDevices
                  inCollection:kOWSPrimaryStorage_OWSDeviceCollection];
    }];
}

- (void)setMayHaveLinkedDevices
{
    // Note that we write async to avoid opening transactions within transactions.
    [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
        [transaction setObject:@(YES)
                        forKey:kOWSPrimaryStorage_MayHaveLinkedDevices
                  inCollection:kOWSPrimaryStorage_OWSDeviceCollection];
    }];
}

- (BOOL)hasReceivedSyncMessageInLastSeconds:(NSTimeInterval)intervalSeconds
{
    return (self.lastReceivedSyncMessage && fabs(self.lastReceivedSyncMessage.timeIntervalSinceNow) < intervalSeconds);
}

- (void)setHasReceivedSyncMessage
{
    self.lastReceivedSyncMessage = [NSDate new];

    [self setMayHaveLinkedDevices];
}

@end

#pragma mark -

@interface OWSDevice ()

@property (nonatomic) NSInteger deviceId;
@property (nonatomic, nullable) NSString *name;
@property (nonatomic) NSDate *createdAt;
@property (nonatomic) NSDate *lastSeenAt;

@end

#pragma mark -

@implementation OWSDevice

#pragma mark - Dependencies

+ (id<ProfileManagerProtocol>)profileManager
{
    return SSKEnvironment.shared.profileManager;
}

+ (id<OWSUDManager>)udManager
{
    return SSKEnvironment.shared.udManager;
}

+ (TSAccountManager *)tsAccountManager
{
    return TSAccountManager.sharedInstance;
}

- (OWSIdentityManager *)identityManager
{
    OWSAssertDebug(SSKEnvironment.shared.identityManager);

    return SSKEnvironment.shared.identityManager;
}

#pragma mark -

- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
    [super saveWithTransaction:transaction];
}

+ (nullable instancetype)deviceFromJSONDictionary:(NSDictionary *)deviceAttributes error:(NSError **)error
{
    OWSDevice *device = [MTLJSONAdapter modelOfClass:[self class] fromJSONDictionary:deviceAttributes error:error];
    if (device.deviceId < OWSDevicePrimaryDeviceId) {
        OWSFailDebug(@"Invalid device id: %lu", (unsigned long)device.deviceId);
        *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecodeJson, @"Invalid device id.");
        return nil;
    }
    return device;
}

+ (NSDictionary *)JSONKeyPathsByPropertyKey
{
    return @{
             @"createdAt": @"created",
             @"lastSeenAt": @"lastSeen",
             @"deviceId": @"id",
             @"name": @"name"
             };
}

+ (MTLValueTransformer *)createdAtJSONTransformer
{
    return self.millisecondTimestampToDateTransformer;
}

+ (MTLValueTransformer *)lastSeenAtJSONTransformer
{
    return self.millisecondTimestampToDateTransformer;
}

+ (NSArray<OWSDevice *> *)currentDevicesWithTransaction:(YapDatabaseReadTransaction *)transaction
{
    OWSAssertDebug(transaction);

    NSMutableArray<OWSDevice *> *result = [NSMutableArray new];
    [transaction enumerateKeysAndObjectsInCollection:OWSDevice.collection
                                          usingBlock:^(NSString *key, OWSDevice *object, BOOL *stop) {
                                              if (![object isKindOfClass:[OWSDevice class]]) {
                                                  OWSFailDebug(@"Unexpected object in collection: %@", object.class);
                                                  return;
                                              }
                                              [result addObject:object];
                                          }];
    return result;
}

+ (BOOL)replaceAll:(NSArray<OWSDevice *> *)currentDevices
{
    BOOL didAddOrRemove = NO;
    NSMutableArray<OWSDevice *> *existingDevices = [[self allObjectsInCollection] mutableCopy];
    for (OWSDevice *currentDevice in currentDevices) {
        NSUInteger existingDeviceIndex = [existingDevices indexOfObject:currentDevice];
        if (existingDeviceIndex == NSNotFound) {
            // New Device
            OWSLogInfo(@"Adding device: %@", currentDevice);
            [currentDevice save];
            didAddOrRemove = YES;
        } else {
            OWSDevice *existingDevice = existingDevices[existingDeviceIndex];
            if ([existingDevice updateAttributesWithDevice:currentDevice]) {
                [existingDevice save];
            }
            [existingDevices removeObjectAtIndex:existingDeviceIndex];
        }
    }

    // Since we removed existing devices as we went, only stale devices remain
    for (OWSDevice *staleDevice in existingDevices) {
        OWSLogVerbose(@"Removing device: %@", staleDevice);
        [staleDevice remove];
        didAddOrRemove = YES;
    }

    if (didAddOrRemove) {
        dispatch_async(dispatch_get_main_queue(), ^{
            // Device changes can affect the UD access mode for a recipient,
            // so we need to fetch the profile for this user to update UD access mode.
            [self.profileManager fetchLocalUsersProfile];
        });
        return YES;
    } else {
        return NO;
    }
}

+ (MTLValueTransformer *)millisecondTimestampToDateTransformer
{
    static MTLValueTransformer *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [MTLValueTransformer transformerUsingForwardBlock:^id(id value, BOOL *success, NSError **error) {
            if ([value isKindOfClass:[NSNumber class]]) {
                NSNumber *number = (NSNumber *)value;
                NSDate *result = [NSDate ows_dateWithMillisecondsSince1970:[number longLongValue]];
                if (result) {
                    *success = YES;
                    return result;
                }
            }
            *success = NO;
            OWSLogError(@"unable to decode date from %@", value);
            *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecodeJson, @"Unable to decode date from JSON.");
            return nil;
        }
            reverseBlock:^id(id value, BOOL *success, NSError **error) {
                if ([value isKindOfClass:[NSDate class]]) {
                    NSDate *date = (NSDate *)value;
                    NSNumber *result = [NSNumber numberWithLongLong:[NSDate ows_millisecondsSince1970ForDate:date]];
                    if (result) {
                        *success = YES;
                        return result;
                    }
                }
                OWSLogError(@"unable to encode date from %@", value);
                *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToEncodeJson, @"Unable to encode date to JSON.");
                *success = NO;
                return nil;
            }];
    });
    return instance;
}

+ (uint32_t)currentDeviceId
{
    // Someday it may be possible to have a non-primary iOS device, but for now
    // any iOS device must be the primary device.
    return OWSDevicePrimaryDeviceId;
}

- (BOOL)isPrimaryDevice
{
    return self.deviceId == OWSDevicePrimaryDeviceId;
}

- (NSString *)displayName
{
    if (self.name) {
        ECKeyPair *_Nullable identityKeyPair = self.identityManager.identityKeyPair;
        OWSAssertDebug(identityKeyPair);
        if (identityKeyPair) {
            NSError *error;
            NSString *_Nullable decryptedName =
                [DeviceNames decryptDeviceNameWithBase64String:self.name identityKeyPair:identityKeyPair error:&error];
            if (error) {
                // Not necessarily an error; might be a legacy device name.
                OWSLogError(@"Could not decrypt device name: %@", error);
            } else if (decryptedName) {
                return decryptedName;
            }
        }

        return self.name;
    }

    if (self.deviceId == OWSDevicePrimaryDeviceId) {
        return @"This Device";
    }
    return NSLocalizedString(@"UNNAMED_DEVICE", @"Label text in device manager for a device with no name");
}

- (BOOL)updateAttributesWithDevice:(OWSDevice *)other
{
    BOOL changed = NO;
    if (![self.lastSeenAt isEqual:other.lastSeenAt]) {
        self.lastSeenAt = other.lastSeenAt;
        changed = YES;
    }

    if (![self.createdAt isEqual:other.createdAt]) {
        self.createdAt = other.createdAt;
        changed = YES;
    }

    if (![self.name isEqual:other.name]) {
        self.name = other.name;
        changed = YES;
    }

    return changed;
}

+ (BOOL)hasSecondaryDevicesWithTransaction:(YapDatabaseReadTransaction *)transaction
{
    return [self numberOfKeysInCollectionWithTransaction:transaction] > 1;
}

- (BOOL)isEqual:(id)object
{
    if (self == object) {
        return YES;
    }

    if (![object isKindOfClass:[OWSDevice class]]) {
        return NO;
    }

    return [self isEqualToDevice:(OWSDevice *)object];
}

- (BOOL)isEqualToDevice:(OWSDevice *)device
{
    return self.deviceId == device.deviceId;
}

@end

NS_ASSUME_NONNULL_END
