/* 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 "mozilla/dom/SerialPortParent.h"

#include "SerialLogging.h"
#include "SerialPortPumps.h"
#include "mozilla/Services.h"
#include "mozilla/dom/SerialManagerParent.h"
#include "mozilla/dom/SerialPlatformService.h"
#include "mozilla/dom/SerialPortIPCTypes.h"
#include "mozilla/ipc/DataPipe.h"
#include "mozilla/ipc/Endpoint.h"
#include "nsHashPropertyBag.h"
#include "nsIObserverService.h"
#include "nsStreamUtils.h"

namespace mozilla::dom {

SerialPortParent::SerialPortParent(const nsString& aPortId, uint64_t aBrowserId,
                                   SerialDeviceChangeProxy* aProxy)
    : mPortId(aPortId), mBrowserId(aBrowserId), mDeviceChangeProxy(aProxy) {
  MOZ_LOG(gWebSerialLog, LogLevel::Info,
          ("SerialPortParent[%p] created for port '%s'", this,
           NS_ConvertUTF16toUTF8(mPortId).get()));
}

SerialPortParent::~SerialPortParent() {
  MOZ_LOG(gWebSerialLog, LogLevel::Debug,
          ("SerialPortParent[%p] destroyed for port '%s'", this,
           NS_ConvertUTF16toUTF8(mPortId).get()));
}

mozilla::ipc::IPCResult SerialPortParent::RecvOpen(
    const IPCSerialOptions& aOptions, OpenResolver&& aResolver) {
  if (mIsOpen) {
    MOZ_LOG(gWebSerialLog, LogLevel::Warning,
            ("SerialPortParent[%p]::RecvOpen failed: port '%s' already open",
             this, NS_ConvertUTF16toUTF8(mPortId).get()));
    aResolver(NS_ERROR_ALREADY_INITIALIZED);
    return IPC_OK();
  }

  if (aOptions.bufferSize() > kMaxSerialBufferSize) {
    MOZ_LOG(
        gWebSerialLog, LogLevel::Warning,
        ("SerialPortParent[%p]::RecvOpen rejecting oversized bufferSize "
         "%u for port '%s'",
         this, aOptions.bufferSize(), NS_ConvertUTF16toUTF8(mPortId).get()));
    return IPC_FAIL(this, "bufferSize exceeds maximum");
  }

  MOZ_LOG(
      gWebSerialLog, LogLevel::Info,
      ("SerialPortParent[%p]::RecvOpen opening port '%s' (baudRate=%u, "
       "dataBits=%u, stopBits=%u, parity=%u, bufferSize=%u, flowControl=%u)",
       this, NS_ConvertUTF16toUTF8(mPortId).get(), aOptions.baudRate(),
       aOptions.dataBits(), aOptions.stopBits(),
       static_cast<unsigned>(aOptions.parity()), aOptions.bufferSize(),
       static_cast<unsigned>(aOptions.flowControl())));

  RefPtr<SerialPlatformService> service = SerialPlatformService::GetInstance();
  nsresult rv = NS_ERROR_FAILURE;

  if (service) {
    service->AssertIsOnIOThread();

    // Validate portId against enumerated ports before opening.
    bool portFound = false;
    SerialPortList ports;
    nsresult enumRv = service->EnumeratePorts(ports);
    if (NS_SUCCEEDED(enumRv)) {
      for (const auto& portInfo : ports) {
        if (portInfo.id() == mPortId) {
          portFound = true;
          break;
        }
      }
    }

    if (portFound) {
      rv = service->Open(mPortId, aOptions);
    } else {
      MOZ_LOG(gWebSerialLog, LogLevel::Warning,
              ("SerialPortParent[%p]::RecvOpen portId '%s' not found in "
               "enumerated ports, rejecting open",
               this, NS_ConvertUTF16toUTF8(mPortId).get()));
      rv = NS_ERROR_NOT_AVAILABLE;
    }
  }

  if (NS_FAILED(rv)) {
    MOZ_LOG(
        gWebSerialLog, LogLevel::Error,
        ("SerialPortParent[%p]::RecvOpen failed for port '%s': 0x%08x", this,
         NS_ConvertUTF16toUTF8(mPortId).get(), static_cast<uint32_t>(rv)));
    aResolver(rv);
    return IPC_OK();
  }

  MOZ_LOG(gWebSerialLog, LogLevel::Info,
          ("SerialPortParent[%p]::RecvOpen succeeded for port '%s'", this,
           NS_ConvertUTF16toUTF8(mPortId).get()));

  mIsOpen = true;
  mPipeCapacity = std::max(aOptions.bufferSize(), kMinSerialPortPumpSize);

  aResolver(NS_OK);
  return IPC_OK();
}

mozilla::ipc::IPCResult SerialPortParent::RecvClose(CloseResolver&& aResolver) {
  if (!mIsOpen) {
    MOZ_LOG(gWebSerialLog, LogLevel::Debug,
            ("SerialPortParent[%p]::RecvClose: port '%s' already closed", this,
             NS_ConvertUTF16toUTF8(mPortId).get()));
    aResolver(NS_OK);
    return IPC_OK();
  }

  MOZ_LOG(gWebSerialLog, LogLevel::Info,
          ("SerialPortParent[%p]::RecvClose closing port '%s'", this,
           NS_ConvertUTF16toUTF8(mPortId).get()));

  StopPumpsBeforeClose();

  RefPtr<SerialPlatformService> service = SerialPlatformService::GetInstance();
  nsresult rv = NS_ERROR_FAILURE;
  if (service) {
    service->AssertIsOnIOThread();
    rv = service->Close(mPortId);
  }

  if (NS_SUCCEEDED(rv)) {
    MOZ_LOG(gWebSerialLog, LogLevel::Info,
            ("SerialPortParent[%p]::RecvClose succeeded for port '%s'", this,
             NS_ConvertUTF16toUTF8(mPortId).get()));
  } else {
    MOZ_LOG(
        gWebSerialLog, LogLevel::Error,
        ("SerialPortParent[%p]::RecvClose failed for port '%s': 0x%08x", this,
         NS_ConvertUTF16toUTF8(mPortId).get(), static_cast<uint32_t>(rv)));
  }
  mIsOpen = false;

  aResolver(rv);
  return IPC_OK();
}

void SerialPortParent::StopReadPump() {
  if (mReadCopierCtx) {
    MOZ_LOG(gWebSerialLog, LogLevel::Debug,
            ("SerialPortParent[%p]::StopReadPump cancelling copy for port '%s'",
             this, NS_ConvertUTF16toUTF8(mPortId).get()));
    NS_CancelAsyncCopy(mReadCopierCtx, NS_BASE_STREAM_CLOSED);
    mReadCopierCtx = nullptr;
  }
  // mPlatformInputStream is kept alive: on Windows, IOCP associates the kernel
  // file object permanently, so re-registering a new duplicate handle fails.
  // The reader is only destroyed in DestroyPlatformReader (port close).
}

void SerialPortParent::DestroyPlatformReader() {
  if (!mPlatformInputStream) {
    return;
  }
  MOZ_LOG(gWebSerialLog, LogLevel::Debug,
          ("SerialPortParent[%p]::DestroyPlatformReader calling "
           "CloseWithStatus for port '%s'",
           this, NS_ConvertUTF16toUTF8(mPortId).get()));
  // PlatformPipeReader cancels pending I/O and dispatches the handle close
  // to a background thread with NS_DISPATCH_EVENT_MAY_BLOCK, so USB-serial
  // driver cleanup cannot block the IPC I/O thread.
  mPlatformInputStream->CloseWithStatus(NS_ERROR_ABORT);
  mPlatformInputStream = nullptr;
}

void SerialPortParent::StopWritePump() {
  if (mWritePump) {
    mWritePump->Stop();
    mWritePump = nullptr;
  }
  if (mWritePipeReceiver) {
    mWritePipeReceiver->CloseWithStatus(NS_BASE_STREAM_CLOSED);
    mWritePipeReceiver = nullptr;
  }
}

void SerialPortParent::StartReadPump(
    already_AddRefed<mozilla::ipc::DataPipeSender> aReadPipeSender) {
  mReadPipeSender = aReadPipeSender;

  RefPtr<SerialPlatformService> service = SerialPlatformService::GetInstance();
  if (!service) {
    return;
  }

  if (!mPlatformInputStream) {
    uint32_t bufferSize = std::max(mPipeCapacity, kMinSerialPortPumpSize);
    nsresult rv = service->GetReadStream(mPortId, bufferSize,
                                         getter_AddRefs(mPlatformInputStream));
    if (NS_FAILED(rv) || !mPlatformInputStream) {
      MOZ_LOG(gWebSerialLog, LogLevel::Error,
              ("SerialPortParent[%p]::StartReadPump GetReadStream failed for "
               "port '%s': 0x%08x",
               this, NS_ConvertUTF16toUTF8(mPortId).get(),
               static_cast<uint32_t>(rv)));
      mReadPipeSender->CloseWithStatus(NS_FAILED(rv) ? rv : NS_ERROR_FAILURE);
      mReadPipeSender = nullptr;
      return;
    }
  }

  // aCloseSource=false: PlatformPipeReader must outlive the copy because its
  // IOCP registration is permanent per file object on Windows, so we reuse
  // it across flush/reattach cycles.
  nsresult rv =
      NS_AsyncCopy(mPlatformInputStream, mReadPipeSender, service->IOThread(),
                   NS_ASYNCCOPY_VIA_READSEGMENTS, 4096, nullptr, nullptr,
                   /* aCloseSource = */ false, /* aCloseSink = */ true,
                   getter_AddRefs(mReadCopierCtx));
  if (NS_FAILED(rv)) {
    MOZ_LOG(gWebSerialLog, LogLevel::Error,
            ("SerialPortParent[%p]::StartReadPump NS_AsyncCopy failed for "
             "port '%s': 0x%08x",
             this, NS_ConvertUTF16toUTF8(mPortId).get(),
             static_cast<uint32_t>(rv)));
    mReadPipeSender->CloseWithStatus(rv);
    mReadPipeSender = nullptr;
    return;
  }

  MOZ_LOG(gWebSerialLog, LogLevel::Info,
          ("SerialPortParent[%p]::StartReadPump started async read for "
           "port '%s' (pipeCapacity=%u)",
           this, NS_ConvertUTF16toUTF8(mPortId).get(), mPipeCapacity));
}

void SerialPortParent::StartWritePump(
    already_AddRefed<mozilla::ipc::DataPipeReceiver> aWritePipeReceiver) {
  mWritePipeReceiver = aWritePipeReceiver;

  mWritePump =
      MakeRefPtr<webserial::SerialPortWritePump>(mPortId, mWritePipeReceiver);
  mWritePump->Start();
}

mozilla::ipc::IPCResult SerialPortParent::RecvAttachReadPipe(
    const RefPtr<mozilla::ipc::DataPipeSender>& aReadPipeSender) {
  if (!mIsOpen || !aReadPipeSender) {
    MOZ_LOG(gWebSerialLog, LogLevel::Warning,
            ("SerialPortParent[%p]::RecvAttachReadPipe: port '%s' not open",
             this, NS_ConvertUTF16toUTF8(mPortId).get()));
    if (aReadPipeSender) {
      aReadPipeSender->CloseWithStatus(NS_ERROR_NOT_AVAILABLE);
    }
    return IPC_OK();
  }

  MOZ_LOG(gWebSerialLog, LogLevel::Info,
          ("SerialPortParent[%p]::RecvAttachReadPipe for port '%s'", this,
           NS_ConvertUTF16toUTF8(mPortId).get()));

  // Clean up any existing read pipe/pump from a previous readable stream.
  StopReadPump();

  RefPtr<mozilla::ipc::DataPipeSender> sender = aReadPipeSender;
  StartReadPump(sender.forget());

  return IPC_OK();
}

mozilla::ipc::IPCResult SerialPortParent::RecvAttachWritePipe(
    const RefPtr<mozilla::ipc::DataPipeReceiver>& aWritePipeReceiver) {
  if (!mIsOpen || !aWritePipeReceiver) {
    MOZ_LOG(gWebSerialLog, LogLevel::Warning,
            ("SerialPortParent[%p]::RecvAttachWritePipe: port '%s' not open",
             this, NS_ConvertUTF16toUTF8(mPortId).get()));
    if (aWritePipeReceiver) {
      aWritePipeReceiver->CloseWithStatus(NS_ERROR_NOT_AVAILABLE);
    }
    return IPC_OK();
  }

  MOZ_LOG(gWebSerialLog, LogLevel::Info,
          ("SerialPortParent[%p]::RecvAttachWritePipe for port '%s'", this,
           NS_ConvertUTF16toUTF8(mPortId).get()));

  // Clean up any existing write pipe/pump from a previous writable stream.
  StopWritePump();

  RefPtr<mozilla::ipc::DataPipeReceiver> receiver = aWritePipeReceiver;
  StartWritePump(receiver.forget());

  return IPC_OK();
}

mozilla::ipc::IPCResult SerialPortParent::RecvSetSignals(
    const IPCSerialOutputSignals& aSignals, SetSignalsResolver&& aResolver) {
  if (!mIsOpen) {
    aResolver(NS_ERROR_NOT_AVAILABLE);
    return IPC_OK();
  }

  RefPtr<SerialPlatformService> service = SerialPlatformService::GetInstance();
  nsresult rv = NS_ERROR_FAILURE;
  if (service) {
    service->AssertIsOnIOThread();
    rv = service->SetSignals(mPortId, aSignals);
  }

  aResolver(rv);
  return IPC_OK();
}

mozilla::ipc::IPCResult SerialPortParent::RecvGetSignals(
    GetSignalsResolver&& aResolver) {
  IPCSerialInputSignals signals(
      /* dataCarrierDetect */ false, /* clearToSend */ false,
      /* ringIndicator */ false, /* dataSetReady */ false);

  if (!mIsOpen) {
    aResolver(std::tuple(NS_ERROR_NOT_AVAILABLE, signals));
    return IPC_OK();
  }

  RefPtr<SerialPlatformService> service = SerialPlatformService::GetInstance();
  nsresult rv = NS_ERROR_FAILURE;
  if (service) {
    service->AssertIsOnIOThread();
    rv = service->GetSignals(mPortId, signals);
  }

  aResolver(std::tuple(rv, signals));
  return IPC_OK();
}

mozilla::ipc::IPCResult SerialPortParent::RecvDrain(DrainResolver&& aResolver) {
  if (!mIsOpen) {
    MOZ_LOG(gWebSerialLog, LogLevel::Error,
            ("SerialPortParent[%p]::RecvDrain failed: port '%s' not open", this,
             NS_ConvertUTF16toUTF8(mPortId).get()));
    aResolver(NS_ERROR_NOT_AVAILABLE);
    return IPC_OK();
  }

  MOZ_LOG(gWebSerialLog, LogLevel::Debug,
          ("SerialPortParent[%p]::RecvDrain draining buffers for port '%s'",
           this, NS_ConvertUTF16toUTF8(mPortId).get()));

  // There is no ordering guarantee between DataPipe notifications and IPC
  // messages. The child closes its DataPipeSender before sending Drain, but
  // the DataPipe closure notification may not have reached the parent yet,
  // meaning the write pump may still have unconsumed data. We must wait for
  // the write pipe to be fully closed (all data written to the device)
  // before draining OS transmit buffers.
  auto completeDrain = [portId = mPortId, aResolver]() {
    RefPtr<SerialPlatformService> service =
        SerialPlatformService::GetInstance();
    nsresult rv = NS_ERROR_FAILURE;
    if (service) {
      service->AssertIsOnIOThread();
      rv = service->Drain(portId);
    }
    aResolver(rv);
  };

  if (mWritePump) {
    mWritePump->OnPipeClosed(
        NS_NewRunnableFunction("DrainAfterPipeClosed", completeDrain));
    return IPC_OK();
  }
  completeDrain();
  return IPC_OK();
}

mozilla::ipc::IPCResult SerialPortParent::RecvFlush(bool aReceive,
                                                    FlushResolver&& aResolver) {
  if (!mIsOpen) {
    MOZ_LOG(gWebSerialLog, LogLevel::Error,
            ("SerialPortParent[%p]::RecvFlush failed: port '%s' not open", this,
             NS_ConvertUTF16toUTF8(mPortId).get()));
    aResolver(NS_ERROR_NOT_AVAILABLE);
    return IPC_OK();
  }

  MOZ_LOG(gWebSerialLog, LogLevel::Debug,
          ("SerialPortParent[%p]::RecvFlush flushing %s buffers for port '%s'",
           this, aReceive ? "receive" : "transmit",
           NS_ConvertUTF16toUTF8(mPortId).get()));

  if (aReceive) {
    StopReadPump();
  } else {
    StopWritePump();
  }

  RefPtr<SerialPlatformService> service = SerialPlatformService::GetInstance();
  nsresult rv = NS_ERROR_FAILURE;
  if (service) {
    service->AssertIsOnIOThread();
    rv = service->Flush(mPortId, aReceive);
  }

  aResolver(rv);
  return IPC_OK();
}

void SerialPortParent::NotifySharingStateChanged(bool aConnected) {
  mSharingConnected = aConnected;

  NS_DispatchToMainThread(NS_NewRunnableFunction(
      "SerialPortParent::NotifySharingStateChanged",
      [browserId = mBrowserId, aConnected]() {
        nsCOMPtr<nsIObserverService> obs =
            mozilla::services::GetObserverService();
        if (!obs) {
          return;
        }

        auto props = MakeRefPtr<nsHashPropertyBag>();
        props->SetPropertyAsUint64(u"browserId"_ns, browserId);
        props->SetPropertyAsBool(u"connected"_ns, aConnected);

        obs->NotifyObservers(static_cast<nsIPropertyBag2*>(props),
                             "serial-device-state-changed", nullptr);
      }));
}

mozilla::ipc::IPCResult SerialPortParent::RecvUpdateSharingState(
    bool aConnected) {
  if (aConnected != mSharingConnected) {
    NotifySharingStateChanged(aConnected);
  } else {
    MOZ_LOG(gWebSerialLog, LogLevel::Warning,
            ("SerialPortParent[%p]::RecvUpdateSharingState got same state of "
             "%d for port '%s'",
             this, aConnected ? 1 : 0, NS_ConvertUTF16toUTF8(mPortId).get()));
  }
  return IPC_OK();
}

mozilla::ipc::IPCResult SerialPortParent::RecvClone(
    mozilla::ipc::Endpoint<PSerialPortParent>&& aEndpoint) {
  if (!aEndpoint.IsValid()) {
    return IPC_FAIL(this, "Invalid endpoint in RecvClone");
  }

  auto actor = MakeRefPtr<SerialPortParent>(mPortId, mBrowserId);
  if (!aEndpoint.Bind(actor)) {
    MOZ_LOG(gWebSerialLog, LogLevel::Error,
            ("SerialPortParent[%p]::RecvClone failed to bind for port '%s'",
             this, NS_ConvertUTF16toUTF8(mPortId).get()));
    return IPC_OK();
  }

  mClones.AppendElement(actor);
  return IPC_OK();
}

void SerialPortParent::NotifyConnected() {
  if (CanSend()) {
    (void)SendConnected();
  }
  for (const auto& clone : mClones) {
    clone->NotifyConnected();
  }
}

void SerialPortParent::NotifyDisconnected() {
  MOZ_LOG(gWebSerialLog, LogLevel::Info,
          ("SerialPortParent[%p]::NotifyDisconnected for port '%s'", this,
           NS_ConvertUTF16toUTF8(mPortId).get()));

  if (mIsOpen) {
    StopPumpsBeforeClose();
    mIsOpen = false;
    RefPtr<SerialPlatformService> service =
        SerialPlatformService::GetInstance();
    if (service) {
      service->Close(mPortId);
    }
  }

  if (CanSend()) {
    (void)SendDisconnected();
  }
  for (const auto& clone : mClones) {
    clone->NotifyDisconnected();
  }
}

void SerialPortParent::StopPumpsBeforeClose() {
  MOZ_LOG(gWebSerialLog, LogLevel::Debug,
          ("SerialPortParent[%p]::StopPumpsBeforeClose for port '%s' "
           "(hasReader=%d, "
           "hasCopier=%d, hasWritePump=%d)",
           this, NS_ConvertUTF16toUTF8(mPortId).get(), !!mPlatformInputStream,
           !!mReadCopierCtx, !!mWritePump));
  StopReadPump();
  DestroyPlatformReader();
  StopWritePump();
}

void SerialPortParent::ActorDestroy(ActorDestroyReason aWhy) {
  MOZ_LOG(gWebSerialLog, LogLevel::Debug,
          ("SerialPortParent[%p]::ActorDestroy for port '%s' (reason: %d)",
           this, NS_ConvertUTF16toUTF8(mPortId).get(), (int)aWhy));

  if (mDeviceChangeProxy) {
    mDeviceChangeProxy->RemovePortActor(this);
    mDeviceChangeProxy = nullptr;
  }

  StopPumpsBeforeClose();

  if (mIsOpen) {
    RefPtr<SerialPlatformService> service =
        SerialPlatformService::GetInstance();
    if (service) {
      service->Close(mPortId);
    }
    mIsOpen = false;
  }

  // If the child sent connected=true but never sent connected=false (e.g.
  // content process crash or iframe navigation), send the disconnect
  // notification so the browser sharing indicator count stays in sync.
  if (mSharingConnected) {
    NotifySharingStateChanged(false);
  }

  nsTArray<RefPtr<SerialPortParent>> clones = std::move(mClones);
  for (const auto& clone : clones) {
    if (clone->CanSend()) {
      clone->Close();
    }
  }
}

}  // namespace mozilla::dom
