// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "third_party/blink/renderer/modules/printing/web_printer.h"

#include <limits>

#include "base/task/single_thread_task_runner.h"
#include "base/types/expected_macros.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_web_print_job_template_attributes.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_web_printer_attributes.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_web_printing_media_collection_requested.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_web_printing_media_size_requested.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_web_printing_resolution.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/fileapi/blob.h"
#include "third_party/blink/renderer/modules/printing/web_print_job.h"
#include "third_party/blink/renderer/modules/printing/web_printing_type_converters.h"
#include "third_party/blink/renderer/platform/bindings/exception_code.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"

namespace blink {

namespace {

constexpr char kUserPermissionDeniedError[] =
    "User denied access to Web Printing API.";

constexpr char kPrinterUnreachableError[] = "Unable to connect to the printer.";

bool IsPositiveInt32(uint32_t value) {
  return value > 0 && value <= std::numeric_limits<int32_t>::max();
}

bool ValidatePrintJobTemplateAttributes(
    const WebPrintJobTemplateAttributes* pjt_attributes,
    ExceptionState& exception_state) {
  if (pjt_attributes->hasCopies() && pjt_attributes->copies() < 1) {
    exception_state.ThrowTypeError("|copies| cannot be less than 1.");
    return false;
  }
  if (pjt_attributes->hasPrinterResolution()) {
    auto* printer_resolution = pjt_attributes->printerResolution();
    if (!printer_resolution->hasCrossFeedDirectionResolution() ||
        !printer_resolution->hasFeedDirectionResolution()) {
      exception_state.ThrowTypeError(
          "crossFeedDirectionResolution and feedDirectionResolution must be "
          "specified if printerResolution is present.");
      return false;
    }
    if (printer_resolution->crossFeedDirectionResolution() == 0 ||
        printer_resolution->feedDirectionResolution() == 0) {
      exception_state.ThrowTypeError(
          "crossFeedDirectionResolution and feedDirectionResolution must be "
          "greater than 0 if specified.");
      return false;
    }
  }
  if (pjt_attributes->hasMediaCol()) {
    const auto& media_col = *pjt_attributes->mediaCol();
    const auto& media_size = *media_col.mediaSize();
    if (!IsPositiveInt32(media_size.yDimension()) ||
        !IsPositiveInt32(media_size.xDimension())) {
      exception_state.ThrowTypeError(
          "Both `xDimension` and `yDimension` must be positive integer "
          "values.");
      return false;
    }
  }
  return true;
}

}  // namespace

WebPrinter::WebPrinter(ExecutionContext* execution_context,
                       mojom::blink::WebPrinterInfoPtr printer_info)
    : printer_(execution_context) {
  printer_.Bind(std::move(printer_info->printer_remote),
                execution_context->GetTaskRunner(TaskType::kMiscPlatformAPI));
  attributes_ = WebPrinterAttributes::Create();
  attributes_->setPrinterName(printer_info->printer_name);
  attributes_->setPrinterId(printer_info->printer_id);
}

WebPrinter::~WebPrinter() = default;

void WebPrinter::Trace(Visitor* visitor) const {
  visitor->Trace(attributes_);
  visitor->Trace(fetch_attributes_resolver_);
  visitor->Trace(printer_);
  ScriptWrappable::Trace(visitor);
}

ScriptPromise<WebPrinterAttributes> WebPrinter::fetchAttributes(
    ScriptState* script_state,
    ExceptionState& exception_state) {
  if (!script_state->ContextIsValid()) {
    exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
                                      "Context has shut down.");
    return EmptyPromise();
  }

  if (fetch_attributes_resolver_) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kInvalidStateError,
        "A call to fetchAttributes() is already in progress.");
    return EmptyPromise();
  }

  fetch_attributes_resolver_ =
      MakeGarbageCollected<ScriptPromiseResolver<WebPrinterAttributes>>(
          script_state, exception_state.GetContext());
  printer_->FetchAttributes(
      fetch_attributes_resolver_->WrapCallbackInScriptScope(blink::BindOnce(
          &WebPrinter::OnFetchAttributes, WrapPersistent(this))));
  return fetch_attributes_resolver_->Promise();
}

ScriptPromise<WebPrintJob> WebPrinter::submitPrintJob(
    ScriptState* script_state,
    const String& job_name,
    Blob* document_data,
    const WebPrintJobTemplateAttributes* pjt_attributes,
    ExceptionState& exception_state) {
  if (!script_state->ContextIsValid()) {
    exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
                                      "Context has shut down.");
    return EmptyPromise();
  }

  if (!ValidatePrintJobTemplateAttributes(pjt_attributes, exception_state)) {
    return EmptyPromise();
  }

  auto attributes =
      mojo::ConvertTo<mojom::blink::WebPrintJobTemplateAttributesPtr>(
          pjt_attributes);
  attributes->job_name = job_name;

  auto* resolver = MakeGarbageCollected<ScriptPromiseResolver<WebPrintJob>>(
      script_state, exception_state.GetContext());
  printer_->Print(document_data->AsMojoBlob(), std::move(attributes),
                  resolver->WrapCallbackInScriptScope(blink::BindOnce(
                      &WebPrinter::OnPrint, WrapPersistent(this),
                      WrapPersistent(pjt_attributes->getSignalOr(nullptr)))));
  return resolver->Promise();
}

void WebPrinter::OnFetchAttributes(
    ScriptPromiseResolver<WebPrinterAttributes>*,
    mojom::blink::WebPrinter::FetchAttributesResult fetch_result) {
  ASSIGN_OR_RETURN(auto attributes, std::move(fetch_result), [&](auto error) {
    switch (error) {
      case mojom::blink::WebPrinterFetchError::kPrinterUnreachable:
        fetch_attributes_resolver_->RejectWithDOMException(
            DOMExceptionCode::kNetworkError, kPrinterUnreachableError);
        break;
      case mojom::blink::WebPrinterFetchError::kUserPermissionDenied:
        fetch_attributes_resolver_->RejectWithDOMException(
            DOMExceptionCode::kNotAllowedError, kUserPermissionDeniedError);
        break;
    }
    fetch_attributes_resolver_ = nullptr;
  });

  auto* new_attributes =
      mojo::ConvertTo<WebPrinterAttributes*>(std::move(attributes));
  new_attributes->setPrinterName(attributes_->printerName());
  new_attributes->setPrinterId(attributes_->printerId());
  attributes_ = new_attributes;

  fetch_attributes_resolver_->Resolve(attributes_);
  fetch_attributes_resolver_ = nullptr;
}

void WebPrinter::OnPrint(AbortSignal* signal,
                         ScriptPromiseResolver<WebPrintJob>* resolver,
                         mojom::blink::WebPrinter::PrintResult print_result) {
  ASSIGN_OR_RETURN(auto job_info, std::move(print_result), [&](auto error) {
    switch (error) {
      case mojom::blink::WebPrintError::kPrinterUnreachable:
        resolver->RejectWithDOMException(DOMExceptionCode::kNetworkError,
                                         kPrinterUnreachableError);
        break;
      case mojom::blink::WebPrintError::kPrintJobTemplateAttributesMismatch:
        resolver->RejectWithDOMException(
            DOMExceptionCode::kDataError,
            "The requested WebPrintJobTemplateAttributes do not align with the "
            "printer capabilities.");
        break;
      case mojom::blink::WebPrintError::kDocumentMalformed:
        resolver->RejectWithDOMException(DOMExceptionCode::kDataError,
                                         "The provided `data` is malformed.");
        break;
      case mojom::blink::WebPrintError::kUserPermissionDenied:
        resolver->RejectWithDOMException(DOMExceptionCode::kNotAllowedError,
                                         kUserPermissionDeniedError);
        break;
    }
  });

  auto* print_job = MakeGarbageCollected<WebPrintJob>(
      resolver->GetExecutionContext(), std::move(job_info), signal);
  resolver->Resolve(print_job);
}

}  // namespace blink
