/*
 *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */

#include "webrtc/system_wrappers/source/event_timer_posix.h"

#include "webrtc/base/criticalsection.h"
#include "webrtc/base/event.h"
#include "webrtc/test/gtest.h"

namespace webrtc {

enum class ThreadState {
  kNotStarted,
  kWaiting,
  kRequestProcessCall,
  kCallingProcess,
  kProcessDone,
  kContinue,
  kExiting,
  kDead
};

class EventTimerPosixTest : public testing::Test, public EventTimerPosix {
 public:
  EventTimerPosixTest()
      : thread_state_(ThreadState::kNotStarted),
        process_event_(false, true),
        main_event_(false, true),
        process_thread_id_(0),
        process_thread_(nullptr) {}
  virtual ~EventTimerPosixTest() {}

  rtc::PlatformThread* CreateThread() override {
    EXPECT_TRUE(process_thread_ == nullptr);
    process_thread_ =
        new rtc::PlatformThread(Run, this, "EventTimerPosixTestThread");
    return process_thread_;
  }

  static bool Run(void* obj) {
    return static_cast<EventTimerPosixTest*>(obj)->Process();
  }

  bool Process() {
    bool res = ProcessInternal();
    if (!res) {
      rtc::CritScope cs(&lock_);
      thread_state_ = ThreadState::kDead;
      main_event_.Set();
    }
    return res;
  }

  bool ProcessInternal() {
    {
      rtc::CritScope cs(&lock_);
      if (thread_state_ == ThreadState::kNotStarted) {
        if (!ChangeThreadState(ThreadState::kNotStarted,
                               ThreadState::kContinue)) {
          ADD_FAILURE() << "Unable to start process thread";
          return false;
        }
        process_thread_id_ = rtc::CurrentThreadId();
      }
    }

    if (!ChangeThreadState(ThreadState::kContinue, ThreadState::kWaiting))
      return false;

    if (!AwaitThreadState(ThreadState::kRequestProcessCall,
                          rtc::Event::kForever))
      return false;

    if (!ChangeThreadState(ThreadState::kRequestProcessCall,
                           ThreadState::kCallingProcess))
      return false;

    EventTimerPosix::Process();

    if (!ChangeThreadState(ThreadState::kCallingProcess,
                           ThreadState::kProcessDone))
      return false;

    if (!AwaitThreadState(ThreadState::kContinue, rtc::Event::kForever))
      return false;

    return true;
  }

  bool IsProcessThread() {
    rtc::CritScope cs(&lock_);
    return process_thread_id_ == rtc::CurrentThreadId();
  }

  bool ChangeThreadState(ThreadState prev_state, ThreadState new_state) {
    rtc::CritScope cs(&lock_);
    if (thread_state_ != prev_state)
      return false;
    thread_state_ = new_state;
    if (IsProcessThread()) {
      main_event_.Set();
    } else {
      process_event_.Set();
    }
    return true;
  }

  bool AwaitThreadState(ThreadState state, int timeout) {
    rtc::Event* event = IsProcessThread() ? &process_event_ : &main_event_;
    do {
      rtc::CritScope cs(&lock_);
      if (state != ThreadState::kDead && thread_state_ == ThreadState::kExiting)
        return false;
      if (thread_state_ == state)
        return true;
    } while (event->Wait(timeout));
    return false;
  }

  bool CallProcess(int timeout_ms) {
    return AwaitThreadState(ThreadState::kWaiting, timeout_ms) &&
           ChangeThreadState(ThreadState::kWaiting,
                             ThreadState::kRequestProcessCall);
  }

  bool AwaitProcessDone(int timeout_ms) {
    return AwaitThreadState(ThreadState::kProcessDone, timeout_ms) &&
           ChangeThreadState(ThreadState::kProcessDone, ThreadState::kContinue);
  }

  void TearDown() override {
    if (process_thread_) {
      {
        rtc::CritScope cs(&lock_);
        if (thread_state_ != ThreadState::kDead) {
          thread_state_ = ThreadState::kExiting;
          process_event_.Set();
        }
      }
      ASSERT_TRUE(AwaitThreadState(ThreadState::kDead, 5000));
    }
  }

  ThreadState thread_state_;
  rtc::CriticalSection lock_;
  rtc::Event process_event_;
  rtc::Event main_event_;
  rtc::PlatformThreadId process_thread_id_;
  rtc::PlatformThread* process_thread_;
};

TEST_F(EventTimerPosixTest, WaiterBlocksUntilTimeout) {
  const int kTimerIntervalMs = 100;
  const int kTimeoutMs = 5000;
  ASSERT_TRUE(StartTimer(false, kTimerIntervalMs));
  ASSERT_TRUE(CallProcess(kTimeoutMs));
  EventTypeWrapper res = Wait(kTimeoutMs);
  EXPECT_EQ(kEventSignaled, res);
  ASSERT_TRUE(AwaitProcessDone(kTimeoutMs));
}

TEST_F(EventTimerPosixTest, WaiterWakesImmediatelyAfterTimeout) {
  const int kTimerIntervalMs = 100;
  const int kTimeoutMs = 5000;
  ASSERT_TRUE(StartTimer(false, kTimerIntervalMs));
  ASSERT_TRUE(CallProcess(kTimeoutMs));
  ASSERT_TRUE(AwaitProcessDone(kTimeoutMs));
  EventTypeWrapper res = Wait(0);
  EXPECT_EQ(kEventSignaled, res);
}

TEST_F(EventTimerPosixTest, WaiterBlocksUntilTimeoutProcessInactiveOnStart) {
  const int kTimerIntervalMs = 100;
  const int kTimeoutMs = 5000;
  // First call to StartTimer initializes thread.
  ASSERT_TRUE(StartTimer(false, kTimerIntervalMs));

  // Process thread currently _not_ blocking on Process() call.
  ASSERT_TRUE(AwaitThreadState(ThreadState::kWaiting, kTimeoutMs));

  // Start new one-off timer, then call Process().
  ASSERT_TRUE(StartTimer(false, kTimerIntervalMs));
  ASSERT_TRUE(CallProcess(kTimeoutMs));

  EventTypeWrapper res = Wait(kTimeoutMs);
  EXPECT_EQ(kEventSignaled, res);

  ASSERT_TRUE(AwaitProcessDone(kTimeoutMs));
}

}  // namespace webrtc
