/*
 * Copyright 2018 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 "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORClock.h"

#import <sys/sysctl.h>

// Using a monotonic clock is necessary because CFAbsoluteTimeGetCurrent(), NSDate, and related all
// are subject to drift. That it to say, multiple consecutive calls do not always result in a
// time that is in the future. Clocks may be adjusted by the user, NTP, or any number of external
// factors. This class attempts to determine the wall-clock time at the time of the event by
// capturing the kernel start and time since boot to determine a wallclock time in UTC.
//
// Timezone offsets at the time of a snapshot are also captured in order to provide local-time
// details. Other classes in this library depend on comparing times at some time in the future to
// a time captured in the past, and this class needs to provide a mechanism to do that.
//
// TL;DR: This class attempts to accomplish two things: 1. Provide accurate event times. 2. Provide
// a monotonic clock mechanism to accurately check if some clock snapshot was before or after
// by using a shared reference point (kernel boot time).
//
// Note: Much of the mach time stuff doesn't work properly in the simulator. So this class can be
// difficult to unit test.

/** Returns the kernel boottime property from sysctl.
 *
 * @return The KERN_BOOTTIME property from sysctl, in nanoseconds.
 */
static int64_t KernelBootTimeInNanoseconds(void) {
  // Caching the result is not possible because clock drift would not be accounted for.
  struct timeval boottime;
  int mib[2] = {CTL_KERN, KERN_BOOTTIME};
  size_t size = sizeof(boottime);
  int rc = sysctl(mib, 2, &boottime, &size, NULL, 0);
  if (rc != 0) {
    return 0;
  }
  return (int64_t)boottime.tv_sec * NSEC_PER_SEC + (int64_t)boottime.tv_usec * NSEC_PER_USEC;
}

/** Returns value of gettimeofday, in nanoseconds.
 *
 * @return The value of gettimeofday, in nanoseconds.
 */
static int64_t UptimeInNanoseconds(void) {
  int64_t before_now_nsec;
  int64_t after_now_nsec;
  struct timeval now;

  before_now_nsec = KernelBootTimeInNanoseconds();
  // Addresses a race condition in which the system time has updated, but the boottime has not.
  do {
    gettimeofday(&now, NULL);
    after_now_nsec = KernelBootTimeInNanoseconds();
  } while (after_now_nsec != before_now_nsec);
  return (int64_t)now.tv_sec * NSEC_PER_SEC + (int64_t)now.tv_usec * NSEC_PER_USEC -
         before_now_nsec;
}

// TODO: Consider adding a 'trustedTime' property that can be populated by the response from a BE.
@implementation GDTCORClock

- (instancetype)init {
  self = [super init];
  if (self) {
    _kernelBootTimeNanoseconds = KernelBootTimeInNanoseconds();
    _uptimeNanoseconds = UptimeInNanoseconds();
    _timeMillis =
        (int64_t)((CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) * NSEC_PER_USEC);
    _timezoneOffsetSeconds = [[NSTimeZone systemTimeZone] secondsFromGMT];
  }
  return self;
}

+ (GDTCORClock *)snapshot {
  return [[GDTCORClock alloc] init];
}

+ (instancetype)clockSnapshotInTheFuture:(uint64_t)millisInTheFuture {
  GDTCORClock *snapshot = [self snapshot];
  snapshot->_timeMillis += millisInTheFuture;
  return snapshot;
}

- (BOOL)isAfter:(GDTCORClock *)otherClock {
  // These clocks are trivially comparable when they share a kernel boot time.
  if (_kernelBootTimeNanoseconds == otherClock->_kernelBootTimeNanoseconds) {
    int64_t timeDiff = (_timeMillis + _timezoneOffsetSeconds) -
                       (otherClock->_timeMillis + otherClock->_timezoneOffsetSeconds);
    return timeDiff > 0;
  } else {
    int64_t kernelBootTimeDiff =
        otherClock->_kernelBootTimeNanoseconds - _kernelBootTimeNanoseconds;
    // This isn't a great solution, but essentially, if the other clock's boot time is 'later', NO
    // is returned. This can be altered by changing the system time and rebooting.
    return kernelBootTimeDiff < 0 ? YES : NO;
  }
}

- (int64_t)uptimeMilliseconds {
  return self.uptimeNanoseconds / NSEC_PER_MSEC;
}

- (NSUInteger)hash {
  return [@(_kernelBootTimeNanoseconds) hash] ^ [@(_uptimeNanoseconds) hash] ^
         [@(_timeMillis) hash] ^ [@(_timezoneOffsetSeconds) hash];
}

- (BOOL)isEqual:(id)object {
  return [self hash] == [object hash];
}

#pragma mark - NSSecureCoding

/** NSKeyedCoder key for timeMillis property. */
static NSString *const kGDTCORClockTimeMillisKey = @"GDTCORClockTimeMillis";

/** NSKeyedCoder key for timezoneOffsetMillis property. */
static NSString *const kGDTCORClockTimezoneOffsetSeconds = @"GDTCORClockTimezoneOffsetSeconds";

/** NSKeyedCoder key for _kernelBootTime ivar. */
static NSString *const kGDTCORClockKernelBootTime = @"GDTCORClockKernelBootTime";

/** NSKeyedCoder key for _uptimeNanoseconds ivar. */
static NSString *const kGDTCORClockUptime = @"GDTCORClockUptime";

+ (BOOL)supportsSecureCoding {
  return YES;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
  self = [super init];
  if (self) {
    // TODO: If the kernelBootTimeNanoseconds is more recent, we need to change the kernel boot time
    // and uptimeMillis ivars
    _timeMillis = [aDecoder decodeInt64ForKey:kGDTCORClockTimeMillisKey];
    _timezoneOffsetSeconds = [aDecoder decodeInt64ForKey:kGDTCORClockTimezoneOffsetSeconds];
    _kernelBootTimeNanoseconds = [aDecoder decodeInt64ForKey:kGDTCORClockKernelBootTime];
    _uptimeNanoseconds = [aDecoder decodeInt64ForKey:kGDTCORClockUptime];
  }
  return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
  [aCoder encodeInt64:_timeMillis forKey:kGDTCORClockTimeMillisKey];
  [aCoder encodeInt64:_timezoneOffsetSeconds forKey:kGDTCORClockTimezoneOffsetSeconds];
  [aCoder encodeInt64:_kernelBootTimeNanoseconds forKey:kGDTCORClockKernelBootTime];
  [aCoder encodeInt64:_uptimeNanoseconds forKey:kGDTCORClockUptime];
}

#pragma mark - Deprecated properties

- (int64_t)kernelBootTime {
  return self.kernelBootTimeNanoseconds;
}

- (int64_t)uptime {
  return self.uptimeNanoseconds;
}

@end
