/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "mozilla/net/ChildDNSService.h"
#include "mozilla/net/DNSRequestChild.h"
#include "mozilla/net/NeckoChild.h"
#include "mozilla/Unused.h"
#include "nsIDNSRecord.h"
#include "nsHostResolver.h"
#include "nsTArray.h"
#include "nsNetAddr.h"
#include "nsIThread.h"
#include "nsThreadUtils.h"

using namespace mozilla::ipc;

namespace mozilla {
namespace net {

//-----------------------------------------------------------------------------
// ChildDNSRecord:
// A simple class to provide nsIDNSRecord on the child
//-----------------------------------------------------------------------------

class ChildDNSRecord : public nsIDNSRecord
{
public:
  NS_DECL_THREADSAFE_ISUPPORTS
  NS_DECL_NSIDNSRECORD

  ChildDNSRecord(const DNSRecord& reply, uint16_t flags);

private:
  virtual ~ChildDNSRecord();

  nsCString mCanonicalName;
  nsTArray<NetAddr> mAddresses;
  uint32_t mCurrent; // addr iterator
  uint32_t mLength;  // number of addrs
  uint16_t mFlags;
};

NS_IMPL_ISUPPORTS(ChildDNSRecord, nsIDNSRecord)

ChildDNSRecord::ChildDNSRecord(const DNSRecord& reply, uint16_t flags)
  : mCurrent(0)
  , mFlags(flags)
{
  mCanonicalName = reply.canonicalName();

  // A shame IPDL gives us no way to grab ownership of array: so copy it.
  const nsTArray<NetAddr>& addrs = reply.addrs();
  uint32_t i = 0;
  mLength = addrs.Length();
  for (; i < mLength; i++) {
    mAddresses.AppendElement(addrs[i]);
  }
}

ChildDNSRecord::~ChildDNSRecord()
{
}

//-----------------------------------------------------------------------------
// ChildDNSRecord::nsIDNSRecord
//-----------------------------------------------------------------------------

NS_IMETHODIMP
ChildDNSRecord::GetCanonicalName(nsACString &result)
{
  if (!(mFlags & nsHostResolver::RES_CANON_NAME)) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  result = mCanonicalName;
  return NS_OK;
}

NS_IMETHODIMP
ChildDNSRecord::GetNextAddr(uint16_t port, NetAddr *addr)
{
  if (mCurrent >= mLength) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  memcpy(addr, &mAddresses[mCurrent++], sizeof(NetAddr));

  // both Ipv4/6 use same bits for port, so safe to just use ipv4's field
  addr->inet.port = htons(port);

  return NS_OK;
}

NS_IMETHODIMP
ChildDNSRecord::GetAddresses(nsTArray<NetAddr> & aAddressArray)
{
  aAddressArray = mAddresses;
  return NS_OK;
}

// shamelessly copied from nsDNSRecord
NS_IMETHODIMP
ChildDNSRecord::GetScriptableNextAddr(uint16_t port, nsINetAddr **result)
{
  NetAddr addr;
  nsresult rv = GetNextAddr(port, &addr);
  if (NS_FAILED(rv)) return rv;

  NS_ADDREF(*result = new nsNetAddr(&addr));

  return NS_OK;
}

// also copied from nsDNSRecord
NS_IMETHODIMP
ChildDNSRecord::GetNextAddrAsString(nsACString &result)
{
  NetAddr addr;
  nsresult rv = GetNextAddr(0, &addr);
  if (NS_FAILED(rv)) {
    return rv;
  }

  char buf[kIPv6CStrBufSize];
  if (NetAddrToString(&addr, buf, sizeof(buf))) {
    result.Assign(buf);
    return NS_OK;
  }
  NS_ERROR("NetAddrToString failed unexpectedly");
  return NS_ERROR_FAILURE; // conversion failed for some reason
}

NS_IMETHODIMP
ChildDNSRecord::HasMore(bool *result)
{
  *result = mCurrent < mLength;
  return NS_OK;
}

NS_IMETHODIMP
ChildDNSRecord::Rewind()
{
  mCurrent = 0;
  return NS_OK;
}

NS_IMETHODIMP
ChildDNSRecord::ReportUnusable(uint16_t aPort)
{
  // "We thank you for your feedback" == >/dev/null
  // TODO: we could send info back to parent.
  return NS_OK;
}

//-----------------------------------------------------------------------------
// CancelDNSRequestEvent
//-----------------------------------------------------------------------------

class CancelDNSRequestEvent : public Runnable
{
public:
  CancelDNSRequestEvent(DNSRequestChild* aDnsReq, nsresult aReason)
    : mDnsRequest(aDnsReq)
    , mReasonForCancel(aReason)
  {}

  NS_IMETHOD Run() override
  {
    if (mDnsRequest->mIPCOpen) {
      // Send request to Parent process.
      mDnsRequest->SendCancelDNSRequest(mDnsRequest->mHost, mDnsRequest->mFlags,
                                        mDnsRequest->mNetworkInterface,
                                        mReasonForCancel);
    }
    return NS_OK;
  }
private:
  RefPtr<DNSRequestChild> mDnsRequest;
  nsresult mReasonForCancel;
};

//-----------------------------------------------------------------------------
// DNSRequestChild
//-----------------------------------------------------------------------------

DNSRequestChild::DNSRequestChild(const nsCString& aHost,
                                 const uint32_t& aFlags,
                                 const nsCString& aNetworkInterface,
                                 nsIDNSListener *aListener,
                                 nsIEventTarget *target)
  : mListener(aListener)
  , mTarget(target)
  , mResultStatus(NS_OK)
  , mHost(aHost)
  , mFlags(aFlags)
  , mNetworkInterface(aNetworkInterface)
  , mIPCOpen(false)
{
}

void
DNSRequestChild::StartRequest()
{
  // we can only do IPDL on the main thread
  if (!NS_IsMainThread()) {
    NS_DispatchToMainThread(
      NewRunnableMethod(this, &DNSRequestChild::StartRequest));
    return;
  }

  // Send request to Parent process.
  gNeckoChild->SendPDNSRequestConstructor(this, mHost, mFlags,
                                          mNetworkInterface);
  mIPCOpen = true;

  // IPDL holds a reference until IPDL channel gets destroyed
  AddIPDLReference();
}

void
DNSRequestChild::CallOnLookupComplete()
{
  MOZ_ASSERT(mListener);
  mListener->OnLookupComplete(this, mResultRecord, mResultStatus);
}

bool
DNSRequestChild::RecvLookupCompleted(const DNSRequestResponse& reply)
{
  mIPCOpen = false;
  MOZ_ASSERT(mListener);

  switch (reply.type()) {
  case DNSRequestResponse::TDNSRecord: {
    mResultRecord = new ChildDNSRecord(reply.get_DNSRecord(), mFlags);
    break;
  }
  case DNSRequestResponse::Tnsresult: {
    mResultStatus = reply.get_nsresult();
    break;
  }
  default:
    NS_NOTREACHED("unknown type");
    return false;
  }

  MOZ_ASSERT(NS_IsMainThread());

  bool targetIsMain = false;
  if (!mTarget) {
    targetIsMain = true;
  } else {
    mTarget->IsOnCurrentThread(&targetIsMain);
  }

  if (targetIsMain) {
    CallOnLookupComplete();
  } else {
    nsCOMPtr<nsIRunnable> event =
      NewRunnableMethod(this, &DNSRequestChild::CallOnLookupComplete);
    mTarget->Dispatch(event, NS_DISPATCH_NORMAL);
  }

  Unused << Send__delete__(this);

  return true;
}

void
DNSRequestChild::ReleaseIPDLReference()
{
  // Request is done or destroyed. Remove it from the hash table.
  RefPtr<ChildDNSService> dnsServiceChild =
    dont_AddRef(ChildDNSService::GetSingleton());
  dnsServiceChild->NotifyRequestDone(this);

  Release();
}

void
DNSRequestChild::ActorDestroy(ActorDestroyReason why)
{
  mIPCOpen = false;
}

//-----------------------------------------------------------------------------
// DNSRequestChild::nsISupports
//-----------------------------------------------------------------------------

NS_IMPL_ISUPPORTS(DNSRequestChild,
                  nsICancelable)

//-----------------------------------------------------------------------------
// DNSRequestChild::nsICancelable
//-----------------------------------------------------------------------------

NS_IMETHODIMP
DNSRequestChild::Cancel(nsresult reason)
{
  if(mIPCOpen) {
    // We can only do IPDL on the main thread
    NS_DispatchToMainThread(
      new CancelDNSRequestEvent(this, reason));
  }
  return NS_OK;
}

//------------------------------------------------------------------------------
} // namespace net
} // namespace mozilla
