/*
 * 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 <TargetConditionals.h>
#if TARGET_OS_IOS || TARGET_OS_TV

#import "FirebaseCore/Extension/FirebaseCoreInternal.h"

#import "FirebaseInAppMessaging/Sources/FIRCore+InAppMessaging.h"
#import "FirebaseInAppMessaging/Sources/Private/Flows/FIRIAMBookKeeper.h"

NSString *const FIRIAM_UserDefaultsKeyForImpressions = @"firebase-iam-message-impressions";
NSString *const FIRIAM_UserDefaultsKeyForLastImpressionTimestamp =
    @"firebase-iam-last-impression-timestamp";
NSString *FIRIAM_UserDefaultsKeyForLastFetchTimestamp = @"firebase-iam-last-fetch-timestamp";

// The two keys used to map FIRIAMImpressionRecord object to a NSDictionary object for
// persistence.
NSString *const FIRIAM_ImpressionDictKeyForID = @"message_id";
NSString *const FIRIAM_ImpressionDictKeyForTimestamp = @"impression_time";

static NSString *const kUserDefaultsKeyForFetchWaitTime = @"firebase-iam-fetch-wait-time";

// 24 hours
static NSTimeInterval kDefaultFetchWaitTimeInSeconds = 24 * 60 * 60;

// 3 days
static NSTimeInterval kMaxFetchWaitTimeInSeconds = 3 * 24 * 60 * 60;

@interface FIRIAMBookKeeperViaUserDefaults ()
@property(nonatomic) double lastDisplayTime;
@property(nonatomic) double lastFetchTime;
@property(nonatomic) double nextFetchWaitTime;
@property(nonatomic, nonnull) NSUserDefaults *defaults;
@end

@interface FIRIAMImpressionRecord ()
- (instancetype)initWithStorageDictionary:(NSDictionary *)dict;
@end

@implementation FIRIAMImpressionRecord

- (instancetype)initWithMessageID:(NSString *)messageID
          impressionTimeInSeconds:(long)impressionTime {
  if (self = [super init]) {
    _messageID = messageID;
    _impressionTimeInSeconds = impressionTime;
  }
  return self;
}

- (instancetype)initWithStorageDictionary:(NSDictionary *)dict {
  id timestamp = dict[FIRIAM_ImpressionDictKeyForTimestamp];
  id messageID = dict[FIRIAM_ImpressionDictKeyForID];

  if (![timestamp isKindOfClass:[NSNumber class]] || ![messageID isKindOfClass:[NSString class]]) {
    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270003",
                @"Incorrect data in the dictionary object for creating a FIRIAMImpressionRecord"
                 " object");
    return nil;
  } else {
    return [self initWithMessageID:messageID
           impressionTimeInSeconds:((NSNumber *)timestamp).longValue];
  }
}

- (NSString *)description {
  return [NSString stringWithFormat:@"%@ impressed at %ld in seconds", self.messageID,
                                    self.impressionTimeInSeconds];
}
@end

@implementation FIRIAMBookKeeperViaUserDefaults

- (instancetype)initWithUserDefaults:(NSUserDefaults *)userDefaults {
  if (self = [super init]) {
    _defaults = userDefaults;

    // ok if it returns 0 due to the entry being absent
    _lastDisplayTime = [_defaults doubleForKey:FIRIAM_UserDefaultsKeyForLastImpressionTimestamp];
    _lastFetchTime = [_defaults doubleForKey:FIRIAM_UserDefaultsKeyForLastFetchTimestamp];

    id fetchWaitTimeEntry = [_defaults objectForKey:kUserDefaultsKeyForFetchWaitTime];

    if (![fetchWaitTimeEntry isKindOfClass:NSNumber.class]) {
      // This corresponds to the case there is no wait time entry is set in user defaults yet
      _nextFetchWaitTime = kDefaultFetchWaitTimeInSeconds;
    } else {
      _nextFetchWaitTime = ((NSNumber *)fetchWaitTimeEntry).doubleValue;
      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270009",
                  @"Next fetch wait time loaded from user defaults is %lf", _nextFetchWaitTime);
    }
  }
  return self;
}

// A helper function for reading and verifying the stored array data for impressions
// in UserDefaults. It returns nil if it does not exist or fail to pass the data type
// checking.
- (NSArray *)fetchImpressionArrayFromStorage {
  id impressionsData = [self.defaults objectForKey:FIRIAM_UserDefaultsKeyForImpressions];

  if (impressionsData && ![impressionsData isKindOfClass:[NSArray class]]) {
    FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM270007",
                  @"Found non-array data from impression userdefaults storage with key %@",
                  FIRIAM_UserDefaultsKeyForImpressions);
    return nil;
  }
  return (NSArray *)impressionsData;
}

- (void)recordNewImpressionForMessage:(NSString *)messageID
          withStartTimestampInSeconds:(double)timestamp {
  @synchronized(self) {
    NSArray *oldImpressions = [self fetchImpressionArrayFromStorage];
    // oldImpressions could be nil at the first time
    NSMutableArray *newImpressions =
        oldImpressions ? [oldImpressions mutableCopy] : [[NSMutableArray alloc] init];

    // Two cases
    //    If a prior impression exists for that messageID, update its impression timestamp
    //    If a prior impression for that messageID does not exist, add a new entry for the
    //    messageID.

    NSDictionary *newImpressionEntry = @{
      FIRIAM_ImpressionDictKeyForID : messageID,
      FIRIAM_ImpressionDictKeyForTimestamp : [NSNumber numberWithDouble:timestamp]
    };

    BOOL oldImpressionRecordFound = NO;

    for (int i = 0; i < newImpressions.count; i++) {
      if ([newImpressions[i] isKindOfClass:[NSDictionary class]]) {
        NSDictionary *currentItem = (NSDictionary *)newImpressions[i];
        if ([messageID isEqualToString:currentItem[FIRIAM_ImpressionDictKeyForID]]) {
          FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270001",
                      @"Updating timestamp of existing impression record to be %f for "
                       "message %@",
                      timestamp, messageID);

          [newImpressions replaceObjectAtIndex:i withObject:newImpressionEntry];
          oldImpressionRecordFound = YES;
          break;
        }
      }
    }

    if (!oldImpressionRecordFound) {
      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270002",
                  @"Insert the first impression record for message %@ with timestamp in seconds "
                   "as %f",
                  messageID, timestamp);
      [newImpressions addObject:newImpressionEntry];
    }

    [self.defaults setObject:newImpressions forKey:FIRIAM_UserDefaultsKeyForImpressions];
    [self.defaults setDouble:timestamp forKey:FIRIAM_UserDefaultsKeyForLastImpressionTimestamp];
    self.lastDisplayTime = timestamp;
  }
}

- (void)clearImpressionsWithMessageList:(NSArray<NSString *> *)messageList {
  @synchronized(self) {
    NSArray *existingImpressions = [self fetchImpressionArrayFromStorage];

    NSSet<NSString *> *messageIDSet = [NSSet setWithArray:messageList];
    NSPredicate *notInMessageListPredicate =
        [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
          if (![evaluatedObject isKindOfClass:[NSDictionary class]]) {
            return NO;  // unexpected item. Throw it away
          }
          NSDictionary *impression = (NSDictionary *)evaluatedObject;
          return impression[FIRIAM_ImpressionDictKeyForID] &&
                 ![messageIDSet containsObject:impression[FIRIAM_ImpressionDictKeyForID]];
        }];

    NSArray<NSDictionary *> *updatedImpressions =
        [existingImpressions filteredArrayUsingPredicate:notInMessageListPredicate];

    if (existingImpressions.count != updatedImpressions.count) {
      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270004",
                  @"Updating the impression records after purging %d items based on the "
                   "server fetch response",
                  (int)(existingImpressions.count - updatedImpressions.count));
      [self.defaults setObject:updatedImpressions forKey:FIRIAM_UserDefaultsKeyForImpressions];
    } else {
      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270005",
                  @"No impression records update due to no change after applying the server "
                   "message list");
    }
  }
}

- (NSArray<FIRIAMImpressionRecord *> *)getImpressions {
  NSArray<NSDictionary *> *impressionsFromStorage = [self fetchImpressionArrayFromStorage];

  NSMutableArray<FIRIAMImpressionRecord *> *resultArray = [[NSMutableArray alloc] init];

  for (NSDictionary *next in impressionsFromStorage) {
    FIRIAMImpressionRecord *nextImpression =
        [[FIRIAMImpressionRecord alloc] initWithStorageDictionary:next];
    [resultArray addObject:nextImpression];
  }

  return resultArray;
}

- (NSArray<NSString *> *)getMessageIDsFromImpressions {
  NSArray<NSDictionary *> *impressionsFromStorage = [self fetchImpressionArrayFromStorage];

  NSMutableArray<NSString *> *resultArray = [[NSMutableArray alloc] init];

  for (NSDictionary *next in impressionsFromStorage) {
    [resultArray addObject:next[FIRIAM_ImpressionDictKeyForID]];
  }

  return resultArray;
}

- (void)recordNewFetchWithFetchCount:(NSInteger)fetchedMsgCount
              withTimestampInSeconds:(double)fetchTimestamp
                   nextFetchWaitTime:(nullable NSNumber *)nextFetchWaitTime;
{
  [self.defaults setDouble:fetchTimestamp forKey:FIRIAM_UserDefaultsKeyForLastFetchTimestamp];
  self.lastFetchTime = fetchTimestamp;

  if (nextFetchWaitTime != nil) {
    if (nextFetchWaitTime.doubleValue > kMaxFetchWaitTimeInSeconds) {
      FIRLogInfo(kFIRLoggerInAppMessaging, @"I-IAM270006",
                 @"next fetch wait time %lf is too large. Ignore it.",
                 nextFetchWaitTime.doubleValue);
    } else {
      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270008",
                  @"Setting next fetch wait time as %lf from fetch response.",
                  nextFetchWaitTime.doubleValue);
      self.nextFetchWaitTime = nextFetchWaitTime.doubleValue;
      [self.defaults setObject:nextFetchWaitTime forKey:kUserDefaultsKeyForFetchWaitTime];
    }
  }
}

- (void)cleanupImpressions {
  [self.defaults setObject:@[] forKey:FIRIAM_UserDefaultsKeyForImpressions];
}

- (void)cleanupFetchRecords {
  [self.defaults setDouble:0 forKey:FIRIAM_UserDefaultsKeyForLastFetchTimestamp];
  self.lastFetchTime = 0;
}
@end

#endif  // TARGET_OS_IOS || TARGET_OS_TV
