// Copyright 2013 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 "chrome/browser/local_discovery/service_discovery_client_mac.h"

#import <Foundation/Foundation.h>
#import <arpa/inet.h>
#import <net/if_dl.h>

#include "base/memory/singleton.h"
#include "base/metrics/histogram.h"
#include "base/threading/thread.h"

using local_discovery::ServiceWatcherImplMac;
using local_discovery::ServiceResolverImplMac;

@interface NetServiceBrowserDelegate
    : NSObject<NSNetServiceBrowserDelegate, NSNetServiceDelegate> {
 @private
  ServiceWatcherImplMac::NetServiceBrowserContainer* container_;  // weak.
  base::scoped_nsobject<NSMutableArray> services_;
}

- (id)initWithContainer:
        (ServiceWatcherImplMac::NetServiceBrowserContainer*)serviceWatcherImpl;

@end

@interface NetServiceDelegate : NSObject <NSNetServiceDelegate> {
  @private
   ServiceResolverImplMac::NetServiceContainer* container_;
}

- (id)initWithContainer:
        (ServiceResolverImplMac::NetServiceContainer*)serviceResolverImpl;

@end

namespace local_discovery {

namespace {

const char kServiceDiscoveryThreadName[] = "Service Discovery Thread";

const NSTimeInterval kResolveTimeout = 10.0;

// Extracts the instance name, name type and domain from a full service name or
// the service type and domain from a service type. Returns true if successful.
// TODO(justinlin): This current only handles service names with format
// <name>._<protocol2>._<protocol1>.<domain>. Service names with
// subtypes will not parse correctly:
// <name>._<type>._<sub>._<protocol2>._<protocol1>.<domain>.
bool ExtractServiceInfo(const std::string& service,
                        bool is_service_name,
                        std::string* instance,
                        std::string* type,
                        std::string* domain) {
  if (service.empty())
    return false;

  const size_t last_period = service.find_last_of('.');
  if (last_period == std::string::npos || service.length() <= last_period)
    return false;

  if (!is_service_name) {
    *instance = std::string();
    *type = service.substr(0, last_period) + ".";
  } else {
    // Find third last period that delimits type and instance name.
    size_t type_period = last_period;
    for (int i = 0; i < 2; ++i) {
      type_period = service.find_last_of('.', type_period - 1);
      if (type_period == std::string::npos)
        return false;
    }

    *instance = service.substr(0, type_period);
    *type = service.substr(type_period + 1, last_period - type_period);
  }
  *domain = service.substr(last_period + 1) + ".";

  return !domain->empty() &&
         !type->empty() &&
         (!is_service_name || !instance->empty());
}

void ParseTxtRecord(NSData* record, std::vector<std::string>* output) {
  if (record == nil || [record length] <= 1)
    return;

  VLOG(1) << "ParseTxtRecord: " << [record length];

  const char* record_bytes = reinterpret_cast<const char*>([record bytes]);
  const char* const record_end = record_bytes + [record length];
  // TODO(justinlin): More strict bounds checking.
  while (record_bytes < record_end) {
    uint8 size = *record_bytes++;
    if (size <= 0)
      continue;

    if (record_bytes + size <= record_end) {
      VLOG(1) << "TxtRecord: "
              << std::string(record_bytes, static_cast<size_t>(size));
      output->push_back(
          [[[NSString alloc] initWithBytes:record_bytes
                             length:size
                             encoding:NSUTF8StringEncoding] UTF8String]);
    }
    record_bytes += size;
  }
}

}  // namespace

ServiceDiscoveryClientMac::ServiceDiscoveryClientMac() {}
ServiceDiscoveryClientMac::~ServiceDiscoveryClientMac() {}

scoped_ptr<ServiceWatcher> ServiceDiscoveryClientMac::CreateServiceWatcher(
    const std::string& service_type,
    const ServiceWatcher::UpdatedCallback& callback) {
  StartThreadIfNotStarted();
  VLOG(1) << "CreateServiceWatcher: " << service_type;
  return scoped_ptr<ServiceWatcher>(new ServiceWatcherImplMac(
      service_type, callback, service_discovery_thread_->message_loop_proxy()));
}

scoped_ptr<ServiceResolver> ServiceDiscoveryClientMac::CreateServiceResolver(
    const std::string& service_name,
    const ServiceResolver::ResolveCompleteCallback& callback) {
  StartThreadIfNotStarted();
  VLOG(1) << "CreateServiceResolver: " << service_name;
  return scoped_ptr<ServiceResolver>(new ServiceResolverImplMac(
      service_name, callback, service_discovery_thread_->message_loop_proxy()));
}

scoped_ptr<LocalDomainResolver>
ServiceDiscoveryClientMac::CreateLocalDomainResolver(
    const std::string& domain,
    net::AddressFamily address_family,
    const LocalDomainResolver::IPAddressCallback& callback) {
  NOTIMPLEMENTED();  // TODO(noamsml): Implement.
  VLOG(1) << "CreateLocalDomainResolver: " << domain;
  return scoped_ptr<LocalDomainResolver>();
}

void ServiceDiscoveryClientMac::StartThreadIfNotStarted() {
  if (!service_discovery_thread_) {
    service_discovery_thread_.reset(
        new base::Thread(kServiceDiscoveryThreadName));
    // Only TYPE_UI uses an NSRunLoop.
    base::Thread::Options options(base::MessageLoop::TYPE_UI, 0);
    service_discovery_thread_->StartWithOptions(options);
  }
}

ServiceWatcherImplMac::NetServiceBrowserContainer::NetServiceBrowserContainer(
    const std::string& service_type,
    const ServiceWatcher::UpdatedCallback& callback,
    scoped_refptr<base::MessageLoopProxy> service_discovery_runner)
    : service_type_(service_type),
      callback_(callback),
      callback_runner_(base::MessageLoopProxy::current()),
      service_discovery_runner_(service_discovery_runner),
      weak_factory_(this) {}

ServiceWatcherImplMac::NetServiceBrowserContainer::
    ~NetServiceBrowserContainer() {
  DCHECK(IsOnServiceDiscoveryThread());
}

void ServiceWatcherImplMac::NetServiceBrowserContainer::Start() {
  service_discovery_runner_->PostTask(
      FROM_HERE,
      base::Bind(&NetServiceBrowserContainer::StartOnDiscoveryThread,
                 weak_factory_.GetWeakPtr()));
}

void ServiceWatcherImplMac::NetServiceBrowserContainer::DiscoverNewServices() {
  service_discovery_runner_->PostTask(
      FROM_HERE,
      base::Bind(&NetServiceBrowserContainer::DiscoverOnDiscoveryThread,
                 weak_factory_.GetWeakPtr()));
}

void
ServiceWatcherImplMac::NetServiceBrowserContainer::StartOnDiscoveryThread() {
  DCHECK(IsOnServiceDiscoveryThread());

  delegate_.reset([[NetServiceBrowserDelegate alloc] initWithContainer:this]);
  browser_.reset([[NSNetServiceBrowser alloc] init]);
  [browser_ setDelegate:delegate_];
}

void
ServiceWatcherImplMac::NetServiceBrowserContainer::DiscoverOnDiscoveryThread() {
  DCHECK(IsOnServiceDiscoveryThread());
  std::string instance;
  std::string type;
  std::string domain;

  if (!ExtractServiceInfo(service_type_, false, &instance, &type, &domain))
    return;

  DCHECK(instance.empty());
  DVLOG(1) << "Listening for service type '" << type
           << "' on domain '" << domain << "'";

  base::Time start_time = base::Time::Now();
  [browser_ searchForServicesOfType:[NSString stringWithUTF8String:type.c_str()]
            inDomain:[NSString stringWithUTF8String:domain.c_str()]];
  UMA_HISTOGRAM_TIMES("LocalDiscovery.MacBrowseCallTimes",
                      base::Time::Now() - start_time);
}

void ServiceWatcherImplMac::NetServiceBrowserContainer::OnServicesUpdate(
    ServiceWatcher::UpdateType update,
    const std::string& service) {
  callback_runner_->PostTask(FROM_HERE, base::Bind(callback_, update, service));
}

void ServiceWatcherImplMac::NetServiceBrowserContainer::DeleteSoon() {
  service_discovery_runner_->DeleteSoon(FROM_HERE, this);
}

ServiceWatcherImplMac::ServiceWatcherImplMac(
    const std::string& service_type,
    const ServiceWatcher::UpdatedCallback& callback,
    scoped_refptr<base::MessageLoopProxy> service_discovery_runner)
    : service_type_(service_type),
      callback_(callback),
      started_(false),
      weak_factory_(this) {
  container_.reset(new NetServiceBrowserContainer(
      service_type,
      base::Bind(&ServiceWatcherImplMac::OnServicesUpdate,
                 weak_factory_.GetWeakPtr()),
      service_discovery_runner));
}

ServiceWatcherImplMac::~ServiceWatcherImplMac() {}

void ServiceWatcherImplMac::Start() {
  DCHECK(!started_);
  VLOG(1) << "ServiceWatcherImplMac::Start";
  container_->Start();
  started_ = true;
}

void ServiceWatcherImplMac::DiscoverNewServices(bool force_update) {
  DCHECK(started_);
  VLOG(1) << "ServiceWatcherImplMac::DiscoverNewServices";
  container_->DiscoverNewServices();
}

void ServiceWatcherImplMac::SetActivelyRefreshServices(
    bool actively_refresh_services) {
  DCHECK(started_);
  VLOG(1) << "ServiceWatcherImplMac::SetActivelyRefreshServices";
}

std::string ServiceWatcherImplMac::GetServiceType() const {
  return service_type_;
}

void ServiceWatcherImplMac::OnServicesUpdate(ServiceWatcher::UpdateType update,
                                             const std::string& service) {
  VLOG(1) << "ServiceWatcherImplMac::OnServicesUpdate: "
          << service + "." + service_type_;
  callback_.Run(update, service + "." + service_type_);
}

ServiceResolverImplMac::NetServiceContainer::NetServiceContainer(
    const std::string& service_name,
    const ServiceResolver::ResolveCompleteCallback& callback,
    scoped_refptr<base::MessageLoopProxy> service_discovery_runner)
    : service_name_(service_name),
      callback_(callback),
      callback_runner_(base::MessageLoopProxy::current()),
      service_discovery_runner_(service_discovery_runner),
      weak_factory_(this) {}

ServiceResolverImplMac::NetServiceContainer::~NetServiceContainer() {
  DCHECK(IsOnServiceDiscoveryThread());
}

void ServiceResolverImplMac::NetServiceContainer::StartResolving() {
  service_discovery_runner_->PostTask(
      FROM_HERE,
      base::Bind(&NetServiceContainer::StartResolvingOnDiscoveryThread,
                 weak_factory_.GetWeakPtr()));
}

void ServiceResolverImplMac::NetServiceContainer::DeleteSoon() {
  service_discovery_runner_->DeleteSoon(FROM_HERE, this);
}

void
ServiceResolverImplMac::NetServiceContainer::StartResolvingOnDiscoveryThread() {
  DCHECK(IsOnServiceDiscoveryThread());
  std::string instance;
  std::string type;
  std::string domain;

  // The service object is set ahead of time by tests.
  if (service_)
    return;

  if (ExtractServiceInfo(service_name_, true, &instance, &type, &domain)) {
    VLOG(1) << "ServiceResolverImplMac::ServiceResolverImplMac::"
            << "StartResolvingOnDiscoveryThread: Success";
    delegate_.reset([[NetServiceDelegate alloc] initWithContainer:this]);
    service_.reset(
        [[NSNetService alloc]
            initWithDomain:[[NSString alloc] initWithUTF8String:domain.c_str()]
            type:[[NSString alloc] initWithUTF8String:type.c_str()]
            name:[[NSString alloc] initWithUTF8String:instance.c_str()]]);
    [service_ setDelegate:delegate_];

    [service_ resolveWithTimeout:kResolveTimeout];
  }

  VLOG(1) << "ServiceResolverImplMac::NetServiceContainer::"
          << "StartResolvingOnDiscoveryThread: " << service_name_
          << ", instance: " << instance << ", type: " << type
          << ", domain: " << domain;
}

void ServiceResolverImplMac::NetServiceContainer::OnResolveUpdate(
    RequestStatus status) {
  if (status == STATUS_SUCCESS) {
    service_description_.service_name = service_name_;

    for (NSData* address in [service_ addresses]) {
      const void* bytes = [address bytes];
      // TODO(justinlin): Handle IPv6 addresses?
      if (static_cast<const sockaddr*>(bytes)->sa_family == AF_INET) {
        const sockaddr_in* sock = static_cast<const sockaddr_in*>(bytes);
        char addr[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &sock->sin_addr, addr, INET_ADDRSTRLEN);
        service_description_.address =
            net::HostPortPair(addr, ntohs(sock->sin_port));
        net::ParseIPLiteralToNumber(addr, &service_description_.ip_address);
        break;
      }
    }

    ParseTxtRecord([service_ TXTRecordData], &service_description_.metadata);

    // TODO(justinlin): Implement last_seen.
    service_description_.last_seen = base::Time::Now();
    callback_runner_->PostTask(
        FROM_HERE, base::Bind(callback_, status, service_description_));
  } else {
    callback_runner_->PostTask(
        FROM_HERE, base::Bind(callback_, status, ServiceDescription()));
  }
}

void ServiceResolverImplMac::NetServiceContainer::SetServiceForTesting(
    base::scoped_nsobject<NSNetService> service) {
  service_ = service;
}

ServiceResolverImplMac::ServiceResolverImplMac(
    const std::string& service_name,
    const ServiceResolver::ResolveCompleteCallback& callback,
    scoped_refptr<base::MessageLoopProxy> service_discovery_runner)
    : service_name_(service_name),
      callback_(callback),
      has_resolved_(false),
      weak_factory_(this) {
  container_.reset(new NetServiceContainer(
      service_name,
      base::Bind(&ServiceResolverImplMac::OnResolveComplete,
                 weak_factory_.GetWeakPtr()),
      service_discovery_runner));
}

ServiceResolverImplMac::~ServiceResolverImplMac() {}

void ServiceResolverImplMac::StartResolving() {
  container_->StartResolving();

  VLOG(1) << "Resolving service " << service_name_;
}

std::string ServiceResolverImplMac::GetName() const { return service_name_; }

void ServiceResolverImplMac::OnResolveComplete(
    RequestStatus status,
    const ServiceDescription& description) {
  VLOG(1) << "ServiceResolverImplMac::OnResolveComplete: " << service_name_
          << ", " << status;

  has_resolved_ = true;

  callback_.Run(status, description);
}

ServiceResolverImplMac::NetServiceContainer*
ServiceResolverImplMac::GetContainerForTesting() {
  return container_.get();
}

}  // namespace local_discovery

@implementation NetServiceBrowserDelegate

- (id)initWithContainer:
          (ServiceWatcherImplMac::NetServiceBrowserContainer*)container {
  if ((self = [super init])) {
    container_ = container;
    services_.reset([[NSMutableArray alloc] initWithCapacity:1]);
  }
  return self;
}

- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser
        didFindService:(NSNetService *)netService
        moreComing:(BOOL)moreServicesComing {
  // Start monitoring this service for updates.
  [netService setDelegate:self];
  [netService startMonitoring];
  [services_ addObject:netService];

  container_->OnServicesUpdate(local_discovery::ServiceWatcher::UPDATE_ADDED,
                               [[netService name] UTF8String]);
}

- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser
        didRemoveService:(NSNetService *)netService
        moreComing:(BOOL)moreServicesComing {
  container_->OnServicesUpdate(local_discovery::ServiceWatcher::UPDATE_REMOVED,
                               [[netService name] UTF8String]);

  NSUInteger index = [services_ indexOfObject:netService];
  if (index != NSNotFound) {
    // Stop monitoring this service for updates.
    [[services_ objectAtIndex:index] stopMonitoring];
    [services_ removeObjectAtIndex:index];
  }
}

- (void)netService:(NSNetService *)sender
        didUpdateTXTRecordData:(NSData *)data {
  container_->OnServicesUpdate(local_discovery::ServiceWatcher::UPDATE_CHANGED,
                               [[sender name] UTF8String]);
}

@end

@implementation NetServiceDelegate

- (id)initWithContainer:
          (ServiceResolverImplMac::NetServiceContainer*)container {
  if ((self = [super init])) {
    container_ = container;
  }
  return self;
}

- (void)netServiceDidResolveAddress:(NSNetService *)sender {
  container_->OnResolveUpdate(local_discovery::ServiceResolver::STATUS_SUCCESS);
}

- (void)netService:(NSNetService *)sender
        didNotResolve:(NSDictionary *)errorDict {
  container_->OnResolveUpdate(
      local_discovery::ServiceResolver::STATUS_REQUEST_TIMEOUT);
}

@end
