/* -*- Mode: C++; tab-width: 2; 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 "NetworkActivityMonitor.h"
#include "prmem.h"
#include "nsIObserverService.h"
#include "nsPISocketTransportService.h"
#include "nsSocketTransportService2.h"
#include "nsThreadUtils.h"
#include "mozilla/Services.h"
#include "prerror.h"

using namespace mozilla::net;

static PRStatus
nsNetMon_Connect(PRFileDesc *fd, const PRNetAddr *addr, PRIntervalTime timeout)
{
  PRStatus ret;
  PRErrorCode code;
  ret = fd->lower->methods->connect(fd->lower, addr, timeout);
  if (ret == PR_SUCCESS || (code = PR_GetError()) == PR_WOULD_BLOCK_ERROR ||
      code == PR_IN_PROGRESS_ERROR)
    NetworkActivityMonitor::DataInOut(NetworkActivityMonitor::kUpload);
  return ret;
}

static int32_t
nsNetMon_Read(PRFileDesc *fd, void *buf, int32_t len)
{
  int32_t ret;
  ret = fd->lower->methods->read(fd->lower, buf, len);
  if (ret >= 0)
    NetworkActivityMonitor::DataInOut(NetworkActivityMonitor::kDownload);
  return ret;
}

static int32_t
nsNetMon_Write(PRFileDesc *fd, const void *buf, int32_t len)
{
  int32_t ret;
  ret = fd->lower->methods->write(fd->lower, buf, len);
  if (ret > 0)
    NetworkActivityMonitor::DataInOut(NetworkActivityMonitor::kUpload);
  return ret;
}

static int32_t
nsNetMon_Writev(PRFileDesc *fd,
                const PRIOVec *iov,
                int32_t size,
                PRIntervalTime timeout)
{
  int32_t ret;
  ret = fd->lower->methods->writev(fd->lower, iov, size, timeout);
  if (ret > 0)
    NetworkActivityMonitor::DataInOut(NetworkActivityMonitor::kUpload);
  return ret;
}

static int32_t
nsNetMon_Recv(PRFileDesc *fd,
              void *buf,
              int32_t amount,
              int flags,
              PRIntervalTime timeout)
{
  int32_t ret;
  ret = fd->lower->methods->recv(fd->lower, buf, amount, flags, timeout);
  if (ret >= 0)
    NetworkActivityMonitor::DataInOut(NetworkActivityMonitor::kDownload);
  return ret;
}

static int32_t
nsNetMon_Send(PRFileDesc *fd,
              const void *buf,
              int32_t amount,
              int flags,
              PRIntervalTime timeout)
{
  int32_t ret;
  ret = fd->lower->methods->send(fd->lower, buf, amount, flags, timeout);
  if (ret > 0)
    NetworkActivityMonitor::DataInOut(NetworkActivityMonitor::kUpload);
  return ret;
}

static int32_t
nsNetMon_RecvFrom(PRFileDesc *fd,
                  void *buf,
                  int32_t amount,
                  int flags,
                  PRNetAddr *addr,
                  PRIntervalTime timeout)
{
  int32_t ret;
  ret = fd->lower->methods->recvfrom(fd->lower,
                                     buf,
                                     amount,
                                     flags,
                                     addr,
                                     timeout);
  if (ret >= 0)
    NetworkActivityMonitor::DataInOut(NetworkActivityMonitor::kDownload);
  return ret;
}

static int32_t
nsNetMon_SendTo(PRFileDesc *fd,
                const void *buf,
                int32_t amount,
                int flags,
                const PRNetAddr *addr,
                PRIntervalTime timeout)
{
  int32_t ret;
  ret = fd->lower->methods->sendto(fd->lower,
                                   buf,
                                   amount,
                                   flags,
                                   addr,
                                   timeout);
  if (ret > 0)
    NetworkActivityMonitor::DataInOut(NetworkActivityMonitor::kUpload);
  return ret;
}

static int32_t
nsNetMon_AcceptRead(PRFileDesc *listenSock,
                    PRFileDesc **acceptedSock,
                    PRNetAddr **peerAddr,
                    void *buf,
                    int32_t amount,
                    PRIntervalTime timeout)
{
  int32_t ret;
  ret = listenSock->lower->methods->acceptread(listenSock->lower,
                                               acceptedSock,
                                               peerAddr,
                                               buf,
                                               amount,
                                               timeout);
  if (ret > 0)
    NetworkActivityMonitor::DataInOut(NetworkActivityMonitor::kDownload);
  return ret;
}


class NotifyNetworkActivity : public nsRunnable {
public:
  NotifyNetworkActivity(nsIObserverService* aObs,
                        NetworkActivityMonitor::Direction aDirection)
    : mObs(aObs)
    , mDirection(aDirection)
  {}
  NS_IMETHOD Run()
  {
    mObs->NotifyObservers(nullptr,
                          mDirection == NetworkActivityMonitor::kUpload
                            ? NS_NETWORK_ACTIVITY_BLIP_UPLOAD_TOPIC
                            : NS_NETWORK_ACTIVITY_BLIP_DOWNLOAD_TOPIC,
                          nullptr);
    return NS_OK;
  }
private:
  nsCOMPtr<nsIObserverService>      mObs;
  NetworkActivityMonitor::Direction mDirection;
};

NetworkActivityMonitor * NetworkActivityMonitor::gInstance = nullptr;

NetworkActivityMonitor::NetworkActivityMonitor()
  : mLayerIdentity(PR_INVALID_IO_LAYER)
  , mBlipInterval(PR_INTERVAL_NO_TIMEOUT)
{
  NS_ASSERTION(gInstance==nullptr,
               "multiple NetworkActivityMonitor instances!");
}

NetworkActivityMonitor::~NetworkActivityMonitor()
{
  gInstance = nullptr;
}

nsresult
NetworkActivityMonitor::Init(int32_t blipInterval)
{
  nsresult rv;

  if (gInstance)
    return NS_ERROR_ALREADY_INITIALIZED;

  NetworkActivityMonitor * mon = new NetworkActivityMonitor();
  rv = mon->Init_Internal(blipInterval);
  if (NS_FAILED(rv)) {
    delete mon;
    return rv;
  }

  gInstance = mon;
  return NS_OK;
}

nsresult
NetworkActivityMonitor::Shutdown()
{
  if (!gInstance)
    return NS_ERROR_NOT_INITIALIZED;

  delete gInstance;
  return NS_OK;
}

nsresult
NetworkActivityMonitor::Init_Internal(int32_t blipInterval)
{
  mLayerIdentity = PR_GetUniqueIdentity("network activity monitor layer");
  mLayerMethods  = *PR_GetDefaultIOMethods();
  mLayerMethods.connect    = nsNetMon_Connect;
  mLayerMethods.read       = nsNetMon_Read;
  mLayerMethods.write      = nsNetMon_Write;
  mLayerMethods.writev     = nsNetMon_Writev;
  mLayerMethods.recv       = nsNetMon_Recv;
  mLayerMethods.send       = nsNetMon_Send;
  mLayerMethods.recvfrom   = nsNetMon_RecvFrom;
  mLayerMethods.sendto     = nsNetMon_SendTo;
  mLayerMethods.acceptread = nsNetMon_AcceptRead;

  mObserverService = mozilla::services::GetObserverService();
  if (!mObserverService)
    return NS_ERROR_FAILURE;

  mBlipInterval = PR_MillisecondsToInterval(blipInterval);
  // Set the last notification times to time that has just expired, so any
  // activity even right now will trigger notification.
  mLastNotificationTime[kUpload] = PR_IntervalNow() - mBlipInterval;
  mLastNotificationTime[kDownload] = mLastNotificationTime[kUpload];

  return NS_OK;
}

nsresult
NetworkActivityMonitor::AttachIOLayer(PRFileDesc *fd)
{
  if (!gInstance)
    return NS_OK;

  PRFileDesc * layer;
  PRStatus     status;

  layer = PR_CreateIOLayerStub(gInstance->mLayerIdentity,
                               &gInstance->mLayerMethods);
  if (!layer) {
    return NS_ERROR_FAILURE;
  }

  status = PR_PushIOLayer(fd, PR_NSPR_IO_LAYER, layer);

  if (status == PR_FAILURE) {
    PR_DELETE(layer);
    return NS_ERROR_FAILURE;
  }

  return NS_OK;
}

nsresult
NetworkActivityMonitor::DataInOut(Direction direction)
{
  NS_ASSERTION(PR_GetCurrentThread() == gSocketThread, "wrong thread");

  if (gInstance) {
    PRIntervalTime now = PR_IntervalNow();
    if ((now - gInstance->mLastNotificationTime[direction]) >
        gInstance->mBlipInterval) {
      gInstance->mLastNotificationTime[direction] = now;
      gInstance->PostNotification(direction);
    }
  }

  return NS_OK;
}

void
NetworkActivityMonitor::PostNotification(Direction direction)
{
  nsRefPtr<nsIRunnable> ev = new NotifyNetworkActivity(mObserverService,
                                                       direction);
  NS_DispatchToMainThread(ev);
}
