/* 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 "PaymentRequestService.h"

#include "BasicCardPayment.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/dom/BasicCardPaymentBinding.h"
#include "mozilla/dom/PaymentRequestParent.h"
#include "nsArrayUtils.h"
#include "nsCOMPtr.h"
#include "nsComponentManagerUtils.h"
#include "nsIMutableArray.h"
#include "nsServiceManagerUtils.h"
#include "nsSimpleEnumerator.h"

namespace mozilla::dom {

StaticRefPtr<PaymentRequestService> gPaymentService;

namespace {

class PaymentRequestEnumerator final : public nsSimpleEnumerator {
 public:
  NS_DECL_NSISIMPLEENUMERATOR

  PaymentRequestEnumerator() : mIndex(0) {}

  const nsID& DefaultInterface() override {
    return NS_GET_IID(nsIPaymentRequest);
  }

 private:
  ~PaymentRequestEnumerator() override = default;
  uint32_t mIndex;
};

NS_IMETHODIMP
PaymentRequestEnumerator::HasMoreElements(bool* aReturn) {
  NS_ENSURE_ARG_POINTER(aReturn);
  *aReturn = false;
  if (NS_WARN_IF(!gPaymentService)) {
    return NS_ERROR_FAILURE;
  }
  RefPtr<PaymentRequestService> service = gPaymentService;
  *aReturn = mIndex < service->NumPayments();
  return NS_OK;
}

NS_IMETHODIMP
PaymentRequestEnumerator::GetNext(nsISupports** aItem) {
  NS_ENSURE_ARG_POINTER(aItem);
  if (NS_WARN_IF(!gPaymentService)) {
    return NS_ERROR_FAILURE;
  }
  RefPtr<payments::PaymentRequest> rowRequest =
      gPaymentService->GetPaymentRequestByIndex(mIndex);
  if (!rowRequest) {
    return NS_ERROR_FAILURE;
  }
  mIndex++;
  rowRequest.forget(aItem);
  return NS_OK;
}

}  // end of anonymous namespace

/* PaymentRequestService */

NS_IMPL_ISUPPORTS(PaymentRequestService, nsIPaymentRequestService)

already_AddRefed<PaymentRequestService> PaymentRequestService::GetSingleton() {
  MOZ_ASSERT(NS_IsMainThread());
  if (!gPaymentService) {
    gPaymentService = new PaymentRequestService();
    ClearOnShutdown(&gPaymentService);
  }
  RefPtr<PaymentRequestService> service = gPaymentService;
  return service.forget();
}

uint32_t PaymentRequestService::NumPayments() const {
  return mRequestQueue.Length();
}

already_AddRefed<payments::PaymentRequest>
PaymentRequestService::GetPaymentRequestByIndex(const uint32_t aIndex) {
  if (aIndex >= mRequestQueue.Length()) {
    return nullptr;
  }
  RefPtr<payments::PaymentRequest> request = mRequestQueue[aIndex];
  MOZ_ASSERT(request);
  return request.forget();
}

NS_IMETHODIMP
PaymentRequestService::GetPaymentRequestById(const nsAString& aRequestId,
                                             nsIPaymentRequest** aRequest) {
  NS_ENSURE_ARG_POINTER(aRequest);
  *aRequest = nullptr;
  RefPtr<payments::PaymentRequest> rowRequest;
  nsresult rv = GetPaymentRequestById(aRequestId, getter_AddRefs(rowRequest));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }
  rowRequest.forget(aRequest);
  return NS_OK;
}

nsresult PaymentRequestService::GetPaymentRequestById(
    const nsAString& aRequestId, payments::PaymentRequest** aRequest) {
  NS_ENSURE_ARG_POINTER(aRequest);
  *aRequest = nullptr;
  uint32_t numRequests = mRequestQueue.Length();
  for (uint32_t index = 0; index < numRequests; ++index) {
    RefPtr<payments::PaymentRequest> request = mRequestQueue[index];
    MOZ_ASSERT(request);
    nsAutoString requestId;
    nsresult rv = request->GetRequestId(requestId);
    NS_ENSURE_SUCCESS(rv, rv);
    if (requestId == aRequestId) {
      request.forget(aRequest);
      break;
    }
  }
  return NS_OK;
}

NS_IMETHODIMP
PaymentRequestService::Enumerate(nsISimpleEnumerator** aEnumerator) {
  NS_ENSURE_ARG_POINTER(aEnumerator);
  nsCOMPtr<nsISimpleEnumerator> enumerator = new PaymentRequestEnumerator();
  enumerator.forget(aEnumerator);
  return NS_OK;
}

NS_IMETHODIMP
PaymentRequestService::Cleanup() {
  mRequestQueue.Clear();
  return NS_OK;
}

NS_IMETHODIMP
PaymentRequestService::SetTestingUIService(nsIPaymentUIService* aUIService) {
  // aUIService can be nullptr
  mTestingUIService = aUIService;
  return NS_OK;
}

nsresult PaymentRequestService::LaunchUIAction(const nsAString& aRequestId,
                                               uint32_t aActionType) {
  nsCOMPtr<nsIPaymentUIService> uiService;
  nsresult rv;
  if (mTestingUIService) {
    uiService = mTestingUIService;
  } else {
    uiService = do_GetService(NS_PAYMENT_UI_SERVICE_CONTRACT_ID, &rv);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }
  }
  switch (aActionType) {
    case IPCPaymentActionRequest::TIPCPaymentShowActionRequest: {
      rv = uiService->ShowPayment(aRequestId);
      break;
    }
    case IPCPaymentActionRequest::TIPCPaymentAbortActionRequest: {
      rv = uiService->AbortPayment(aRequestId);
      break;
    }
    case IPCPaymentActionRequest::TIPCPaymentCompleteActionRequest: {
      rv = uiService->CompletePayment(aRequestId);
      break;
    }
    case IPCPaymentActionRequest::TIPCPaymentUpdateActionRequest: {
      rv = uiService->UpdatePayment(aRequestId);
      break;
    }
    case IPCPaymentActionRequest::TIPCPaymentCloseActionRequest: {
      rv = uiService->ClosePayment(aRequestId);
      break;
    }
    default: {
      return NS_ERROR_FAILURE;
    }
  }
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }
  return NS_OK;
}

nsresult PaymentRequestService::RequestPayment(
    const nsAString& aRequestId, const IPCPaymentActionRequest& aAction,
    PaymentRequestParent* aIPC) {
  NS_ENSURE_ARG_POINTER(aIPC);

  nsresult rv = NS_OK;
  uint32_t type = aAction.type();

  RefPtr<payments::PaymentRequest> request;
  if (type != IPCPaymentActionRequest::TIPCPaymentCreateActionRequest) {
    rv = GetPaymentRequestById(aRequestId, getter_AddRefs(request));
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }
    if (!request &&
        type != IPCPaymentActionRequest::TIPCPaymentCloseActionRequest) {
      return NS_ERROR_FAILURE;
    }
    if (request) {
      request->SetIPC(aIPC);
    }
  }

  switch (type) {
    case IPCPaymentActionRequest::TIPCPaymentCreateActionRequest: {
      MOZ_ASSERT(!request);
      const IPCPaymentCreateActionRequest& action = aAction;
      nsCOMPtr<nsIMutableArray> methodData =
          do_CreateInstance(NS_ARRAY_CONTRACTID);
      MOZ_ASSERT(methodData);
      for (const IPCPaymentMethodData& data : action.methodData()) {
        nsCOMPtr<nsIPaymentMethodData> method;
        rv = payments::PaymentMethodData::Create(data, getter_AddRefs(method));
        NS_ENSURE_SUCCESS(rv, rv);
        rv = methodData->AppendElement(method);
        NS_ENSURE_SUCCESS(rv, rv);
      }
      nsCOMPtr<nsIPaymentDetails> details;
      rv = payments::PaymentDetails::Create(action.details(),
                                            getter_AddRefs(details));
      NS_ENSURE_SUCCESS(rv, rv);
      nsCOMPtr<nsIPaymentOptions> options;
      rv = payments::PaymentOptions::Create(action.options(),
                                            getter_AddRefs(options));
      NS_ENSURE_SUCCESS(rv, rv);
      RefPtr<payments::PaymentRequest> request = new payments::PaymentRequest(
          action.topOuterWindowId(), aRequestId, action.topLevelPrincipal(),
          methodData, details, options, action.shippingOption());

      if (!mRequestQueue.AppendElement(request, mozilla::fallible)) {
        return NS_ERROR_OUT_OF_MEMORY;
      }
      break;
    }
    case IPCPaymentActionRequest::TIPCPaymentCanMakeActionRequest: {
      nsCOMPtr<nsIPaymentCanMakeActionResponse> canMakeResponse =
          do_CreateInstance(NS_PAYMENT_CANMAKE_ACTION_RESPONSE_CONTRACT_ID);
      MOZ_ASSERT(canMakeResponse);
      rv = canMakeResponse->Init(aRequestId, CanMakePayment(aRequestId));
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
      rv = RespondPayment(canMakeResponse.get());
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
      break;
    }
    case IPCPaymentActionRequest::TIPCPaymentShowActionRequest: {
      const IPCPaymentShowActionRequest& action = aAction;
      rv = ShowPayment(aRequestId, action.isUpdating());
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
      break;
    }
    case IPCPaymentActionRequest::TIPCPaymentAbortActionRequest: {
      MOZ_ASSERT(request);
      request->SetState(payments::PaymentRequest::eInteractive);
      rv = LaunchUIAction(aRequestId, type);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
      break;
    }
    case IPCPaymentActionRequest::TIPCPaymentCompleteActionRequest: {
      MOZ_ASSERT(request);
      const IPCPaymentCompleteActionRequest& action = aAction;
      request->SetCompleteStatus(action.completeStatus());
      rv = LaunchUIAction(aRequestId, type);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
      break;
    }
    case IPCPaymentActionRequest::TIPCPaymentUpdateActionRequest: {
      const IPCPaymentUpdateActionRequest& action = aAction;
      nsCOMPtr<nsIPaymentDetails> details;
      rv = payments::PaymentDetails::Create(action.details(),
                                            getter_AddRefs(details));
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
      MOZ_ASSERT(request);
      rv = request->UpdatePaymentDetails(details, action.shippingOption());
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
      nsAutoString completeStatus;
      rv = request->GetCompleteStatus(completeStatus);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
      if (completeStatus.Equals(u"initial"_ns)) {
        request->SetCompleteStatus(u""_ns);
      }
      MOZ_ASSERT(mShowingRequest && mShowingRequest == request);
      rv = LaunchUIAction(aRequestId, type);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
      break;
    }
    case IPCPaymentActionRequest::TIPCPaymentCloseActionRequest: {
      rv = LaunchUIAction(aRequestId, type);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
      if (mShowingRequest == request) {
        mShowingRequest = nullptr;
      }
      mRequestQueue.RemoveElement(request);
      break;
    }
    case IPCPaymentActionRequest::TIPCPaymentRetryActionRequest: {
      const IPCPaymentRetryActionRequest& action = aAction;
      MOZ_ASSERT(request);
      request->UpdateErrors(action.error(), action.payerErrors(),
                            action.paymentMethodErrors(),
                            action.shippingAddressErrors());
      request->SetState(payments::PaymentRequest::eInteractive);
      MOZ_ASSERT(mShowingRequest == request);
      rv = LaunchUIAction(
          aRequestId, IPCPaymentActionRequest::TIPCPaymentUpdateActionRequest);
      break;
    }
    default: {
      return NS_ERROR_FAILURE;
    }
  }
  return NS_OK;
}

NS_IMETHODIMP
PaymentRequestService::RespondPayment(nsIPaymentActionResponse* aResponse) {
  NS_ENSURE_ARG_POINTER(aResponse);
  nsAutoString requestId;
  nsresult rv = aResponse->GetRequestId(requestId);
  NS_ENSURE_SUCCESS(rv, rv);

  RefPtr<payments::PaymentRequest> request;
  rv = GetPaymentRequestById(requestId, getter_AddRefs(request));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }
  if (!request) {
    return NS_ERROR_FAILURE;
  }
  uint32_t type;
  rv = aResponse->GetType(&type);
  NS_ENSURE_SUCCESS(rv, rv);

  // PaymentRequest can only be responded when
  // 1. the state is eInteractive
  // 2. the state is eClosed and response type is COMPLETE_ACTION
  // 3. the state is eCreated and response type is CANMAKE_ACTION
  payments::PaymentRequest::eState state = request->GetState();
  bool canBeResponded = (state == payments::PaymentRequest::eInteractive) ||
                        (state == payments::PaymentRequest::eClosed &&
                         type == nsIPaymentActionResponse::COMPLETE_ACTION) ||
                        (state == payments::PaymentRequest::eCreated &&
                         type == nsIPaymentActionResponse::CANMAKE_ACTION);
  if (!canBeResponded) {
    return NS_ERROR_FAILURE;
  }

  if (!request->GetIPC()) {
    return NS_ERROR_FAILURE;
  }
  rv = request->GetIPC()->RespondPayment(aResponse);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Remove PaymentRequest from mRequestQueue while receive succeeded abort
  // response or complete response
  switch (type) {
    case nsIPaymentActionResponse::ABORT_ACTION: {
      nsCOMPtr<nsIPaymentAbortActionResponse> response =
          do_QueryInterface(aResponse);
      MOZ_ASSERT(response);
      bool isSucceeded;
      rv = response->IsSucceeded(&isSucceeded);
      NS_ENSURE_SUCCESS(rv, rv);
      mShowingRequest = nullptr;
      if (isSucceeded) {
        mRequestQueue.RemoveElement(request);
        request->SetState(payments::PaymentRequest::eClosed);
      }
      break;
    }
    case nsIPaymentActionResponse::SHOW_ACTION: {
      request->SetState(payments::PaymentRequest::eClosed);
      nsCOMPtr<nsIPaymentShowActionResponse> response =
          do_QueryInterface(aResponse);
      MOZ_ASSERT(response);
      uint32_t acceptStatus;
      rv = response->GetAcceptStatus(&acceptStatus);
      NS_ENSURE_SUCCESS(rv, rv);
      if (acceptStatus != nsIPaymentActionResponse::PAYMENT_ACCEPTED) {
        // Check if rejecting the showing PaymentRequest.
        // If yes, set mShowingRequest as nullptr.
        if (mShowingRequest == request) {
          mShowingRequest = nullptr;
        }
        mRequestQueue.RemoveElement(request);
      }
      break;
    }
    case nsIPaymentActionResponse::COMPLETE_ACTION: {
      mShowingRequest = nullptr;
      mRequestQueue.RemoveElement(request);
      break;
    }
    default: {
      break;
    }
  }
  return NS_OK;
}

NS_IMETHODIMP
PaymentRequestService::ChangeShippingAddress(const nsAString& aRequestId,
                                             nsIPaymentAddress* aAddress) {
  RefPtr<payments::PaymentRequest> request;
  nsresult rv = GetPaymentRequestById(aRequestId, getter_AddRefs(request));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }
  if (!request) {
    return NS_ERROR_FAILURE;
  }
  if (request->GetState() != payments::PaymentRequest::eInteractive) {
    return NS_ERROR_FAILURE;
  }
  if (!request->GetIPC()) {
    return NS_ERROR_FAILURE;
  }
  rv = request->GetIPC()->ChangeShippingAddress(aRequestId, aAddress);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }
  return NS_OK;
}

NS_IMETHODIMP
PaymentRequestService::ChangeShippingOption(const nsAString& aRequestId,
                                            const nsAString& aOption) {
  RefPtr<payments::PaymentRequest> request;
  nsresult rv = GetPaymentRequestById(aRequestId, getter_AddRefs(request));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }
  if (!request) {
    return NS_ERROR_FAILURE;
  }
  if (request->GetState() != payments::PaymentRequest::eInteractive) {
    return NS_ERROR_FAILURE;
  }
  if (!request->GetIPC()) {
    return NS_ERROR_FAILURE;
  }
  rv = request->GetIPC()->ChangeShippingOption(aRequestId, aOption);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }
  return NS_OK;
}

NS_IMETHODIMP
PaymentRequestService::ChangePayerDetail(const nsAString& aRequestId,
                                         const nsAString& aPayerName,
                                         const nsAString& aPayerEmail,
                                         const nsAString& aPayerPhone) {
  RefPtr<payments::PaymentRequest> request;
  nsresult rv = GetPaymentRequestById(aRequestId, getter_AddRefs(request));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }
  MOZ_ASSERT(request);
  if (!request->GetIPC()) {
    return NS_ERROR_FAILURE;
  }
  rv = request->GetIPC()->ChangePayerDetail(aRequestId, aPayerName, aPayerEmail,
                                            aPayerPhone);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }
  return NS_OK;
}

NS_IMETHODIMP
PaymentRequestService::ChangePaymentMethod(
    const nsAString& aRequestId, const nsAString& aMethodName,
    nsIMethodChangeDetails* aMethodDetails) {
  RefPtr<payments::PaymentRequest> request;
  nsresult rv = GetPaymentRequestById(aRequestId, getter_AddRefs(request));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }
  if (!request) {
    return NS_ERROR_FAILURE;
  }
  if (request->GetState() != payments::PaymentRequest::eInteractive) {
    return NS_ERROR_FAILURE;
  }
  if (!request->GetIPC()) {
    return NS_ERROR_FAILURE;
  }
  rv = request->GetIPC()->ChangePaymentMethod(aRequestId, aMethodName,
                                              aMethodDetails);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }
  return NS_OK;
}

bool PaymentRequestService::CanMakePayment(const nsAString& aRequestId) {
  /*
   *  TODO: Check third party payment app support by traversing all
   *        registered third party payment apps.
   */
  return IsBasicCardPayment(aRequestId);
}

nsresult PaymentRequestService::ShowPayment(const nsAString& aRequestId,
                                            bool aIsUpdating) {
  nsresult rv;
  RefPtr<payments::PaymentRequest> request;
  rv = GetPaymentRequestById(aRequestId, getter_AddRefs(request));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }
  MOZ_ASSERT(request);
  request->SetState(payments::PaymentRequest::eInteractive);
  if (aIsUpdating) {
    request->SetCompleteStatus(u"initial"_ns);
  }

  if (mShowingRequest || !CanMakePayment(aRequestId)) {
    uint32_t responseStatus;
    if (mShowingRequest) {
      responseStatus = nsIPaymentActionResponse::PAYMENT_REJECTED;
    } else {
      responseStatus = nsIPaymentActionResponse::PAYMENT_NOTSUPPORTED;
    }
    nsCOMPtr<nsIPaymentShowActionResponse> showResponse =
        do_CreateInstance(NS_PAYMENT_SHOW_ACTION_RESPONSE_CONTRACT_ID);
    MOZ_ASSERT(showResponse);
    rv = showResponse->Init(aRequestId, responseStatus, u""_ns, nullptr, u""_ns,
                            u""_ns, u""_ns);
    rv = RespondPayment(showResponse.get());
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }
  } else {
    mShowingRequest = request;
    rv = LaunchUIAction(aRequestId,
                        IPCPaymentActionRequest::TIPCPaymentShowActionRequest);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }
  }
  return NS_OK;
}

bool PaymentRequestService::IsBasicCardPayment(const nsAString& aRequestId) {
  RefPtr<payments::PaymentRequest> request;
  nsresult rv = GetPaymentRequestById(aRequestId, getter_AddRefs(request));
  NS_ENSURE_SUCCESS(rv, false);
  nsCOMPtr<nsIArray> methods;
  rv = request->GetPaymentMethods(getter_AddRefs(methods));
  NS_ENSURE_SUCCESS(rv, false);
  uint32_t length;
  rv = methods->GetLength(&length);
  NS_ENSURE_SUCCESS(rv, false);
  RefPtr<BasicCardService> service = BasicCardService::GetService();
  MOZ_ASSERT(service);
  for (uint32_t index = 0; index < length; ++index) {
    nsCOMPtr<nsIPaymentMethodData> method = do_QueryElementAt(methods, index);
    MOZ_ASSERT(method);
    nsAutoString supportedMethods;
    rv = method->GetSupportedMethods(supportedMethods);
    NS_ENSURE_SUCCESS(rv, false);
    if (service->IsBasicCardPayment(supportedMethods)) {
      return true;
    }
  }
  return false;
}

}  // namespace mozilla::dom
