/* 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 "Netd.h"
#include <android/log.h>
#include <cutils/sockets.h>
#include <fcntl.h>
#include <sys/socket.h>

#include "android/log.h"
#include "base/task.h"

#include "nsWhitespaceTokenizer.h"
#include "nsXULAppAPI.h"
#include "nsString.h"
#include "nsThreadUtils.h"
#include "mozilla/RefPtr.h"
#include "mozilla/Sprintf.h"
#include "SystemProperty.h"

#define NETD_LOG(args...)  __android_log_print(ANDROID_LOG_INFO, "Gonk", args)
#define ICS_SYS_USB_RNDIS_MAC "/sys/class/android_usb/android0/f_rndis/ethaddr"
#define INVALID_SOCKET -1
#define MAX_RECONNECT_TIMES 10

using mozilla::system::Property;

namespace {

RefPtr<mozilla::ipc::NetdClient> gNetdClient;
RefPtr<mozilla::ipc::NetdConsumer> gNetdConsumer;

class StopNetdConsumer : public mozilla::Runnable {
public:
  NS_IMETHOD Run() override
  {
    MOZ_ASSERT(NS_IsMainThread());

    gNetdConsumer = nullptr;
    return NS_OK;
  }
};

bool
InitRndisAddress()
{
  char mac[20];
  char serialno[] = "1234567890ABCDEF";
  static const int kEthernetAddressLength = 6;
  char address[kEthernetAddressLength];
  int i = 0;
  int ret = 0;
  int length = 0;
  mozilla::ScopedClose fd;

  fd.rwget() = open(ICS_SYS_USB_RNDIS_MAC, O_WRONLY);
  if (fd.rwget() == -1) {
    NETD_LOG("Unable to open file %s.", ICS_SYS_USB_RNDIS_MAC);
    return false;
  }

  Property::Get("ro.serialno", serialno, "1234567890ABCDEF");

  memset(address, 0, sizeof(address));
  // First byte is 0x02 to signify a locally administered address.
  address[0] = 0x02;
  length = strlen(serialno);
  for (i = 0; i < length; i++) {
    address[i % (kEthernetAddressLength - 1) + 1] ^= serialno[i];
  }

  SprintfLiteral(mac, "%02x:%02x:%02x:%02x:%02x:%02x",
                 address[0], address[1], address[2],
                 address[3], address[4], address[5]);
  length = strlen(mac);
  ret = write(fd.get(), mac, length);
  if (ret != length) {
    NETD_LOG("Fail to write file %s.", ICS_SYS_USB_RNDIS_MAC);
    return false;
  }
  return true;
}

} // namespace

namespace mozilla {
namespace ipc {

NetdClient::NetdClient()
  : LineWatcher('\0', MAX_COMMAND_SIZE)
  , mIOLoop(MessageLoopForIO::current())
  , mSocket(INVALID_SOCKET)
  , mCurrentWriteOffset(0)
  , mReConnectTimes(0)
{
  MOZ_COUNT_CTOR(NetdClient);
}

NetdClient::~NetdClient()
{
  MOZ_COUNT_DTOR(NetdClient);
}

bool
NetdClient::OpenSocket()
{
  mSocket.rwget() = socket_local_client("netd",
                                        ANDROID_SOCKET_NAMESPACE_RESERVED,
                                        SOCK_STREAM);
  if (mSocket.rwget() < 0) {
    NETD_LOG("Error connecting to : netd (%s) - will retry", strerror(errno));
    return false;
  }
  // Add FD_CLOEXEC flag.
  int flags = fcntl(mSocket.get(), F_GETFD);
  if (flags == -1) {
    NETD_LOG("Error doing fcntl with F_GETFD command(%s)", strerror(errno));
    return false;
  }
  flags |= FD_CLOEXEC;
  if (fcntl(mSocket.get(), F_SETFD, flags) == -1) {
    NETD_LOG("Error doing fcntl with F_SETFD command(%s)", strerror(errno));
    return false;
  }
  // Set non-blocking.
  if (fcntl(mSocket.get(), F_SETFL, O_NONBLOCK) == -1) {
    NETD_LOG("Error set non-blocking socket(%s)", strerror(errno));
    return false;
  }
  if (!MessageLoopForIO::current()->
      WatchFileDescriptor(mSocket.get(),
                          true,
                          MessageLoopForIO::WATCH_READ,
                          &mReadWatcher,
                          this)) {
    NETD_LOG("Error set socket read watcher(%s)", strerror(errno));
    return false;
  }

  if (!mOutgoingQ.empty()) {
    MessageLoopForIO::current()->
      WatchFileDescriptor(mSocket.get(),
                          false,
                          MessageLoopForIO::WATCH_WRITE,
                          &mWriteWatcher,
                          this);
  }

  NETD_LOG("Connected to netd");
  return true;
}

void NetdClient::OnLineRead(int aFd, nsDependentCSubstring& aMessage)
{
  // Set errno to 0 first. For preventing to use the stale version of errno.
  errno = 0;
  // We found a line terminator. Each line is formatted as an
  // integer response code followed by the rest of the line.
  // Fish out the response code.
  int responseCode = strtol(aMessage.Data(), nullptr, 10);
  if (!errno) {
    NetdCommand* response = new NetdCommand();
    // Passing all the response message, including the line terminator.
    response->mSize = aMessage.Length();
    memcpy(response->mData, aMessage.Data(), aMessage.Length());
    gNetdConsumer->MessageReceived(response);
  }

  if (!responseCode) {
    NETD_LOG("Can't parse netd's response");
  }
}

void
NetdClient::OnFileCanWriteWithoutBlocking(int aFd)
{
  MOZ_ASSERT(aFd == mSocket.get());
  WriteNetdCommand();
}

void
NetdClient::OnError()
{
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());

  mReadWatcher.StopWatchingFileDescriptor();
  mWriteWatcher.StopWatchingFileDescriptor();

  mSocket.dispose();
  mCurrentWriteOffset = 0;
  mCurrentNetdCommand = nullptr;
  while (!mOutgoingQ.empty()) {
    delete mOutgoingQ.front();
    mOutgoingQ.pop();
  }
  Start();
}

// static
void
NetdClient::Start()
{
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());

  if (!gNetdClient) {
    NETD_LOG("Netd Client is not initialized");
    return;
  }

  if (!gNetdClient->OpenSocket()) {
    // Socket open failed, try again in a second.
    NETD_LOG("Fail to connect to Netd");
    if (++gNetdClient->mReConnectTimes > MAX_RECONNECT_TIMES) {
      NETD_LOG("Fail to connect to Netd after retry %d times", MAX_RECONNECT_TIMES);
      return;
    }

    MessageLoopForIO::current()->
      PostDelayedTask(NewRunnableFunction(NetdClient::Start),
                      1000);
    return;
  }
  gNetdClient->mReConnectTimes = 0;
}

// static
void
NetdClient::SendNetdCommandIOThread(NetdCommand* aMessage)
{
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());
  MOZ_ASSERT(aMessage);

  if (!gNetdClient) {
    NETD_LOG("Netd Client is not initialized");
    return;
  }

  gNetdClient->mOutgoingQ.push(aMessage);

  if (gNetdClient->mSocket.get() == INVALID_SOCKET) {
    NETD_LOG("Netd connection is not established, push the message to queue");
    return;
  }

  gNetdClient->WriteNetdCommand();
}

void
NetdClient::WriteNetdCommand()
{
  if (!mCurrentNetdCommand) {
    mCurrentWriteOffset = 0;
    mCurrentNetdCommand = mOutgoingQ.front();
    mOutgoingQ.pop();
  }

  while (mCurrentWriteOffset < mCurrentNetdCommand->mSize) {
    ssize_t write_amount = mCurrentNetdCommand->mSize - mCurrentWriteOffset;
    ssize_t written = write(mSocket.get(),
                            mCurrentNetdCommand->mData + mCurrentWriteOffset,
                            write_amount);
    if (written < 0) {
      NETD_LOG("Cannot write to network, error %d\n", (int) written);
      OnError();
      return;
    }

    if (written > 0) {
      mCurrentWriteOffset += written;
    }

    if (written != write_amount) {
      NETD_LOG("WriteNetdCommand fail !!! Write is not completed");
      break;
    }
  }

  if (mCurrentWriteOffset != mCurrentNetdCommand->mSize) {
    MessageLoopForIO::current()->
      WatchFileDescriptor(mSocket.get(),
                          false,
                          MessageLoopForIO::WATCH_WRITE,
                          &mWriteWatcher,
                          this);
    return;
  }

  mCurrentNetdCommand = nullptr;
}

static void
InitNetdIOThread()
{
  bool result;
  char propValue[Property::VALUE_MAX_LENGTH];

  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());
  MOZ_ASSERT(!gNetdClient);

  Property::Get("ro.build.version.sdk", propValue, "0");
  // Assign rndis address for usb tethering in ICS.
  if (atoi(propValue) >= 15) {
    result = InitRndisAddress();
    // We don't return here because InitRnsisAddress() function is related to
    // usb tethering only. Others service such as wifi tethering still need
    // to use ipc to communicate with netd.
    if (!result) {
      NETD_LOG("fail to give rndis interface an address");
    }
  }
  gNetdClient = new NetdClient();
  gNetdClient->Start();
}

static void
ShutdownNetdIOThread()
{
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());
  nsCOMPtr<nsIRunnable> shutdownEvent = new StopNetdConsumer();

  gNetdClient = nullptr;

  NS_DispatchToMainThread(shutdownEvent);
}

void
StartNetd(NetdConsumer* aNetdConsumer)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aNetdConsumer);
  MOZ_ASSERT(gNetdConsumer == nullptr);

  gNetdConsumer = aNetdConsumer;
  XRE_GetIOMessageLoop()->PostTask(
    NewRunnableFunction(InitNetdIOThread));
}

void
StopNetd()
{
  MOZ_ASSERT(NS_IsMainThread());

  nsIThread* currentThread = NS_GetCurrentThread();
  NS_ASSERTION(currentThread, "This should never be null!");

  XRE_GetIOMessageLoop()->PostTask(
    NewRunnableFunction(ShutdownNetdIOThread));

  while (gNetdConsumer) {
    if (!NS_ProcessNextEvent(currentThread)) {
      NS_WARNING("Something bad happened!");
      break;
    }
  }
}

/**************************************************************************
*
*   This function runs in net worker Thread context. The net worker thread
*   is created in dom/system/gonk/NetworkManager.js
*
**************************************************************************/
void
SendNetdCommand(NetdCommand* aMessage)
{
  MOZ_ASSERT(aMessage);

  XRE_GetIOMessageLoop()->PostTask(
    NewRunnableFunction(NetdClient::SendNetdCommandIOThread, aMessage));
}

} // namespace ipc
} // namespace mozilla