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

#include "printing/printing_context_mac.h"

#import <AppKit/AppKit.h>
#include <CoreFoundation/CoreFoundation.h>
#import <QuartzCore/QuartzCore.h>
#include <cups/cups.h>

#import <iomanip>
#import <numeric>

#include "base/apple/bridging.h"
#include "base/apple/foundation_util.h"
#include "base/apple/osstatus_logging.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/apple/scoped_typeref.h"
#include "base/check_op.h"
#include "base/strings/string_piece.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "build/build_config.h"
#include "printing/buildflags/buildflags.h"
#include "printing/metafile.h"
#include "printing/mojom/print.mojom.h"
#include "printing/print_job_constants_cups.h"
#include "printing/print_settings_initializer_mac.h"
#include "printing/printing_features.h"
#include "printing/units.h"

#if BUILDFLAG(ENABLE_OOP_PRINTING_NO_OOP_BASIC_PRINT_DIALOG)
#include "base/numerics/safe_conversions.h"
#include "base/types/expected.h"
#endif

namespace printing {

namespace {

template <typename T>
struct ScopedPMTypeTraits {
  static T InvalidValue() { return nullptr; }
  static T Retain(T object) {
    PMRetain(object);
    return object;
  }
  static void Release(T object) { PMRelease(object); }
};

template <typename T>
using ScopedPMType = base::apple::ScopedTypeRef<T, ScopedPMTypeTraits<T>>;

const int kMaxPaperSizeDifferenceInPoints = 2;

// Return true if PPD name of paper is equal.
bool IsPaperNameEqual(CFStringRef name1, const PMPaper& paper2) {
  CFStringRef name2 = nullptr;
  return (name1 && PMPaperGetPPDPaperName(paper2, &name2) == noErr) &&
         (CFStringCompare(name1, name2, kCFCompareCaseInsensitive) ==
          kCFCompareEqualTo);
}

PMPaper MatchPaper(CFArrayRef paper_list,
                   CFStringRef name,
                   double width,
                   double height) {
  double best_match = std::numeric_limits<double>::max();
  PMPaper best_matching_paper = nullptr;

  CFIndex num_papers = CFArrayGetCount(paper_list);
  for (CFIndex i = 0; i < num_papers; ++i) {
    PMPaper paper = (PMPaper)CFArrayGetValueAtIndex(paper_list, i);
    double paper_width = 0.0;
    double paper_height = 0.0;
    PMPaperGetWidth(paper, &paper_width);
    PMPaperGetHeight(paper, &paper_height);
    double difference =
        std::max(fabs(width - paper_width), fabs(height - paper_height));

    // Ignore papers with size too different from expected.
    if (difference > kMaxPaperSizeDifferenceInPoints) {
      continue;
    }

    if (name && IsPaperNameEqual(name, paper))
      return paper;

    if (difference < best_match) {
      best_matching_paper = paper;
      best_match = difference;
    }
  }
  return best_matching_paper;
}

bool IsIppColorModelColorful(mojom::ColorModel color_model) {
  // Accept `kUnknownColorModel` as it can occur with raw CUPS printers.
  // Treat it similarly to the behavior in  `GetColorModelForModel()`.
  if (color_model == mojom::ColorModel::kUnknownColorModel) {
    return false;
  }
  return IsColorModelSelected(color_model).value();
}

#if BUILDFLAG(ENABLE_OOP_PRINTING_NO_OOP_BASIC_PRINT_DIALOG)

// The set of "capture" routines run in the browser process.
// The set of "apply" routines run in the Print Backend service.

base::expected<std::vector<uint8_t>, mojom::ResultCode>
CaptureSystemPrintSettings(PMPrintSettings& print_settings) {
  CFDataRef data_ref = nullptr;
  OSStatus status = PMPrintSettingsCreateDataRepresentation(
      print_settings, &data_ref, kPMDataFormatXMLDefault);
  if (status != noErr) {
    OSSTATUS_LOG(ERROR, status)
        << "Failed to create data representation of print settings";
    return base::unexpected(mojom::ResultCode::kFailed);
  }

  base::apple::ScopedCFTypeRef<CFDataRef> scoped_data_ref(data_ref);
  uint32_t data_size = CFDataGetLength(data_ref);
  std::vector<uint8_t> capture_data(data_size);
  CFDataGetBytes(data_ref, CFRangeMake(0, data_size),
                 static_cast<UInt8*>(&capture_data.front()));
  return capture_data;
}

base::expected<std::vector<uint8_t>, mojom::ResultCode> CaptureSystemPageFormat(
    PMPageFormat& page_format) {
  CFDataRef data_ref = nullptr;
  OSStatus status = PMPageFormatCreateDataRepresentation(
      page_format, &data_ref, kPMDataFormatXMLDefault);
  if (status != noErr) {
    OSSTATUS_LOG(ERROR, status)
        << "Failed to create data representation of page format";
    return base::unexpected(mojom::ResultCode::kFailed);
  }

  uint32_t data_size = CFDataGetLength(data_ref);
  std::vector<uint8_t> capture_data(data_size);
  CFDataGetBytes(data_ref, CFRangeMake(0, data_size),
                 static_cast<UInt8*>(&capture_data.front()));
  return capture_data;
}

base::expected<base::apple::ScopedCFTypeRef<CFStringRef>, mojom::ResultCode>
CaptureSystemDestinationFormat(PMPrintSession& print_session,
                               PMPrintSettings& print_settings) {
  CFStringRef destination_format_ref = nullptr;
  OSStatus status = PMSessionCopyDestinationFormat(
      print_session, print_settings, &destination_format_ref);
  if (status != noErr) {
    OSSTATUS_LOG(ERROR, status) << "Failed to get printing destination format";
    return base::unexpected(mojom::ResultCode::kFailed);
  }
  return base::apple::ScopedCFTypeRef<CFStringRef>(destination_format_ref);
}

base::expected<base::apple::ScopedCFTypeRef<CFURLRef>, mojom::ResultCode>
CaptureSystemDestinationLocation(PMPrintSession& print_session,
                                 PMPrintSettings& print_settings) {
  CFURLRef destination_location_ref = nullptr;
  OSStatus status = PMSessionCopyDestinationLocation(
      print_session, print_settings, &destination_location_ref);
  if (status != noErr) {
    OSSTATUS_LOG(ERROR, status)
        << "Failed to get printing destination location";
    return base::unexpected(mojom::ResultCode::kFailed);
  }
  return base::apple::ScopedCFTypeRef<CFURLRef>(destination_location_ref);
}

mojom::ResultCode CaptureSystemPrintDialogData(NSPrintInfo* print_info,
                                               PrintSettings* settings) {
  PMPrintSettings print_settings =
      (PMPrintSettings)[print_info PMPrintSettings];

  base::expected<std::vector<uint8_t>, mojom::ResultCode> print_settings_data =
      CaptureSystemPrintSettings(print_settings);
  if (!print_settings_data.has_value()) {
    return print_settings_data.error();
  }

  PMPageFormat page_format =
      static_cast<PMPageFormat>([print_info PMPageFormat]);

  base::expected<std::vector<uint8_t>, mojom::ResultCode> page_format_data =
      CaptureSystemPageFormat(page_format);
  if (!page_format_data.has_value()) {
    return page_format_data.error();
  }

  PMPrintSession print_session =
      static_cast<PMPrintSession>([print_info PMPrintSession]);

  PMDestinationType destination_type = kPMDestinationInvalid;
  PMSessionGetDestinationType(print_session, print_settings, &destination_type);

  base::expected<base::apple::ScopedCFTypeRef<CFStringRef>, mojom::ResultCode>
      destination_format =
          CaptureSystemDestinationFormat(print_session, print_settings);
  if (!destination_format.has_value()) {
    return destination_format.error();
  }

  base::expected<base::apple::ScopedCFTypeRef<CFURLRef>, mojom::ResultCode>
      destination_location =
          CaptureSystemDestinationLocation(print_session, print_settings);
  if (!destination_location.has_value()) {
    return destination_location.error();
  }

  base::Value::Dict dialog_data;
  dialog_data.Set(kMacSystemPrintDialogDataPrintSettings,
                  std::move(print_settings_data.value()));
  dialog_data.Set(kMacSystemPrintDialogDataPageFormat,
                  std::move(page_format_data.value()));
  dialog_data.Set(kMacSystemPrintDialogDataDestinationType, destination_type);
  if (destination_format.value()) {
    dialog_data.Set(kMacSystemPrintDialogDataDestinationFormat,
                    base::SysCFStringRefToUTF8(destination_format.value()));
  }
  if (destination_location.value()) {
    dialog_data.Set(kMacSystemPrintDialogDataDestinationLocation,
                    base::SysCFStringRefToUTF8(
                        CFURLGetString(destination_location.value())));
  }
  settings->set_system_print_dialog_data(std::move(dialog_data));
  return mojom::ResultCode::kSuccess;
}

void ApplySystemPrintSettings(const base::Value::Dict& system_print_dialog_data,
                              NSPrintInfo* print_info,
                              PMPrintSession& print_session,
                              PMPrintSettings& print_settings) {
  const base::Value::BlobStorage* data =
      system_print_dialog_data.FindBlob(kMacSystemPrintDialogDataPrintSettings);
  CHECK(data);
  uint32_t data_size = data->size();
  CHECK_GT(data_size, 0u);
  CFDataRef data_ref =
      CFDataCreate(kCFAllocatorDefault,
                   static_cast<const UInt8*>(&data->front()), data_size);
  CHECK(data_ref);
  base::apple::ScopedCFTypeRef<CFDataRef> scoped_data_ref(data_ref);

  ScopedPMType<PMPrintSettings> new_print_settings;
  OSStatus status = PMPrintSettingsCreateWithDataRepresentation(
      data_ref, new_print_settings.InitializeInto());
  CHECK_EQ(status, noErr) << logging::DescriptionFromOSStatus(status);

  status = PMSessionValidatePrintSettings(print_session, new_print_settings,
                                          kPMDontWantBoolean);
  CHECK_EQ(status, noErr) << logging::DescriptionFromOSStatus(status);
  status = PMCopyPrintSettings(new_print_settings, print_settings);
  CHECK_EQ(status, noErr) << logging::DescriptionFromOSStatus(status);

  [print_info updateFromPMPrintSettings];
}

void ApplySystemPageFormat(const base::Value::Dict& system_print_dialog_data,
                           NSPrintInfo* print_info,
                           PMPrintSession& print_session,
                           PMPageFormat& page_format) {
  const base::Value::BlobStorage* data =
      system_print_dialog_data.FindBlob(kMacSystemPrintDialogDataPageFormat);
  CHECK(data);
  uint32_t data_size = data->size();
  CHECK_GT(data_size, 0u);
  CFDataRef data_ref =
      CFDataCreate(kCFAllocatorDefault,
                   static_cast<const UInt8*>(&data->front()), data_size);
  CHECK(data_ref);

  ScopedPMType<PMPageFormat> new_page_format;
  OSStatus status = PMPageFormatCreateWithDataRepresentation(
      data_ref, new_page_format.InitializeInto());
  CHECK_EQ(status, noErr) << logging::DescriptionFromOSStatus(status);
  status = PMSessionValidatePageFormat(print_session, page_format,
                                       kPMDontWantBoolean);
  status = PMCopyPageFormat(new_page_format, page_format);
  CHECK_EQ(status, noErr) << logging::DescriptionFromOSStatus(status);

  [print_info updateFromPMPageFormat];
}

void ApplySystemDestination(const std::u16string& device_name,
                            const base::Value::Dict& system_print_dialog_data,
                            PMPrintSession& print_session,
                            PMPrintSettings& print_settings) {
  absl::optional<int> destination_type = system_print_dialog_data.FindInt(
      kMacSystemPrintDialogDataDestinationType);

  CHECK(destination_type.has_value());
  CHECK(base::IsValueInRangeForNumericType<uint16_t>(*destination_type));

  const std::string* destination_format_str =
      system_print_dialog_data.FindString(
          kMacSystemPrintDialogDataDestinationFormat);
  const std::string* destination_location_str =
      system_print_dialog_data.FindString(
          kMacSystemPrintDialogDataDestinationLocation);

  base::apple::ScopedCFTypeRef<CFStringRef> destination_format;
  if (destination_format_str) {
    destination_format.reset(
        base::SysUTF8ToCFStringRef(*destination_format_str));
  }

  base::apple::ScopedCFTypeRef<CFURLRef> destination_location;
  if (destination_location_str) {
    destination_location.reset(CFURLCreateWithFileSystemPath(
        kCFAllocatorDefault,
        base::SysUTF8ToCFStringRef(*destination_location_str),
        kCFURLPOSIXPathStyle,
        /*isDirectory=*/FALSE));
  }

  base::apple::ScopedCFTypeRef<CFStringRef> destination_name(
      base::SysUTF16ToCFStringRef(device_name));
  ScopedPMType<PMPrinter> printer(
      PMPrinterCreateFromPrinterID(destination_name.get()));
  CHECK(printer);
  OSStatus status = PMSessionSetCurrentPMPrinter(print_session, printer);
  CHECK_EQ(status, noErr) << logging::DescriptionFromOSStatus(status);

  status = PMSessionSetDestination(
      print_session, print_settings,
      static_cast<PMDestinationType>(*destination_type),
      destination_format.get(), destination_location.get());
  CHECK_EQ(status, noErr) << logging::DescriptionFromOSStatus(status);
}

void ApplySystemPrintDialogData(
    const std::u16string& device_name,
    const base::Value::Dict& system_print_dialog_data,
    NSPrintInfo* print_info) {
  PMPrintSession print_session =
      static_cast<PMPrintSession>([print_info PMPrintSession]);
  PMPrintSettings print_settings =
      static_cast<PMPrintSettings>([print_info PMPrintSettings]);
  PMPageFormat page_format =
      static_cast<PMPageFormat>([print_info PMPageFormat]);

  ApplySystemDestination(device_name, system_print_dialog_data, print_session,
                         print_settings);
  ApplySystemPrintSettings(system_print_dialog_data, print_info, print_session,
                           print_settings);
  ApplySystemPageFormat(system_print_dialog_data, print_info, print_session,
                        page_format);
}
#endif  // BUILDFLAG(ENABLE_OOP_PRINTING_NO_OOP_BASIC_PRINT_DIALOG)

}  // namespace

// static
std::unique_ptr<PrintingContext> PrintingContext::CreateImpl(
    Delegate* delegate,
    bool skip_system_calls) {
  auto context = std::make_unique<PrintingContextMac>(delegate);
#if BUILDFLAG(ENABLE_OOP_PRINTING)
  if (skip_system_calls)
    context->set_skip_system_calls();
#endif
  return context;
}

PrintingContextMac::PrintingContextMac(Delegate* delegate)
    : PrintingContext(delegate),
      print_info_([NSPrintInfo.sharedPrintInfo copy]) {}

PrintingContextMac::~PrintingContextMac() {
  ReleaseContext();
}

void PrintingContextMac::AskUserForSettings(int max_pages,
                                            bool has_selection,
                                            bool is_scripted,
                                            PrintSettingsCallback callback) {
  // Exceptions can also happen when the NSPrintPanel is being
  // deallocated, so it must be autoreleased within this scope.
  @autoreleasepool {
    DCHECK(NSThread.isMainThread);

    // We deliberately don't feed max_pages into the dialog, because setting
    // NSPrintLastPage makes the print dialog pre-select the option to only
    // print a range.

    // TODO(stuartmorgan): implement 'print selection only' (probably requires
    // adding a new custom view to the panel on 10.5; 10.6 has
    // NSPrintPanelShowsPrintSelection).
    NSPrintPanel* panel = [NSPrintPanel printPanel];
    panel.options |= NSPrintPanelShowsPaperSize | NSPrintPanelShowsOrientation |
                     NSPrintPanelShowsScaling;

    // Set the print job title text.
    gfx::NativeView parent_view = delegate_->GetParentView();
    if (parent_view) {
      NSString* job_title = parent_view.GetNativeNSView().window.title;
      if (job_title) {
        PMPrintSettings print_settings =
            static_cast<PMPrintSettings>([print_info_ PMPrintSettings]);
        PMPrintSettingsSetJobName(print_settings,
                                  base::apple::NSToCFPtrCast(job_title));
        [print_info_ updateFromPMPrintSettings];
      }
    }

    // TODO(stuartmorgan): We really want a tab sheet here, not a modal window.
    // Will require restructuring the PrintingContext API to use a callback.

    // This function may be called in the middle of a CATransaction, where
    // running a modal panel is forbidden. That situation isn't ideal, but from
    // this code's POV the right answer is to defer running the panel until
    // after the current transaction. See https://crbug.com/849538.
    __block auto block_callback = std::move(callback);
    [CATransaction setCompletionBlock:^{
      NSInteger selection = [panel runModalWithPrintInfo:print_info_];
      if (selection == NSModalResponseOK) {
        print_info_ = [panel printInfo];
        settings_->set_ranges(GetPageRangesFromPrintInfo());
        InitPrintSettingsFromPrintInfo();
        mojom::ResultCode result = mojom::ResultCode::kSuccess;
#if BUILDFLAG(ENABLE_OOP_PRINTING_NO_OOP_BASIC_PRINT_DIALOG)
        if (features::kEnableOopPrintDriversJobPrint.Get()) {
          result = CaptureSystemPrintDialogData(print_info_, settings_.get());
        }
#endif
        std::move(block_callback).Run(result);
      } else {
        std::move(block_callback).Run(mojom::ResultCode::kCanceled);
      }
    }];
  }
}

gfx::Size PrintingContextMac::GetPdfPaperSizeDeviceUnits() {
  // NOTE: Reset |print_info_| with a copy of |sharedPrintInfo| so as to start
  // with a clean slate.
  print_info_ = [[NSPrintInfo sharedPrintInfo] copy];
  UpdatePageFormatWithPaperInfo();

  PMPageFormat page_format =
      static_cast<PMPageFormat>([print_info_ PMPageFormat]);
  PMRect paper_rect;
  PMGetAdjustedPaperRect(page_format, &paper_rect);

  // Device units are in points. Units per inch is 72.
  gfx::Size physical_size_device_units((paper_rect.right - paper_rect.left),
                                       (paper_rect.bottom - paper_rect.top));
  DCHECK(settings_->device_units_per_inch() == kPointsPerInch);
  return physical_size_device_units;
}

mojom::ResultCode PrintingContextMac::UseDefaultSettings() {
  DCHECK(!in_print_job_);

  print_info_ = [[NSPrintInfo sharedPrintInfo] copy];
  settings_->set_ranges(GetPageRangesFromPrintInfo());
  InitPrintSettingsFromPrintInfo();

  return mojom::ResultCode::kSuccess;
}

mojom::ResultCode PrintingContextMac::UpdatePrinterSettings(
    const PrinterSettings& printer_settings) {
  DCHECK(!printer_settings.show_system_dialog);
  DCHECK(!in_print_job_);

  // NOTE: Reset |print_info_| with a copy of |sharedPrintInfo| so as to start
  // with a clean slate.
  print_info_ = [[NSPrintInfo sharedPrintInfo] copy];

  if (printer_settings.external_preview) {
    if (!SetPrintPreviewJob())
      return OnError();
  } else {
    // Don't need this for preview.
    if (!SetPrinter(base::UTF16ToUTF8(settings_->device_name())) ||
        !SetCopiesInPrintSettings(settings_->copies()) ||
        !SetCollateInPrintSettings(settings_->collate()) ||
        !SetDuplexModeInPrintSettings(settings_->duplex_mode()) ||
        !SetOutputColor(static_cast<int>(settings_->color())) ||
        !SetResolution(settings_->dpi_size())) {
      return OnError();
    }
  }

  if (!UpdatePageFormatWithPaperInfo() ||
      !SetOrientationIsLandscape(settings_->landscape())) {
    return OnError();
  }

  [print_info_ updateFromPMPrintSettings];

  InitPrintSettingsFromPrintInfo();
  return mojom::ResultCode::kSuccess;
}

bool PrintingContextMac::SetPrintPreviewJob() {
  PMPrintSession print_session =
      static_cast<PMPrintSession>([print_info_ PMPrintSession]);
  PMPrintSettings print_settings =
      static_cast<PMPrintSettings>([print_info_ PMPrintSettings]);
  return PMSessionSetDestination(print_session, print_settings,
                                 kPMDestinationPreview, nullptr,
                                 nullptr) == noErr;
}

void PrintingContextMac::InitPrintSettingsFromPrintInfo() {
  PMPrintSession print_session =
      static_cast<PMPrintSession>([print_info_ PMPrintSession]);
  PMPageFormat page_format =
      static_cast<PMPageFormat>([print_info_ PMPageFormat]);
  PMPrinter printer;
  PMSessionGetCurrentPrinter(print_session, &printer);
  PrintSettingsInitializerMac::InitPrintSettings(printer, page_format,
                                                 settings_.get());
}

bool PrintingContextMac::SetPrinter(const std::string& device_name) {
  DCHECK(print_info_);
  PMPrintSession print_session =
      static_cast<PMPrintSession>([print_info_ PMPrintSession]);

  PMPrinter current_printer;
  if (PMSessionGetCurrentPrinter(print_session, &current_printer) != noErr)
    return false;

  CFStringRef current_printer_id = PMPrinterGetID(current_printer);
  if (!current_printer_id)
    return false;

  base::apple::ScopedCFTypeRef<CFStringRef> new_printer_id(
      base::SysUTF8ToCFStringRef(device_name));
  if (!new_printer_id.get())
    return false;

  if (CFStringCompare(new_printer_id.get(), current_printer_id, 0) ==
      kCFCompareEqualTo) {
    return true;
  }

  ScopedPMType<PMPrinter> new_printer(
      PMPrinterCreateFromPrinterID(new_printer_id.get()));
  if (!new_printer) {
    return false;
  }

  return PMSessionSetCurrentPMPrinter(print_session, new_printer) == noErr;
}

bool PrintingContextMac::UpdatePageFormatWithPaperInfo() {
  PMPrintSession print_session =
      static_cast<PMPrintSession>([print_info_ PMPrintSession]);

  PMPageFormat default_page_format =
      static_cast<PMPageFormat>([print_info_ PMPageFormat]);

  PMPrinter current_printer = nullptr;
  if (PMSessionGetCurrentPrinter(print_session, &current_printer) != noErr)
    return false;

  double page_width = 0.0;
  double page_height = 0.0;
  base::apple::ScopedCFTypeRef<CFStringRef> paper_name;
  PMPaperMargins margins = {0};

  const PrintSettings::RequestedMedia& media = settings_->requested_media();
  if (media.IsDefault()) {
    PMPaper default_paper;
    if (PMGetPageFormatPaper(default_page_format, &default_paper) != noErr ||
        PMPaperGetWidth(default_paper, &page_width) != noErr ||
        PMPaperGetHeight(default_paper, &page_height) != noErr) {
      return false;
    }

    // Ignore result, because we can continue without following.
    CFStringRef tmp_paper_name = nullptr;
    PMPaperGetPPDPaperName(default_paper, &tmp_paper_name);
    PMPaperGetMargins(default_paper, &margins);
    paper_name.reset(tmp_paper_name, base::scoped_policy::RETAIN);
  } else {
    const double kMultiplier =
        kPointsPerInch / static_cast<float>(kMicronsPerInch);
    page_width = media.size_microns.width() * kMultiplier;
    page_height = media.size_microns.height() * kMultiplier;
    paper_name.reset(base::SysUTF8ToCFStringRef(media.vendor_id));
  }

  CFArrayRef paper_list = nullptr;
  if (PMPrinterGetPaperList(current_printer, &paper_list) != noErr)
    return false;

  PMPaper best_matching_paper =
      MatchPaper(paper_list, paper_name, page_width, page_height);

  if (best_matching_paper)
    return UpdatePageFormatWithPaper(best_matching_paper, default_page_format);

  // Do nothing if unmatched paper was default system paper.
  if (media.IsDefault())
    return true;

  ScopedPMType<PMPaper> paper;
  if (PMPaperCreateCustom(current_printer, CFSTR("Custom paper ID"),
                          CFSTR("Custom paper"), page_width, page_height,
                          &margins, paper.InitializeInto()) != noErr) {
    return false;
  }
  return UpdatePageFormatWithPaper(paper, default_page_format);
}

bool PrintingContextMac::UpdatePageFormatWithPaper(PMPaper paper,
                                                   PMPageFormat page_format) {
  ScopedPMType<PMPageFormat> new_format;
  if (PMCreatePageFormatWithPMPaper(new_format.InitializeInto(), paper) !=
      noErr) {
    return false;
  }
  // Copy over the original format with the new page format.
  bool result = (PMCopyPageFormat(new_format, page_format) == noErr);
  [print_info_ updateFromPMPageFormat];
  return result;
}

bool PrintingContextMac::SetCopiesInPrintSettings(int copies) {
  if (copies < 1)
    return false;

  PMPrintSettings print_settings =
      static_cast<PMPrintSettings>([print_info_ PMPrintSettings]);
  return PMSetCopies(print_settings, copies, false) == noErr;
}

bool PrintingContextMac::SetCollateInPrintSettings(bool collate) {
  PMPrintSettings print_settings =
      static_cast<PMPrintSettings>([print_info_ PMPrintSettings]);
  return PMSetCollate(print_settings, collate) == noErr;
}

bool PrintingContextMac::SetOrientationIsLandscape(bool landscape) {
  PMPageFormat page_format =
      static_cast<PMPageFormat>([print_info_ PMPageFormat]);

  PMOrientation orientation = landscape ? kPMLandscape : kPMPortrait;

  if (PMSetOrientation(page_format, orientation, false) != noErr)
    return false;

  PMPrintSession print_session =
      static_cast<PMPrintSession>([print_info_ PMPrintSession]);

  PMSessionValidatePageFormat(print_session, page_format, kPMDontWantBoolean);

  [print_info_ updateFromPMPageFormat];
  return true;
}

bool PrintingContextMac::SetDuplexModeInPrintSettings(mojom::DuplexMode mode) {
  PMDuplexMode duplexSetting;
  switch (mode) {
    case mojom::DuplexMode::kLongEdge:
      duplexSetting = kPMDuplexNoTumble;
      break;
    case mojom::DuplexMode::kShortEdge:
      duplexSetting = kPMDuplexTumble;
      break;
    case mojom::DuplexMode::kSimplex:
      duplexSetting = kPMDuplexNone;
      break;
    default:  // kUnknownDuplexMode
      return true;
  }

  PMPrintSettings print_settings =
      static_cast<PMPrintSettings>([print_info_ PMPrintSettings]);
  return PMSetDuplex(print_settings, duplexSetting) == noErr;
}

bool PrintingContextMac::SetOutputColor(int color_mode) {
  const mojom::ColorModel color_model = ColorModeToColorModel(color_mode);

  if (!base::FeatureList::IsEnabled(features::kCupsIppPrintingBackend)) {
    std::string color_setting_name;
    std::string color_value;
    GetColorModelForModel(color_model, &color_setting_name, &color_value);
    return SetKeyValue(color_setting_name, color_value);
  }

  // First, set the default CUPS IPP output color.
  if (!SetKeyValue(CUPS_PRINT_COLOR_MODE,
                   GetIppColorModelForModel(color_model))) {
    return false;
  }

  // Even when interfacing with printer settings using CUPS IPP, the print job
  // may still expect PPD color values if the printer was added to the system
  // with a PPD. To avoid parsing PPDs (which is the point of using CUPS IPP),
  // set every single known PPD color setting and hope that one of them sticks.
  const bool is_color = IsIppColorModelColorful(color_model);
  for (const auto& setting : GetKnownPpdColorSettings()) {
    const base::StringPiece& color_setting_name = setting.name;
    const base::StringPiece& color_value =
        is_color ? setting.color : setting.bw;
    if (!SetKeyValue(color_setting_name, color_value))
      return false;
  }

  return true;
}

bool PrintingContextMac::SetResolution(const gfx::Size& dpi_size) {
  if (dpi_size.IsEmpty())
    return true;

  PMPrintSession print_session =
      static_cast<PMPrintSession>([print_info_ PMPrintSession]);
  PMPrinter current_printer;
  if (PMSessionGetCurrentPrinter(print_session, &current_printer) != noErr)
    return false;

  PMResolution resolution;
  resolution.hRes = dpi_size.width();
  resolution.vRes = dpi_size.height();

  PMPrintSettings print_settings =
      static_cast<PMPrintSettings>([print_info_ PMPrintSettings]);
  return PMPrinterSetOutputResolution(current_printer, print_settings,
                                      &resolution) == noErr;
}

bool PrintingContextMac::SetKeyValue(base::StringPiece key,
                                     base::StringPiece value) {
  PMPrintSettings print_settings =
      static_cast<PMPrintSettings>([print_info_ PMPrintSettings]);
  base::apple::ScopedCFTypeRef<CFStringRef> cf_key =
      base::SysUTF8ToCFStringRef(key);
  base::apple::ScopedCFTypeRef<CFStringRef> cf_value =
      base::SysUTF8ToCFStringRef(value);

  return PMPrintSettingsSetValue(print_settings, cf_key.get(), cf_value.get(),
                                 /*locked=*/false) == noErr;
}

PageRanges PrintingContextMac::GetPageRangesFromPrintInfo() {
  PageRanges page_ranges;
  NSDictionary* print_info_dict = [print_info_ dictionary];
  if (![print_info_dict[NSPrintAllPages] boolValue]) {
    PageRange range;
    range.from = [print_info_dict[NSPrintFirstPage] intValue] - 1;
    range.to = [print_info_dict[NSPrintLastPage] intValue] - 1;
    page_ranges.push_back(range);
  }
  return page_ranges;
}

mojom::ResultCode PrintingContextMac::NewDocument(
    const std::u16string& document_name) {
  DCHECK(!in_print_job_);

  in_print_job_ = true;

  if (skip_system_calls())
    return mojom::ResultCode::kSuccess;

#if BUILDFLAG(ENABLE_OOP_PRINTING_NO_OOP_BASIC_PRINT_DIALOG)
  if (features::kEnableOopPrintDriversJobPrint.Get() &&
      !settings_->system_print_dialog_data().empty()) {
    // NOTE: Reset `print_info_` with a copy of `sharedPrintInfo` so as to
    // start with a clean slate.
    print_info_ = [[NSPrintInfo sharedPrintInfo] copy];

    ApplySystemPrintDialogData(settings_->device_name(),
                               settings_->system_print_dialog_data(),
                               print_info_);
  }
#endif

  PMPrintSession print_session =
      static_cast<PMPrintSession>([print_info_ PMPrintSession]);
  PMPrintSettings print_settings =
      static_cast<PMPrintSettings>([print_info_ PMPrintSettings]);
  PMPageFormat page_format =
      static_cast<PMPageFormat>([print_info_ PMPageFormat]);

  base::apple::ScopedCFTypeRef<CFStringRef> job_title =
      base::SysUTF16ToCFStringRef(document_name);
  PMPrintSettingsSetJobName(print_settings, job_title.get());

  OSStatus status = PMSessionBeginCGDocumentNoDialog(
      print_session, print_settings, page_format);
  if (status != noErr)
    return OnError();

  return mojom::ResultCode::kSuccess;
}

mojom::ResultCode PrintingContextMac::NewPage() {
  if (abort_printing_)
    return mojom::ResultCode::kCanceled;
  DCHECK(in_print_job_);
  DCHECK(!context_);

  PMPrintSession print_session =
      static_cast<PMPrintSession>([print_info_ PMPrintSession]);
  PMPageFormat page_format =
      static_cast<PMPageFormat>([print_info_ PMPageFormat]);
  OSStatus status;
  status = PMSessionBeginPageNoDialog(print_session, page_format, nullptr);
  if (status != noErr)
    return OnError();
  status = PMSessionGetCGGraphicsContext(print_session, &context_);
  if (status != noErr)
    return OnError();

  return mojom::ResultCode::kSuccess;
}

mojom::ResultCode PrintingContextMac::PageDone() {
  if (abort_printing_)
    return mojom::ResultCode::kCanceled;
  DCHECK(in_print_job_);
  DCHECK(context_);

  PMPrintSession print_session =
      static_cast<PMPrintSession>([print_info_ PMPrintSession]);
  OSStatus status = PMSessionEndPageNoDialog(print_session);
  if (status != noErr)
    OnError();
  context_ = nullptr;

  return mojom::ResultCode::kSuccess;
}

mojom::ResultCode PrintingContextMac::PrintDocument(
    const MetafilePlayer& metafile,
    const PrintSettings& settings,
    uint32_t num_pages) {
  const PageSetup& page_setup = settings.page_setup_device_units();
  const CGRect paper_rect = gfx::Rect(page_setup.physical_size()).ToCGRect();

  for (size_t metafile_page_number = 1; metafile_page_number <= num_pages;
       metafile_page_number++) {
    mojom::ResultCode result = NewPage();
    if (result != mojom::ResultCode::kSuccess)
      return result;
    if (!metafile.RenderPage(metafile_page_number, context_, paper_rect,
                             /*autorotate=*/true, /*fit_to_page=*/false)) {
      return mojom::ResultCode::kFailed;
    }
    result = PageDone();
    if (result != mojom::ResultCode::kSuccess)
      return result;
  }
  return mojom::ResultCode::kSuccess;
}

mojom::ResultCode PrintingContextMac::DocumentDone() {
  if (abort_printing_)
    return mojom::ResultCode::kCanceled;
  DCHECK(in_print_job_);

  PMPrintSession print_session =
      static_cast<PMPrintSession>([print_info_ PMPrintSession]);
  OSStatus status = PMSessionEndDocumentNoDialog(print_session);
  if (status != noErr)
    OnError();

  ResetSettings();
  return mojom::ResultCode::kSuccess;
}

void PrintingContextMac::Cancel() {
  abort_printing_ = true;
  in_print_job_ = false;
  context_ = nullptr;

  PMPrintSession print_session =
      static_cast<PMPrintSession>([print_info_ PMPrintSession]);
  PMSessionEndPageNoDialog(print_session);
}

void PrintingContextMac::ReleaseContext() {
  print_info_ = nil;
  context_ = nullptr;
}

printing::NativeDrawingContext PrintingContextMac::context() const {
  return context_;
}

}  // namespace printing
