/*
 * Copyright 2019, gRPC Authors All rights reserved.
 *
 * 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 Dispatch
import NIO

/// A timeout for a gRPC call.
///
/// Timeouts must be positive and at most 8-digits long.
public struct GRPCTimeout: CustomStringConvertible, Equatable {
  /// Creates an infinite timeout. This is a sentinel value which must __not__ be sent to a gRPC service.
  public static let infinite = GRPCTimeout(
    nanoseconds: Int64.max,
    wireEncoding: "infinite"
  )

  /// The largest amount of any unit of time which may be represented by a gRPC timeout.
  internal static let maxAmount: Int64 = 99_999_999

  /// The wire encoding of this timeout as described in the gRPC protocol.
  /// See: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md.
  public let wireEncoding: String
  public let nanoseconds: Int64

  public var description: String {
    return self.wireEncoding
  }

  /// Creates a timeout from the given deadline.
  ///
  /// - Parameter deadline: The deadline to create a timeout from.
  internal init(deadline: NIODeadline, testingOnlyNow: NIODeadline? = nil) {
    switch deadline {
    case .distantFuture:
      self = .infinite
    default:
      let timeAmountUntilDeadline = deadline - (testingOnlyNow ?? .now())
      self.init(rounding: timeAmountUntilDeadline.nanoseconds, unit: .nanoseconds)
    }
  }

  private init(nanoseconds: Int64, wireEncoding: String) {
    self.nanoseconds = nanoseconds
    self.wireEncoding = wireEncoding
  }

  /// Creates a `GRPCTimeout`.
  ///
  /// - Precondition: The amount should be greater than or equal to zero and less than or equal
  ///   to `GRPCTimeout.maxAmount`.
  internal init(amount: Int64, unit: GRPCTimeoutUnit) {
    precondition(amount >= 0 && amount <= GRPCTimeout.maxAmount)
    // See "Timeout" in https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests

    // If we overflow at this point, which is certainly possible if `amount` is sufficiently large
    // and `unit` is `.hours`, clamp the nanosecond timeout to `Int64.max`. It's about 292 years so
    // it should be long enough for the user not to notice the difference should the rpc time out.
    let (partial, overflow) = amount.multipliedReportingOverflow(by: unit.asNanoseconds)

    self.init(
      nanoseconds: overflow ? Int64.max : partial,
      wireEncoding: "\(amount)\(unit.rawValue)"
    )
  }

  /// Create a timeout by rounding up the timeout so that it may be represented in the gRPC
  /// wire format.
  internal init(rounding amount: Int64, unit: GRPCTimeoutUnit) {
    var roundedAmount = amount
    var roundedUnit = unit

    if roundedAmount <= 0 {
      roundedAmount = 0
    } else {
      while roundedAmount > GRPCTimeout.maxAmount {
        switch roundedUnit {
        case .nanoseconds:
          roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 1000)
          roundedUnit = .microseconds
        case .microseconds:
          roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 1000)
          roundedUnit = .milliseconds
        case .milliseconds:
          roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 1000)
          roundedUnit = .seconds
        case .seconds:
          roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 60)
          roundedUnit = .minutes
        case .minutes:
          roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 60)
          roundedUnit = .hours
        case .hours:
          roundedAmount = GRPCTimeout.maxAmount
          roundedUnit = .hours
        }
      }
    }

    self.init(amount: roundedAmount, unit: roundedUnit)
  }
}

private extension Int64 {
  /// Returns the quotient of this value when divided by `divisor` rounded up to the nearest
  /// multiple of `divisor` if the remainder is non-zero.
  ///
  /// - Parameter divisor: The value to divide this value by.
  func quotientRoundedUp(dividingBy divisor: Int64) -> Int64 {
    let (quotient, remainder) = self.quotientAndRemainder(dividingBy: divisor)
    return quotient + (remainder != 0 ? 1 : 0)
  }
}

internal enum GRPCTimeoutUnit: String {
  case hours = "H"
  case minutes = "M"
  case seconds = "S"
  case milliseconds = "m"
  case microseconds = "u"
  case nanoseconds = "n"

  internal var asNanoseconds: Int64 {
    switch self {
    case .hours:
      return 60 * 60 * 1000 * 1000 * 1000

    case .minutes:
      return 60 * 1000 * 1000 * 1000

    case .seconds:
      return 1000 * 1000 * 1000

    case .milliseconds:
      return 1000 * 1000

    case .microseconds:
      return 1000

    case .nanoseconds:
      return 1
    }
  }
}
