/* 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 "midirMIDIPlatformService.h"
#include "mozilla/StaticMutex.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/dom/MIDIPort.h"
#include "mozilla/dom/MIDITypes.h"
#include "mozilla/dom/MIDIPortInterface.h"
#include "mozilla/dom/MIDIPortParent.h"
#include "mozilla/dom/MIDIPlatformRunnables.h"
#include "mozilla/dom/MIDIUtils.h"
#include "mozilla/dom/midi/midir_impl_ffi_generated.h"
#include "mozilla/ipc/BackgroundParent.h"
#include "mozilla/Unused.h"
#include "nsIThread.h"
#include "mozilla/Logging.h"
#include "MIDILog.h"

using namespace mozilla;
using namespace mozilla::dom;
using namespace mozilla::ipc;

static_assert(sizeof(TimeStamp) == sizeof(GeckoTimeStamp));

/**
 * Runnable used for to send messages asynchronously on the I/O thread.
 */
class SendRunnable : public MIDIBackgroundRunnable {
 public:
  explicit SendRunnable(const nsAString& aPortID, const MIDIMessage& aMessage)
      : MIDIBackgroundRunnable("SendRunnable"),
        mPortID(aPortID),
        mMessage(aMessage) {}
  ~SendRunnable() = default;
  virtual void RunInternal() {
    MIDIPlatformService::AssertThread();
    if (!MIDIPlatformService::IsRunning()) {
      // Some send operations might outlive the service, bail out and do nothing
      return;
    }
    midirMIDIPlatformService* srv =
        static_cast<midirMIDIPlatformService*>(MIDIPlatformService::Get());
    srv->SendMessage(mPortID, mMessage);
  }

 private:
  nsString mPortID;
  MIDIMessage mMessage;
};

// static
StaticMutex midirMIDIPlatformService::gOwnerThreadMutex;

// static
nsCOMPtr<nsISerialEventTarget> midirMIDIPlatformService::gOwnerThread;

midirMIDIPlatformService::midirMIDIPlatformService()
    : mImplementation(nullptr) {
  StaticMutexAutoLock lock(gOwnerThreadMutex);
  gOwnerThread = OwnerThread();
}

midirMIDIPlatformService::~midirMIDIPlatformService() {
  LOG("midir_impl_shutdown");
  if (mImplementation) {
    midir_impl_shutdown(mImplementation);
  }
  StaticMutexAutoLock lock(gOwnerThreadMutex);
  gOwnerThread = nullptr;
}

// static
void midirMIDIPlatformService::AddPort(const nsString* aId,
                                       const nsString* aName, bool aInput) {
  MIDIPortType type = aInput ? MIDIPortType::Input : MIDIPortType::Output;
  MIDIPortInfo port(*aId, *aName, u""_ns, u""_ns, static_cast<uint32_t>(type));
  MIDIPlatformService::Get()->AddPortInfo(port);
}

// static
void midirMIDIPlatformService::RemovePort(const nsString* aId,
                                          const nsString* aName, bool aInput) {
  MIDIPortType type = aInput ? MIDIPortType::Input : MIDIPortType::Output;
  MIDIPortInfo port(*aId, *aName, u""_ns, u""_ns, static_cast<uint32_t>(type));
  MIDIPlatformService::Get()->RemovePortInfo(port);
}

void midirMIDIPlatformService::Init() {
  if (mImplementation) {
    return;
  }

  mImplementation = midir_impl_init(AddPort);

  if (mImplementation) {
    MIDIPlatformService::Get()->SendPortList();
  } else {
    LOG("midir_impl_init failure");
  }
}

// static
void midirMIDIPlatformService::CheckAndReceive(const nsString* aId,
                                               const uint8_t* aData,
                                               size_t aLength,
                                               const GeckoTimeStamp* aTimeStamp,
                                               uint64_t aMicros) {
  nsTArray<uint8_t> data;
  data.AppendElements(aData, aLength);
  const TimeStamp* openTime = reinterpret_cast<const TimeStamp*>(aTimeStamp);
  TimeStamp timestamp =
      *openTime + TimeDuration::FromMicroseconds(static_cast<double>(aMicros));
  MIDIMessage message(data, timestamp);
  LogMIDIMessage(message, *aId, MIDIPortType::Input);
  nsTArray<MIDIMessage> messages;
  messages.AppendElement(message);

  nsCOMPtr<nsIRunnable> r(new ReceiveRunnable(*aId, messages));
  StaticMutexAutoLock lock(gOwnerThreadMutex);
  if (gOwnerThread) {
    gOwnerThread->Dispatch(r, NS_DISPATCH_NORMAL);
  }
}

void midirMIDIPlatformService::Refresh() {
  midir_impl_refresh(mImplementation, AddPort, RemovePort);
}

void midirMIDIPlatformService::Open(MIDIPortParent* aPort) {
  AssertThread();
  MOZ_ASSERT(aPort);
  nsString id = aPort->MIDIPortInterface::Id();
  TimeStamp openTimeStamp = TimeStamp::Now();
  if (midir_impl_open_port(mImplementation, &id,
                           reinterpret_cast<GeckoTimeStamp*>(&openTimeStamp),
                           CheckAndReceive)) {
    LOG("MIDI port open: %s at t=%lf", NS_ConvertUTF16toUTF8(id).get(),
        (openTimeStamp - TimeStamp::ProcessCreation()).ToSeconds());
    nsCOMPtr<nsIRunnable> r(new SetStatusRunnable(
        aPort, aPort->DeviceState(), MIDIPortConnectionState::Open));
    OwnerThread()->Dispatch(r.forget());
  } else {
    LOG("MIDI port open failed: %s", NS_ConvertUTF16toUTF8(id).get());
  }
}

void midirMIDIPlatformService::Stop() {
  // Nothing to do here AFAIK
}

void midirMIDIPlatformService::ScheduleSend(const nsAString& aPortId) {
  AssertThread();
  LOG("MIDI port schedule send %s", NS_ConvertUTF16toUTF8(aPortId).get());
  nsTArray<MIDIMessage> messages;
  GetMessages(aPortId, messages);
  TimeStamp now = TimeStamp::Now();
  for (const auto& message : messages) {
    if (message.timestamp().IsNull()) {
      SendMessage(aPortId, message);
    } else {
      double delay = (message.timestamp() - now).ToMilliseconds();
      if (delay < 1.0) {
        SendMessage(aPortId, message);
      } else {
        nsCOMPtr<nsIRunnable> r(new SendRunnable(aPortId, message));
        OwnerThread()->DelayedDispatch(r.forget(),
                                       static_cast<uint32_t>(delay));
      }
    }
  }
}

void midirMIDIPlatformService::ScheduleClose(MIDIPortParent* aPort) {
  AssertThread();
  MOZ_ASSERT(aPort);
  nsString id = aPort->MIDIPortInterface::Id();
  LOG("MIDI port schedule close %s", NS_ConvertUTF16toUTF8(id).get());
  if (aPort->ConnectionState() == MIDIPortConnectionState::Open) {
    midir_impl_close_port(mImplementation, &id);
    nsCOMPtr<nsIRunnable> r(new SetStatusRunnable(
        aPort, aPort->DeviceState(), MIDIPortConnectionState::Closed));
    OwnerThread()->Dispatch(r.forget());
  }
}

void midirMIDIPlatformService::SendMessage(const nsAString& aPortId,
                                           const MIDIMessage& aMessage) {
  LOG("MIDI send message on %s", NS_ConvertUTF16toUTF8(aPortId).get());
  LogMIDIMessage(aMessage, aPortId, MIDIPortType::Output);
  midir_impl_send(mImplementation, &aPortId, &aMessage.data());
}
