// Copyright 2023 The Abseil Authors
//
// 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
//
//     https://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.

#include "y_absl/synchronization/internal/kernel_timeout.h"

#ifndef _WIN32
#include <sys/types.h>
#endif

#include <algorithm>
#include <chrono>  // NOLINT(build/c++11)
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <limits>

#include "y_absl/base/attributes.h"
#include "y_absl/base/call_once.h"
#include "y_absl/base/config.h"
#include "y_absl/time/time.h"

namespace y_absl {
Y_ABSL_NAMESPACE_BEGIN
namespace synchronization_internal {

#ifdef Y_ABSL_INTERNAL_NEED_REDUNDANT_CONSTEXPR_DECL
constexpr uint64_t KernelTimeout::kNoTimeout;
constexpr int64_t KernelTimeout::kMaxNanos;
#endif

int64_t KernelTimeout::SteadyClockNow() {
  if (!SupportsSteadyClock()) {
    return y_absl::GetCurrentTimeNanos();
  }
  return std::chrono::duration_cast<std::chrono::nanoseconds>(
             std::chrono::steady_clock::now().time_since_epoch())
      .count();
}

KernelTimeout::KernelTimeout(y_absl::Time t) {
  // `y_absl::InfiniteFuture()` is a common "no timeout" value and cheaper to
  // compare than convert.
  if (t == y_absl::InfiniteFuture()) {
    rep_ = kNoTimeout;
    return;
  }

  int64_t unix_nanos = y_absl::ToUnixNanos(t);

  // A timeout that lands before the unix epoch is converted to 0.
  // In theory implementations should expire these timeouts immediately.
  if (unix_nanos < 0) {
    unix_nanos = 0;
  }

  // Values greater than or equal to kMaxNanos are converted to infinite.
  if (unix_nanos >= kMaxNanos) {
    rep_ = kNoTimeout;
    return;
  }

  rep_ = static_cast<uint64_t>(unix_nanos) << 1;
}

KernelTimeout::KernelTimeout(y_absl::Duration d) {
  // `y_absl::InfiniteDuration()` is a common "no timeout" value and cheaper to
  // compare than convert.
  if (d == y_absl::InfiniteDuration()) {
    rep_ = kNoTimeout;
    return;
  }

  int64_t nanos = y_absl::ToInt64Nanoseconds(d);

  // Negative durations are normalized to 0.
  // In theory implementations should expire these timeouts immediately.
  if (nanos < 0) {
    nanos = 0;
  }

  int64_t now = SteadyClockNow();
  if (nanos > kMaxNanos - now) {
    // Durations that would be greater than kMaxNanos are converted to infinite.
    rep_ = kNoTimeout;
    return;
  }

  nanos += now;
  rep_ = (static_cast<uint64_t>(nanos) << 1) | uint64_t{1};
}

int64_t KernelTimeout::MakeAbsNanos() const {
  if (!has_timeout()) {
    return kMaxNanos;
  }

  int64_t nanos = RawAbsNanos();

  if (is_relative_timeout()) {
    // We need to change epochs, because the relative timeout might be
    // represented by an absolute timestamp from another clock.
    nanos = std::max<int64_t>(nanos - SteadyClockNow(), 0);
    int64_t now = y_absl::GetCurrentTimeNanos();
    if (nanos > kMaxNanos - now) {
      // Overflow.
      nanos = kMaxNanos;
    } else {
      nanos += now;
    }
  } else if (nanos == 0) {
    // Some callers have assumed that 0 means no timeout, so instead we return a
    // time of 1 nanosecond after the epoch.
    nanos = 1;
  }

  return nanos;
}

int64_t KernelTimeout::InNanosecondsFromNow() const {
  if (!has_timeout()) {
    return kMaxNanos;
  }

  int64_t nanos = RawAbsNanos();
  if (is_absolute_timeout()) {
    return std::max<int64_t>(nanos - y_absl::GetCurrentTimeNanos(), 0);
  }
  return std::max<int64_t>(nanos - SteadyClockNow(), 0);
}

struct timespec KernelTimeout::MakeAbsTimespec() const {
  return y_absl::ToTimespec(y_absl::Nanoseconds(MakeAbsNanos()));
}

struct timespec KernelTimeout::MakeRelativeTimespec() const {
  return y_absl::ToTimespec(y_absl::Nanoseconds(InNanosecondsFromNow()));
}

#ifndef _WIN32
struct timespec KernelTimeout::MakeClockAbsoluteTimespec(clockid_t c) const {
  if (!has_timeout()) {
    return y_absl::ToTimespec(y_absl::Nanoseconds(kMaxNanos));
  }

  int64_t nanos = RawAbsNanos();
  if (is_absolute_timeout()) {
    nanos -= y_absl::GetCurrentTimeNanos();
  } else {
    nanos -= SteadyClockNow();
  }

  struct timespec now;
  Y_ABSL_RAW_CHECK(clock_gettime(c, &now) == 0, "clock_gettime() failed");
  y_absl::Duration from_clock_epoch =
      y_absl::DurationFromTimespec(now) + y_absl::Nanoseconds(nanos);
  if (from_clock_epoch <= y_absl::ZeroDuration()) {
    // Some callers have assumed that 0 means no timeout, so instead we return a
    // time of 1 nanosecond after the epoch. For safety we also do not return
    // negative values.
    return y_absl::ToTimespec(y_absl::Nanoseconds(1));
  }
  return y_absl::ToTimespec(from_clock_epoch);
}
#endif

KernelTimeout::DWord KernelTimeout::InMillisecondsFromNow() const {
  constexpr DWord kInfinite = std::numeric_limits<DWord>::max();

  if (!has_timeout()) {
    return kInfinite;
  }

  constexpr uint64_t kNanosInMillis = uint64_t{1'000'000};
  constexpr uint64_t kMaxValueNanos =
      std::numeric_limits<int64_t>::max() - kNanosInMillis + 1;

  uint64_t ns_from_now = static_cast<uint64_t>(InNanosecondsFromNow());
  if (ns_from_now >= kMaxValueNanos) {
    // Rounding up would overflow.
    return kInfinite;
  }
  // Convert to milliseconds, always rounding up.
  uint64_t ms_from_now = (ns_from_now + kNanosInMillis - 1) / kNanosInMillis;
  if (ms_from_now > kInfinite) {
    return kInfinite;
  }
  return static_cast<DWord>(ms_from_now);
}

std::chrono::time_point<std::chrono::system_clock>
KernelTimeout::ToChronoTimePoint() const {
  if (!has_timeout()) {
    return std::chrono::time_point<std::chrono::system_clock>::max();
  }

  // The cast to std::microseconds is because (on some platforms) the
  // std::ratio used by std::chrono::steady_clock doesn't convert to
  // std::nanoseconds, so it doesn't compile.
  auto micros = std::chrono::duration_cast<std::chrono::microseconds>(
      std::chrono::nanoseconds(MakeAbsNanos()));
  return std::chrono::system_clock::from_time_t(0) + micros;
}

std::chrono::nanoseconds KernelTimeout::ToChronoDuration() const {
  if (!has_timeout()) {
    return std::chrono::nanoseconds::max();
  }
  return std::chrono::nanoseconds(InNanosecondsFromNow());
}

}  // namespace synchronization_internal
Y_ABSL_NAMESPACE_END
}  // namespace y_absl