// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <iosfwd>
#include <sstream>
#include <string>
#include <vector>

#include "base/basictypes.h"
#include "base/message_loop.h"
#include "base/port.h"
#include "base/threading/platform_thread.h"
#include "build/build_config.h"
#include "chrome/common/deprecated/event_sys-inl.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {

class Pair;

struct TestEvent {
  Pair* source;
  enum {
    A_CHANGED, B_CHANGED, PAIR_BEING_DELETED,
  } what_happened;
  int old_value;
};

struct TestEventTraits {
  typedef TestEvent EventType;
  static bool IsChannelShutdownEvent(const TestEvent& event) {
    return TestEvent::PAIR_BEING_DELETED == event.what_happened;
  }
};

class Pair {
 public:
  typedef EventChannel<TestEventTraits> Channel;
  explicit Pair(const std::string& name) : name_(name), a_(0), b_(0) {
    TestEvent shutdown = { this, TestEvent::PAIR_BEING_DELETED, 0 };
    event_channel_ = new Channel(shutdown);
  }
  ~Pair() {
    delete event_channel_;
  }
  void set_a(int n) {
    TestEvent event = { this, TestEvent::A_CHANGED, a_ };
    a_ = n;
    event_channel_->NotifyListeners(event);
  }
  void set_b(int n) {
    TestEvent event = { this, TestEvent::B_CHANGED, b_ };
    b_ = n;
    event_channel_->NotifyListeners(event);
  }
  int a() const { return a_; }
  int b() const { return b_; }
  const std::string& name() { return name_; }
  Channel* event_channel() const { return event_channel_; }

 protected:
  const std::string name_;
  int a_;
  int b_;
  Channel* event_channel_;
};

class EventLogger {
 public:
  explicit EventLogger(std::ostream* out) : out_(out) { }
  ~EventLogger() {
    for (Hookups::iterator i = hookups_.begin(); i != hookups_.end(); ++i)
      delete *i;
  }

  void Hookup(const std::string name, Pair::Channel* channel) {
    hookups_.push_back(NewEventListenerHookup(channel, this,
                                              &EventLogger::HandlePairEvent,
                                              name));
  }

  void HandlePairEvent(const std::string& name, const TestEvent& event) {
    const char* what_changed = NULL;
    int new_value = 0;
    Hookups::iterator dead;
    switch (event.what_happened) {
    case TestEvent::A_CHANGED:
      what_changed = "A";
      new_value = event.source->a();
      break;
    case TestEvent::B_CHANGED:
      what_changed = "B";
      new_value = event.source->b();
      break;
    case TestEvent::PAIR_BEING_DELETED:
      *out_ << name << " heard " << event.source->name() << " being deleted."
            << std::endl;
      return;
    default:
      FAIL() << "Bad event.what_happened: " << event.what_happened;
      break;
    }
    *out_ << name << " heard " << event.source->name() << "'s " << what_changed
          << " change from " << event.old_value
          << " to " << new_value << std::endl;
  }

  typedef std::vector<EventListenerHookup*> Hookups;
  Hookups hookups_;
  std::ostream* out_;
};

const char golden_result[] = "Larry heard Sally's B change from 0 to 2\n"
"Larry heard Sally's A change from 1 to 3\n"
"Lewis heard Sam's B change from 0 to 5\n"
"Larry heard Sally's A change from 3 to 6\n"
"Larry heard Sally being deleted.\n";

TEST(EventSys, Basic) {
  Pair sally("Sally"), sam("Sam");
  sally.set_a(1);
  std::stringstream log;
  EventLogger logger(&log);
  logger.Hookup("Larry", sally.event_channel());
  sally.set_b(2);
  sally.set_a(3);
  sam.set_a(4);
  logger.Hookup("Lewis", sam.event_channel());
  sam.set_b(5);
  sally.set_a(6);
  // Test that disconnect within callback doesn't deadlock.
  TestEvent event = {&sally, TestEvent::PAIR_BEING_DELETED, 0 };
  sally.event_channel()->NotifyListeners(event);
  sally.set_a(7);
  ASSERT_EQ(log.str(), golden_result);
}


// This goes pretty far beyond the normal use pattern, so don't use
// ThreadTester as an example of what to do.
class ThreadTester : public EventListener<TestEvent>,
                     public base::PlatformThread::Delegate {
 public:
  explicit ThreadTester(Pair* pair)
    : pair_(pair), remove_event_(&remove_event_mutex_),
      remove_event_bool_(false), completed_(false) {
    pair_->event_channel()->AddListener(this);
  }
  ~ThreadTester() {
    pair_->event_channel()->RemoveListener(this);
    for (size_t i = 0; i < threads_.size(); i++) {
      base::PlatformThread::Join(threads_[i].thread);
    }
  }

  struct ThreadInfo {
    base::PlatformThreadHandle thread;
  };

  struct ThreadArgs {
    base::ConditionVariable* thread_running_cond;
    base::Lock* thread_running_mutex;
    bool thread_running;
  };

  void Go() {
    base::Lock thread_running_mutex;
    base::ConditionVariable thread_running_cond(&thread_running_mutex);
    ThreadArgs args;
    ThreadInfo info;
    args.thread_running_cond = &(thread_running_cond);
    args.thread_running_mutex = &(thread_running_mutex);
    args.thread_running = false;
    args_ = args;
    ASSERT_TRUE(base::PlatformThread::Create(0, this, &info.thread));
    thread_running_mutex.Acquire();
    while ((args_.thread_running) == false) {
      thread_running_cond.Wait();
    }
    thread_running_mutex.Release();
    threads_.push_back(info);
  }

  // PlatformThread::Delegate methods.
  virtual void ThreadMain() {
    // Make sure each thread gets a current MessageLoop in TLS.
    // This test should use chrome threads for testing, but I'll leave it like
    // this for the moment since it requires a big chunk of rewriting and I
    // want the test passing while I checkpoint my CL.  Technically speaking,
    // there should be no functional difference.
    MessageLoop message_loop;
    args_.thread_running_mutex->Acquire();
    args_.thread_running = true;
    args_.thread_running_cond->Signal();
    args_.thread_running_mutex->Release();

    remove_event_mutex_.Acquire();
    while (remove_event_bool_ == false) {
      remove_event_.Wait();
    }
    remove_event_mutex_.Release();

    // Normally, you'd just delete the hookup. This is very bad style, but
    // necessary for the test.
    pair_->event_channel()->RemoveListener(this);

    completed_mutex_.Acquire();
    completed_ = true;
    completed_mutex_.Release();
  }

  void HandleEvent(const TestEvent& event) {
    remove_event_mutex_.Acquire();
    remove_event_bool_ = true;
    remove_event_.Broadcast();
    remove_event_mutex_.Release();

    base::PlatformThread::YieldCurrentThread();

    completed_mutex_.Acquire();
    if (completed_)
      FAIL() << "A test thread exited too early.";
    completed_mutex_.Release();
  }

  Pair* pair_;
  base::ConditionVariable remove_event_;
  base::Lock remove_event_mutex_;
  bool remove_event_bool_;
  base::Lock completed_mutex_;
  bool completed_;
  std::vector<ThreadInfo> threads_;
  ThreadArgs args_;
};

TEST(EventSys, Multithreaded) {
  Pair sally("Sally");
  ThreadTester a(&sally);
  for (int i = 0; i < 3; ++i)
    a.Go();
  sally.set_b(99);
}

class HookupDeleter {
 public:
  void HandleEvent(const TestEvent& event) {
    delete hookup_;
    hookup_ = NULL;
  }
  EventListenerHookup* hookup_;
};

TEST(EventSys, InHandlerDeletion) {
  Pair sally("Sally");
  HookupDeleter deleter;
  deleter.hookup_ = NewEventListenerHookup(sally.event_channel(),
                                           &deleter,
                                           &HookupDeleter::HandleEvent);
  sally.set_a(1);
  ASSERT_TRUE(NULL == deleter.hookup_);
}

}  // namespace