// Copyright (c) 2009 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.

// There are three classes involved here.  There's NetworkChangeNotifierMac,
// which is the Mac specific implementation of NetworkChangeNotifier.  It is the
// class with which clients can register themselves as network change 
// observers.  There's NetworkChangeNotifierThread, which is a base::Thread
// subclass of MessageLoop::TYPE_UI (since it needs a CFRunLoop) that contains
// the NetworkChangeNotifierImpl.  NetworkChangeNotifierImpl is the object
// that receives the actual OS X notifications and posts them to the
// NetworkChangeNotifierMac's message loop, so that NetworkChangeNotifierMac
// can notify all its observers.
//
// When NetworkChangeNotifierMac is being deleted, it will delete the
// NetworkChangeNotifierThread, which will Stop() it and also delete the
// NetworkChangeNotifierImpl.  Therefore, NetworkChangeNotifierImpl and
// NetworkChangeNotifierThread's lifetimes generally begin after and end before
// NetworkChangeNotifierMac.  There is an edge case where a notification task
// gets posted to the IO thread, thereby maintaining a reference to
// NetworkChangeNotifierImpl beyond the lifetime of NetworkChangeNotifierThread.
// In this case, the notification is cancelled, and NetworkChangeNotifierImpl
// will be deleted once all notification tasks that reference it have been run.

#include "net/base/network_change_notifier_mac.h"
#include <SystemConfiguration/SCDynamicStore.h>
#include <SystemConfiguration/SCDynamicStoreKey.h>
#include <SystemConfiguration/SCSchemaDefinitions.h>
#include <algorithm>
#include "base/logging.h"
#include "base/scoped_cftyperef.h"
#include "base/thread.h"

namespace net {

namespace {

// NetworkChangeNotifierImpl should be created on a thread with a CFRunLoop,
// since it requires one to pump notifications.  However, it also runs some
// methods on |notifier_loop_|, because it cannot post calls to |notifier_|
// since NetworkChangeNotifier is not ref counted in a thread safe manner.
class NetworkChangeNotifierImpl
    : public base::RefCountedThreadSafe<NetworkChangeNotifierImpl> {
 public:
  NetworkChangeNotifierImpl(MessageLoop* notifier_loop,
                            NetworkChangeNotifierMac* notifier);

  void Shutdown();

 private:
  friend class base::RefCountedThreadSafe<NetworkChangeNotifierImpl>;
  ~NetworkChangeNotifierImpl();

  static void DynamicStoreCallback(SCDynamicStoreRef /* store */,
                                   CFArrayRef changed_keys,
                                   void* config);

  void OnNetworkConfigChange(CFArrayRef changed_keys);

  // Runs on |notifier_loop_|.
  void OnIPAddressChanged();

  // Raw pointers.  Note that |notifier_| _must_ outlive the
  // NetworkChangeNotifierImpl.  For lifecycle management details, read the
  // comment at the top of the file.
  MessageLoop* const notifier_loop_;
  NetworkChangeNotifierMac* notifier_;

  scoped_cftyperef<CFRunLoopSourceRef> source_;

  DISALLOW_COPY_AND_ASSIGN(NetworkChangeNotifierImpl);
};

NetworkChangeNotifierImpl::NetworkChangeNotifierImpl(
    MessageLoop* notifier_loop, NetworkChangeNotifierMac* notifier)
    : notifier_loop_(notifier_loop),
      notifier_(notifier) {
  DCHECK_EQ(MessageLoop::TYPE_UI, MessageLoop::current()->type());
  SCDynamicStoreContext context = {
    0,    // Version 0.
    this, // User data.
    NULL, // This is not reference counted.  No retain function.
    NULL, // This is not reference counted.  No release function.
    NULL, // No description for this.
  };

  // Get a reference to the dynamic store.
  scoped_cftyperef<SCDynamicStoreRef> store(
      SCDynamicStoreCreate(NULL /* use default allocator */,
                           CFSTR("org.chromium"),
                           DynamicStoreCallback, &context));

  // Create a run loop source for the dynamic store.
  source_.reset(SCDynamicStoreCreateRunLoopSource(
      NULL /* use default allocator */,
      store.get(),
      0 /* 0 sounds like a fine source order to me! */));

  // Add the run loop source to the current run loop.
  CFRunLoopAddSource(CFRunLoopGetCurrent(),
                     source_.get(),
                     kCFRunLoopCommonModes);

  // Set up the notification keys.
  scoped_cftyperef<CFMutableArrayRef> notification_keys(
      CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks));

  // Monitor interface changes.
  scoped_cftyperef<CFStringRef> key(
      SCDynamicStoreKeyCreateNetworkGlobalEntity(
          NULL /* default allocator */, kSCDynamicStoreDomainState,
          kSCEntNetInterface));
  CFArrayAppendValue(notification_keys.get(), key.get());

  // Monitor IP address changes.

  key.reset(SCDynamicStoreKeyCreateNetworkGlobalEntity(
      NULL /* default allocator */, kSCDynamicStoreDomainState,
      kSCEntNetIPv4));
  CFArrayAppendValue(notification_keys.get(), key.get());

  key.reset(SCDynamicStoreKeyCreateNetworkGlobalEntity(
      NULL /* default allocator */, kSCDynamicStoreDomainState,
      kSCEntNetIPv6));
  CFArrayAppendValue(notification_keys.get(), key.get());

  // Ok, let's ask for notifications!
  // TODO(willchan): Figure out a proper way to handle this rather than crash.
  CHECK(SCDynamicStoreSetNotificationKeys(
      store.get(), notification_keys.get(), NULL));
}

NetworkChangeNotifierImpl::~NetworkChangeNotifierImpl() {
  CFRunLoopRemoveSource(CFRunLoopGetCurrent(),
                        source_.get(),
                        kCFRunLoopCommonModes);
}

void NetworkChangeNotifierImpl::Shutdown() {
  CHECK(notifier_);
  notifier_ = NULL;
}

// static
void NetworkChangeNotifierImpl::DynamicStoreCallback(
    SCDynamicStoreRef /* store */,
    CFArrayRef changed_keys,
    void* config) {
  NetworkChangeNotifierImpl* net_config =
      static_cast<NetworkChangeNotifierImpl*>(config);
  net_config->OnNetworkConfigChange(changed_keys);
}

void NetworkChangeNotifierImpl::OnNetworkConfigChange(CFArrayRef changed_keys) {
  for (CFIndex i = 0; i < CFArrayGetCount(changed_keys); ++i) {
    CFStringRef key = static_cast<CFStringRef>(
        CFArrayGetValueAtIndex(changed_keys, i));
    if (CFStringHasSuffix(key, kSCEntNetIPv4) ||
        CFStringHasSuffix(key, kSCEntNetIPv6)) {
      notifier_loop_->PostTask(
          FROM_HERE,
          NewRunnableMethod(
              this,
              &NetworkChangeNotifierImpl::OnIPAddressChanged));
    } else if (CFStringHasSuffix(key, kSCEntNetInterface)) {
      // TODO(willchan): Does not appear to be working.  Look into this.
      // Perhaps this isn't needed anyway.
    } else {
      NOTREACHED();
    }
  }
}

void NetworkChangeNotifierImpl::OnIPAddressChanged() {
  // If |notifier_| doesn't exist, then that means we're shutting down, so
  // notifications are all cancelled.
  if (notifier_)
    notifier_->OnIPAddressChanged();
}

class NetworkChangeNotifierThread : public base::Thread {
 public:
  NetworkChangeNotifierThread(MessageLoop* notifier_loop,
                              NetworkChangeNotifierMac* notifier);
  ~NetworkChangeNotifierThread();

 protected:
  virtual void Init();

 private:
  MessageLoop* const notifier_loop_;
  NetworkChangeNotifierMac* const notifier_;
  scoped_refptr<NetworkChangeNotifierImpl> notifier_impl_;

  DISALLOW_COPY_AND_ASSIGN(NetworkChangeNotifierThread);
};

NetworkChangeNotifierThread::NetworkChangeNotifierThread(
    MessageLoop* notifier_loop, NetworkChangeNotifierMac* notifier)
    : base::Thread("NetworkChangeNotifier"),
      notifier_loop_(notifier_loop),
      notifier_(notifier) {}

NetworkChangeNotifierThread::~NetworkChangeNotifierThread() {
  notifier_impl_->Shutdown();
  Stop();
}

// Note that |notifier_impl_| is initialized on the network change
// notifier thread, not whatever thread constructs the
// NetworkChangeNotifierThread object.  This is important, because this thread
// is the one that has a CFRunLoop.
void NetworkChangeNotifierThread::Init() {
  notifier_impl_ =
      new NetworkChangeNotifierImpl(notifier_loop_, notifier_);
}

}  // namespace

NetworkChangeNotifierMac::NetworkChangeNotifierMac()
    : notifier_thread_(
          new NetworkChangeNotifierThread(MessageLoop::current(), this)) {
  base::Thread::Options thread_options;
  thread_options.message_loop_type = MessageLoop::TYPE_UI;
  notifier_thread_->StartWithOptions(thread_options);
}

NetworkChangeNotifierMac::~NetworkChangeNotifierMac() {}

}  // namespace net
