// Copyright 2025 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
// 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.

#include "third_party/highway/hwy/contrib/thread_pool/spin.h"

#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

#include <atomic>

#include "third_party/highway/hwy/aligned_allocator.h"          // HWY_ALIGNMENT
#include "third_party/highway/hwy/contrib/thread_pool/futex.h"  // NanoSleep
#include "third_party/highway/hwy/contrib/thread_pool/thread_pool.h"
#include "third_party/highway/hwy/contrib/thread_pool/topology.h"
#include "third_party/highway/hwy/tests/hwy_gtest.h"
#include "third_party/highway/hwy/tests/test_util-inl.h"
#include "third_party/highway/hwy/timer.h"

namespace hwy {
namespace {

struct TestPingPongT {
  template <class Spin>
  void operator()(const Spin& spin) const {
    constexpr size_t kU32PerLine = HWY_ALIGNMENT / 4;
    constexpr size_t kF64PerLine = HWY_ALIGNMENT / 8;
    alignas(HWY_ALIGNMENT) std::atomic<uint32_t> thread_active[kU32PerLine];
    alignas(HWY_ALIGNMENT) std::atomic<uint32_t> thread_done[kU32PerLine];

    thread_active[0].store(0, std::memory_order_release);
    thread_done[0].store(0, std::memory_order_release);
    hwy::ThreadPool pool(1);
    HWY_ASSERT(pool.NumWorkers() == 2);

    const double t0 = hwy::platform::Now();
    std::atomic_flag error = ATOMIC_FLAG_INIT;

    alignas(HWY_ALIGNMENT) std::atomic<size_t> reps1;
    alignas(HWY_ALIGNMENT) std::atomic<size_t> reps2;

    alignas(HWY_ALIGNMENT) std::atomic<double> before_thread_done[kF64PerLine];
    alignas(HWY_ALIGNMENT) std::atomic<double> before_thread_go[kF64PerLine];
    alignas(HWY_ALIGNMENT) std::atomic<double> ack_thread_done[kF64PerLine];
    alignas(HWY_ALIGNMENT) std::atomic<double> ack_thread_release[kF64PerLine];

    const auto kAcq = std::memory_order_acquire;
    const auto kRel = std::memory_order_release;
    pool.Run(0, 2, [&](uint64_t task, size_t thread) {
      HWY_ASSERT(task == thread);
      if (task == 0) {  // new thread
        SpinResult result = spin.UntilDifferent(0, thread_active[0]);
        ack_thread_release[0].store(hwy::platform::Now(), kRel);
        reps1.store(result.reps);
        if (!NanoSleep(20 * 1000 * 1000)) {
          error.test_and_set();
        }
        before_thread_done[0].store(hwy::platform::Now(), kRel);
        thread_done[0].store(1, kRel);
      } else {  // main thread
        if (!NanoSleep(30 * 1000 * 1000)) {
          error.test_and_set();
        }
        // Release the thread.
        before_thread_go[0].store(hwy::platform::Now(), kRel);
        thread_active[0].store(1, kRel);
        // Wait for it to finish.
        const size_t reps = spin.UntilEqual(1, thread_done[0]);
        ack_thread_done[0].store(hwy::platform::Now(), kRel);
        reps2.store(reps);
      }
    });

    const double t1 = hwy::platform::Now();
    const double elapsed = t1 - t0;
    const double latency1 =
        ack_thread_release[0].load(kAcq) - before_thread_go[0].load(kAcq);
    const double latency2 =
        ack_thread_done[0].load(kAcq) - before_thread_done[0].load(kAcq);
    fprintf(stderr,
            "Elapsed time: %f us; reps1=%zu, reps2=%zu, latency=%f %f us\n",
            elapsed * 1E6, reps1.load(), reps2.load(), latency1 * 1E6,
            latency2 * 1E6);
    // Unless NanoSleep failed to sleep, this should take 50ms+epsilon.
    HWY_ASSERT(error.test_and_set() || elapsed > 25E-3);
  }
};  // namespace hwy

// Simple mutex.
TEST(SpinTest, TestPingPong) {
  if (!HaveThreadingSupport()) {
    HWY_WARN("Threads not supported, skipping test\n");
    return;
  }

  const SpinType spin_type = DetectSpin();
  fprintf(stderr, "Spin method : %s\n", ToString(spin_type));
  CallWithSpin(spin_type, TestPingPongT());
}

}  // namespace
}  // namespace hwy

HWY_TEST_MAIN();
