/* 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 "TestMIDIPlatformService.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/ipc/BackgroundParent.h"
#include "mozilla/Unused.h"
#include "nsIThread.h"

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

/**
 * Runnable used for making sure ProcessMessages only happens on the IO thread.
 *
 */
class ProcessMessagesRunnable : public mozilla::Runnable {
 public:
  explicit ProcessMessagesRunnable(const nsAString& aPortID)
      : Runnable("ProcessMessagesRunnable"), mPortID(aPortID) {}
  ~ProcessMessagesRunnable() = default;
  NS_IMETHOD Run() override {
    // If service is no longer running, just exist without processing.
    if (!MIDIPlatformService::IsRunning()) {
      return NS_OK;
    }
    TestMIDIPlatformService* srv =
        static_cast<TestMIDIPlatformService*>(MIDIPlatformService::Get());
    srv->ProcessMessages(mPortID);
    return NS_OK;
  }

 private:
  nsString mPortID;
};

/**
 * Runnable used for allowing IO thread to queue more messages for processing,
 * since it can't access the service object directly.
 *
 */
class QueueMessagesRunnable : public MIDIBackgroundRunnable {
 public:
  QueueMessagesRunnable(const nsAString& aPortID,
                        const nsTArray<MIDIMessage>& aMsgs)
      : MIDIBackgroundRunnable("QueueMessagesRunnable"),
        mPortID(aPortID),
        mMsgs(aMsgs.Clone()) {}
  ~QueueMessagesRunnable() = default;
  virtual void RunInternal() {
    MIDIPlatformService::AssertThread();
    MIDIPlatformService::Get()->QueueMessages(mPortID, mMsgs);
  }

 private:
  nsString mPortID;
  nsTArray<MIDIMessage> mMsgs;
};

TestMIDIPlatformService::TestMIDIPlatformService()
    : mControlInputPort(u"b744eebe-f7d8-499b-872b-958f63c8f522"_ns,
                        u"Test Control MIDI Device Input Port"_ns,
                        u"Test Manufacturer"_ns, u"1.0.0"_ns,
                        static_cast<uint32_t>(MIDIPortType::Input)),
      mControlOutputPort(u"ab8e7fe8-c4de-436a-a960-30898a7c9a3d"_ns,
                         u"Test Control MIDI Device Output Port"_ns,
                         u"Test Manufacturer"_ns, u"1.0.0"_ns,
                         static_cast<uint32_t>(MIDIPortType::Output)),
      mStateTestInputPort(u"a9329677-8588-4460-a091-9d4a7f629a48"_ns,
                          u"Test State MIDI Device Input Port"_ns,
                          u"Test Manufacturer"_ns, u"1.0.0"_ns,
                          static_cast<uint32_t>(MIDIPortType::Input)),
      mStateTestOutputPort(u"478fa225-b5fc-4fa6-a543-d32d9cb651e7"_ns,
                           u"Test State MIDI Device Output Port"_ns,
                           u"Test Manufacturer"_ns, u"1.0.0"_ns,
                           static_cast<uint32_t>(MIDIPortType::Output)),
      mAlwaysClosedTestOutputPort(u"f87d0c76-3c68-49a9-a44f-700f1125c07a"_ns,
                                  u"Always Closed MIDI Device Output Port"_ns,
                                  u"Test Manufacturer"_ns, u"1.0.0"_ns,
                                  static_cast<uint32_t>(MIDIPortType::Output)),
      mDoRefresh(false),
      mIsInitialized(false) {
  MIDIPlatformService::AssertThread();
}

TestMIDIPlatformService::~TestMIDIPlatformService() {
  MIDIPlatformService::AssertThread();
}

void TestMIDIPlatformService::Init() {
  MIDIPlatformService::AssertThread();

  if (mIsInitialized) {
    return;
  }
  mIsInitialized = true;

  // Treat all of our special ports as always connected. When the service comes
  // up, prepopulate the port list with them.
  MIDIPlatformService::Get()->AddPortInfo(mControlInputPort);
  MIDIPlatformService::Get()->AddPortInfo(mControlOutputPort);
  MIDIPlatformService::Get()->AddPortInfo(mAlwaysClosedTestOutputPort);
  MIDIPlatformService::Get()->AddPortInfo(mStateTestOutputPort);
  nsCOMPtr<nsIRunnable> r(new SendPortListRunnable());

  // Start the IO Thread.
  OwnerThread()->Dispatch(r.forget());
}

void TestMIDIPlatformService::Refresh() {
  if (mDoRefresh) {
    AddPortInfo(mStateTestInputPort);
    mDoRefresh = false;
  }
}

void TestMIDIPlatformService::Open(MIDIPortParent* aPort) {
  MOZ_ASSERT(aPort);
  MIDIPortConnectionState s = MIDIPortConnectionState::Open;
  if (aPort->MIDIPortInterface::Id() == mAlwaysClosedTestOutputPort.id()) {
    // If it's the always closed testing port, act like it's already opened
    // exclusively elsewhere.
    s = MIDIPortConnectionState::Closed;
  }
  // Connection events are just simulated on the background thread, no need to
  // push to IO thread.
  nsCOMPtr<nsIRunnable> r(
      new SetStatusRunnable(aPort, aPort->DeviceState(), s));
  OwnerThread()->Dispatch(r.forget());
}

void TestMIDIPlatformService::ScheduleClose(MIDIPortParent* aPort) {
  AssertThread();
  MOZ_ASSERT(aPort);
  if (aPort->ConnectionState() == MIDIPortConnectionState::Open) {
    // Connection events are just simulated on the background thread, no need to
    // push to IO thread.
    nsCOMPtr<nsIRunnable> r(new SetStatusRunnable(
        aPort, aPort->DeviceState(), MIDIPortConnectionState::Closed));
    OwnerThread()->Dispatch(r.forget());
  }
}

void TestMIDIPlatformService::Stop() { MIDIPlatformService::AssertThread(); }

void TestMIDIPlatformService::ScheduleSend(const nsAString& aPortId) {
  AssertThread();
  nsCOMPtr<nsIRunnable> r(new ProcessMessagesRunnable(aPortId));
  OwnerThread()->Dispatch(r.forget());
}

void TestMIDIPlatformService::ProcessMessages(const nsAString& aPortId) {
  nsTArray<MIDIMessage> msgs;
  GetMessagesBefore(aPortId, TimeStamp::Now(), msgs);

  for (MIDIMessage msg : msgs) {
    // receiving message from test control port
    if (aPortId == mControlOutputPort.id()) {
      switch (msg.data()[0]) {
        // Hit a note, get a test!
        case 0x90:
          switch (msg.data()[1]) {
            // Echo data/timestamp back through output port
            case 0x00: {
              nsCOMPtr<nsIRunnable> r(
                  new ReceiveRunnable(mControlInputPort.id(), msg));
              OwnerThread()->Dispatch(r, NS_DISPATCH_NORMAL);
              break;
            }
            // Cause control test ports to connect
            case 0x01: {
              nsCOMPtr<nsIRunnable> r1(
                  new AddPortRunnable(mStateTestInputPort));
              OwnerThread()->Dispatch(r1, NS_DISPATCH_NORMAL);
              break;
            }
            // Cause control test ports to disconnect
            case 0x02: {
              nsCOMPtr<nsIRunnable> r1(
                  new RemovePortRunnable(mStateTestInputPort));
              OwnerThread()->Dispatch(r1, NS_DISPATCH_NORMAL);
              break;
            }
            // Test for packet timing
            case 0x03: {
              // Append a few echo command packets in reverse timing order,
              // should come out in correct order on other end.
              nsTArray<MIDIMessage> newMsgs;
              nsTArray<uint8_t> msg;
              msg.AppendElement(0x90);
              msg.AppendElement(0x00);
              msg.AppendElement(0x00);
              // PR_Now() returns nanosecods, and we need a double with
              // fractional milliseconds.
              TimeStamp currentTime = TimeStamp::Now();
              for (int i = 0; i <= 5; ++i) {
                // Insert messages with timestamps in reverse order, to make
                // sure we're sorting correctly.
                newMsgs.AppendElement(MIDIMessage(
                    msg, currentTime - TimeDuration::FromMilliseconds(i * 2)));
              }
              nsCOMPtr<nsIRunnable> r(
                  new QueueMessagesRunnable(aPortId, newMsgs));
              OwnerThread()->Dispatch(r, NS_DISPATCH_NORMAL);
              break;
            }
            // Causes the next refresh to add new ports to the list
            case 0x04: {
              mDoRefresh = true;
              break;
            }
            default:
              NS_WARNING("Unknown Test MIDI message received!");
          }
          break;
          // Sysex tests
        case 0xF0:
          switch (msg.data()[1]) {
              // Echo data/timestamp back through output port
            case 0x00: {
              nsCOMPtr<nsIRunnable> r(
                  new ReceiveRunnable(mControlInputPort.id(), msg));
              OwnerThread()->Dispatch(r, NS_DISPATCH_NORMAL);
              break;
            }
            // Test for system real time messages in the middle of sysex
            // messages.
            case 0x01: {
              nsTArray<uint8_t> msgs;
              const uint8_t msg[] = {0xF0, 0x01, 0xFA, 0x02, 0x03,
                                     0x04, 0xF8, 0x05, 0xF7};
              // Can't use AppendElements on an array here, so just do range
              // based loading.
              for (const auto& s : msg) {
                msgs.AppendElement(s);
              }
              nsTArray<MIDIMessage> newMsgs;
              MIDIUtils::ParseMessages(msgs, TimeStamp::Now(), newMsgs);
              nsCOMPtr<nsIRunnable> r(
                  new ReceiveRunnable(mControlInputPort.id(), newMsgs));
              OwnerThread()->Dispatch(r, NS_DISPATCH_NORMAL);
              break;
            }
            default:
              NS_WARNING("Unknown Test Sysex MIDI message received!");
          }
          break;
      }
    }
  }
}
