/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* Copyright 2012 Mozilla Foundation and Mozilla contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <fcntl.h>
#include <sysutils/NetlinkEvent.h>

#include "base/message_loop.h"
#include "base/task.h"

#include "Hal.h"
#include "HalLog.h"
#include "mozilla/FileUtils.h"
#include "mozilla/RefPtr.h"
#include "mozilla/Monitor.h"
#include "nsPrintfCString.h"
#include "nsXULAppAPI.h"
#include "nsThreadUtils.h"
#include "UeventPoller.h"

using namespace mozilla::hal;

#define SWITCH_HEADSET_DEVPATH "/devices/virtual/switch/h2w"
#define SWITCH_USB_DEVPATH_GB  "/devices/virtual/switch/usb_configuration"
#define SWITCH_USB_DEVPATH_ICS "/devices/virtual/android_usb/android0"

namespace mozilla {
namespace hal_impl {
/**
 * The uevent for a usb on GB insertion looks like:
 *
 *  change@/devices/virtual/switch/usb_configuration
 *    ACTION=change
 *    DEVPATH=/devices/virtual/switch/usb_configuration
 *    SUBSYSTEM=switch
 *    SWITCH_NAME=usb_configuration
 *    SWITCH_STATE=0
 *    SEQNUM=5038
 */
class SwitchHandler
{
public:
  NS_INLINE_DECL_REFCOUNTING(SwitchHandler)

  SwitchHandler(const char* aDevPath, SwitchDevice aDevice)
    : mDevPath(aDevPath),
      mState(SWITCH_STATE_UNKNOWN),
      mDevice(aDevice)
  {
    GetInitialState();
  }

  bool CheckEvent(NetlinkEvent* aEvent)
  {
    if (strcmp(GetSubsystem(), aEvent->getSubsystem()) ||
        strcmp(mDevPath, aEvent->findParam("DEVPATH"))) {
        return false;
    }

    mState = ConvertState(GetStateString(aEvent));
    return mState != SWITCH_STATE_UNKNOWN;
  }

  SwitchState GetState()
  {
    return mState;
  }

  SwitchDevice GetType()
  {
    return mDevice;
  }
protected:
  virtual ~SwitchHandler()
  {
  }

  virtual const char* GetSubsystem()
  {
    return "switch";
  }

  virtual const char* GetStateString(NetlinkEvent* aEvent)
  {
    return aEvent->findParam("SWITCH_STATE");
  }

  void GetInitialState()
  {
    nsPrintfCString statePath("/sys%s/state", mDevPath);
    int fd = open(statePath.get(), O_RDONLY);
    if (fd <= 0) {
      return;
    }

    ScopedClose autoClose(fd);
    char state[16];
    ssize_t bytesRead = read(fd, state, sizeof(state));
    if (bytesRead < 0) {
      HAL_ERR("Read data from %s fails", statePath.get());
      return;
    }

    if (state[bytesRead - 1] == '\n') {
      bytesRead--;
    }

    state[bytesRead] = '\0';
    mState = ConvertState(state);
  }

  virtual SwitchState ConvertState(const char* aState)
  {
    MOZ_ASSERT(aState);
    return aState[0] == '0' ? SWITCH_STATE_OFF : SWITCH_STATE_ON;
  }

  const char* mDevPath;
  SwitchState mState;
  SwitchDevice mDevice;
};

/**
 * The uevent delivered for the USB configuration under ICS looks like,
 *
 *  change@/devices/virtual/android_usb/android0
 *    ACTION=change
 *    DEVPATH=/devices/virtual/android_usb/android0
 *    SUBSYSTEM=android_usb
 *    USB_STATE=CONFIGURED
 *    SEQNUM=1802
 */
class SwitchHandlerUsbIcs: public SwitchHandler
{
public:
  SwitchHandlerUsbIcs(const char* aDevPath) : SwitchHandler(aDevPath, SWITCH_USB)
  {
    SwitchHandler::GetInitialState();
  }

  virtual ~SwitchHandlerUsbIcs() { }

protected:
  virtual const char* GetSubsystem()
  {
    return "android_usb";
  }

  virtual const char* GetStateString(NetlinkEvent* aEvent)
  {
    return aEvent->findParam("USB_STATE");
  }

  SwitchState ConvertState(const char* aState)
  {
    MOZ_ASSERT(aState);
    return strcmp(aState, "CONFIGURED") == 0 ? SWITCH_STATE_ON : SWITCH_STATE_OFF;
  }
};

/**
 * The uevent delivered for the headset under ICS looks like,
 *
 * change@/devices/virtual/switch/h2w
 *   ACTION=change
 *   DEVPATH=/devices/virtual/switch/h2w
 *   SUBSYSTEM=switch
 *   SWITCH_NAME=h2w
 *   SWITCH_STATE=2 // Headset with no mic
 *   SEQNUM=2581
 * On Otoro, SWITCH_NAME could be Headset/No Device when plug/unplug.
 * change@/devices/virtual/switch/h2w
 *   ACTION=change
 *   DEVPATH=/devices/virtual/switch/h2w
 *   SUBSYSTEM=switch
 *   SWITCH_NAME=Headset
 *   SWITCH_STATE=1 // Headset with mic
 *   SEQNUM=1602
 */
class SwitchHandlerHeadphone: public SwitchHandler
{
public:
  SwitchHandlerHeadphone(const char* aDevPath) :
    SwitchHandler(aDevPath, SWITCH_HEADPHONES)
  {
    SwitchHandler::GetInitialState();
  }

  virtual ~SwitchHandlerHeadphone() { }

protected:
  SwitchState ConvertState(const char* aState)
  {
    MOZ_ASSERT(aState);

    return aState[0] == '0' ? SWITCH_STATE_OFF :
      (aState[0] == '1' ? SWITCH_STATE_HEADSET : SWITCH_STATE_HEADPHONE);
  }
};


typedef nsTArray<RefPtr<SwitchHandler> > SwitchHandlerArray;

class SwitchEventRunnable : public Runnable
{
public:
  SwitchEventRunnable(SwitchEvent& aEvent) : mEvent(aEvent)
  {
  }

  NS_IMETHOD Run()
  {
    NotifySwitchChange(mEvent);
    return NS_OK;
  }
private:
  SwitchEvent mEvent;
};

class SwitchEventObserver final : public IUeventObserver
{
  ~SwitchEventObserver()
  {
    mHandler.Clear();
  }

public:
  NS_INLINE_DECL_REFCOUNTING(SwitchEventObserver)
  SwitchEventObserver()
    : mEnableCount(0),
    mHeadphonesFromInputDev(false)
  {
    Init();
  }

  int GetEnableCount()
  {
    return mEnableCount;
  }

  void EnableSwitch(SwitchDevice aDevice)
  {
    mEventInfo[aDevice].mEnabled = true;
    mEnableCount++;
  }

  void DisableSwitch(SwitchDevice aDevice)
  {
    mEventInfo[aDevice].mEnabled = false;
    mEnableCount--;
  }

  void Notify(const NetlinkEvent& aEvent)
  {
    SwitchState currState;

    SwitchDevice device = GetEventInfo(aEvent, currState);
    if (device == SWITCH_DEVICE_UNKNOWN) {
      return;
    }

    EventInfo& info = mEventInfo[device];
    if (currState == info.mEvent.status()) {
      return;
    }

    info.mEvent.status() = currState;

    if (info.mEnabled) {
      NS_DispatchToMainThread(new SwitchEventRunnable(info.mEvent));
    }
  }

  void Notify(SwitchDevice aDevice, SwitchState aState)
  {
    EventInfo& info = mEventInfo[aDevice];
    if (aState == info.mEvent.status()) {
      return;
    }

    info.mEvent.status() = aState;

    if (info.mEnabled) {
      NS_DispatchToMainThread(new SwitchEventRunnable(info.mEvent));
    }
  }

  SwitchState GetCurrentInformation(SwitchDevice aDevice)
  {
    return mEventInfo[aDevice].mEvent.status();
  }

  void NotifyAnEvent(SwitchDevice aDevice)
  {
    EventInfo& info = mEventInfo[aDevice];
    if (info.mEvent.status() != SWITCH_STATE_UNKNOWN) {
      NS_DispatchToMainThread(new SwitchEventRunnable(info.mEvent));
    }
  }

  bool GetHeadphonesFromInputDev()
  {
    return mHeadphonesFromInputDev;
  }

private:
  class EventInfo
  {
  public:
    EventInfo() : mEnabled(false)
    {
      mEvent.status() = SWITCH_STATE_UNKNOWN;
      mEvent.device() = SWITCH_DEVICE_UNKNOWN;
    }
    SwitchEvent mEvent;
    bool mEnabled;
  };

  EventInfo mEventInfo[NUM_SWITCH_DEVICE];
  size_t mEnableCount;
  SwitchHandlerArray mHandler;
  bool mHeadphonesFromInputDev;

  // This function might also get called on the main thread
  // (from IsHeadphoneEventFromInputDev)
  void Init()
  {
    RefPtr<SwitchHandlerHeadphone> switchHeadPhone =
      new SwitchHandlerHeadphone(SWITCH_HEADSET_DEVPATH);

    // If the initial state is unknown, it means the headphone event is from input dev
    mHeadphonesFromInputDev = switchHeadPhone->GetState() == SWITCH_STATE_UNKNOWN ? true : false;

    if (!mHeadphonesFromInputDev) {
      mHandler.AppendElement(switchHeadPhone);
    } else {
      // If headphone status will be notified from input dev then initialize
      // status to "off" and wait for event notification.
      mEventInfo[SWITCH_HEADPHONES].mEvent.device() = SWITCH_HEADPHONES;
      mEventInfo[SWITCH_HEADPHONES].mEvent.status() = SWITCH_STATE_OFF;
    }
    mHandler.AppendElement(new SwitchHandler(SWITCH_USB_DEVPATH_GB, SWITCH_USB));
    mHandler.AppendElement(new SwitchHandlerUsbIcs(SWITCH_USB_DEVPATH_ICS));

    SwitchHandlerArray::index_type handlerIndex;
    SwitchHandlerArray::size_type numHandlers = mHandler.Length();

    for (handlerIndex = 0; handlerIndex < numHandlers; handlerIndex++) {
      SwitchState state = mHandler[handlerIndex]->GetState();
      if (state == SWITCH_STATE_UNKNOWN) {
        continue;
      }

      SwitchDevice device = mHandler[handlerIndex]->GetType();
      mEventInfo[device].mEvent.device() = device;
      mEventInfo[device].mEvent.status() = state;
    }
  }

  SwitchDevice GetEventInfo(const NetlinkEvent& aEvent, SwitchState& aState)
  {
    //working around the android code not being const-correct
    NetlinkEvent *e = const_cast<NetlinkEvent*>(&aEvent);

    for (size_t i = 0; i < mHandler.Length(); i++) {
      if (mHandler[i]->CheckEvent(e)) {
        aState = mHandler[i]->GetState();
        return mHandler[i]->GetType();
      }
    }
    return SWITCH_DEVICE_UNKNOWN;
  }
};

static RefPtr<SwitchEventObserver> sSwitchObserver;

static void
InitializeResourceIfNeed()
{
  if (!sSwitchObserver) {
    sSwitchObserver = new SwitchEventObserver();
    RegisterUeventListener(sSwitchObserver);
  }
}

static void
ReleaseResourceIfNeed()
{
  if (sSwitchObserver->GetEnableCount() == 0) {
    UnregisterUeventListener(sSwitchObserver);
    sSwitchObserver = nullptr;
  }
}

static void
EnableSwitchNotificationsIOThread(SwitchDevice aDevice, Monitor *aMonitor)
{
  InitializeResourceIfNeed();
  sSwitchObserver->EnableSwitch(aDevice);
  {
    MonitorAutoLock lock(*aMonitor);
    lock.Notify();
  }

  // Notify the latest state if IO thread has the information.
  if (sSwitchObserver->GetEnableCount() > 1) {
    sSwitchObserver->NotifyAnEvent(aDevice);
  }
}

void
EnableSwitchNotifications(SwitchDevice aDevice)
{
  Monitor monitor("EnableSwitch.monitor");
  {
    MonitorAutoLock lock(monitor);
    XRE_GetIOMessageLoop()->PostTask(
        NewRunnableFunction(EnableSwitchNotificationsIOThread, aDevice, &monitor));
    lock.Wait();
  }
}

static void
DisableSwitchNotificationsIOThread(SwitchDevice aDevice)
{
  MOZ_ASSERT(sSwitchObserver->GetEnableCount());
  sSwitchObserver->DisableSwitch(aDevice);
  ReleaseResourceIfNeed();
}

void
DisableSwitchNotifications(SwitchDevice aDevice)
{
  XRE_GetIOMessageLoop()->PostTask(
      NewRunnableFunction(DisableSwitchNotificationsIOThread, aDevice));
}

SwitchState
GetCurrentSwitchState(SwitchDevice aDevice)
{
  MOZ_ASSERT(sSwitchObserver && sSwitchObserver->GetEnableCount());
  return sSwitchObserver->GetCurrentInformation(aDevice);
}

static void
NotifySwitchStateIOThread(SwitchDevice aDevice, SwitchState aState)
{
  InitializeResourceIfNeed();
  sSwitchObserver->Notify(aDevice, aState);
}

void NotifySwitchStateFromInputDevice(SwitchDevice aDevice, SwitchState aState)
{
  XRE_GetIOMessageLoop()->PostTask(
      NewRunnableFunction(NotifySwitchStateIOThread, aDevice, aState));
}

bool IsHeadphoneEventFromInputDev()
{
  // Instead of calling InitializeResourceIfNeed, create new SwitchEventObserver
  // to prevent calling RegisterUeventListener in main thread.
  RefPtr<SwitchEventObserver> switchObserver = new SwitchEventObserver();
  return switchObserver->GetHeadphonesFromInputDev();
}

} // hal_impl
} //mozilla
