// Copyright 2016 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/backend/cups_ipp_helper.h"

#include <cups/cups.h>

#include <algorithm>
#include <map>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

#include "base/containers/fixed_flat_map.h"
#include "base/containers/fixed_flat_set.h"
#include "base/logging.h"
#include "base/numerics/clamped_math.h"
#include "base/strings/string_util.h"
#include "build/build_config.h"
#include "printing/backend/cups_connection.h"
#include "printing/backend/cups_ipp_constants.h"
#include "printing/backend/cups_printer.h"
#include "printing/backend/print_backend_consts.h"
#include "printing/backend/print_backend_utils.h"
#include "printing/mojom/print.mojom.h"
#include "printing/printing_utils.h"
#include "printing/units.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "base/functional/callback.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "printing/backend/ipp_handler_map.h"
#include "printing/backend/ipp_handlers.h"
#endif  // BUILDFLAG(IS_CHROMEOS)

namespace printing {

#if BUILDFLAG(IS_CHROMEOS)
constexpr int kPinMinimumLength = 4;
#endif  // BUILDFLAG(IS_CHROMEOS)

namespace {

constexpr double kMMPerInch = 25.4;
constexpr double kCmPerInch = kMMPerInch * 0.1;

constexpr auto kColorMap =
    base::MakeFixedFlatMap<std::string_view, mojom::ColorModel>({
        {CUPS_PRINT_COLOR_MODE_COLOR, mojom::ColorModel::kColorModeColor},
        {CUPS_PRINT_COLOR_MODE_MONOCHROME,
         mojom::ColorModel::kColorModeMonochrome},
    });

constexpr auto kDuplexMap =
    base::MakeFixedFlatMap<std::string_view, mojom::DuplexMode>({
        {CUPS_SIDES_ONE_SIDED, mojom::DuplexMode::kSimplex},
        {CUPS_SIDES_TWO_SIDED_PORTRAIT, mojom::DuplexMode::kLongEdge},
        {CUPS_SIDES_TWO_SIDED_LANDSCAPE, mojom::DuplexMode::kShortEdge},
    });

mojom::ColorModel ColorModelFromIppColor(std::string_view ipp_color) {
  auto it = kColorMap.find(ipp_color);
  return it != kColorMap.end() ? it->second
                               : mojom::ColorModel::kUnknownColorModel;
}

mojom::DuplexMode DuplexModeFromIpp(std::string_view ipp_duplex) {
  auto it = kDuplexMap.find(ipp_duplex);
  return it != kDuplexMap.end() ? it->second
                                : mojom::DuplexMode::kUnknownDuplexMode;
}

mojom::ColorModel DefaultColorModel(const CupsOptionProvider& printer) {
  // default color
  ipp_attribute_t* attr = printer.GetDefaultOptionValue(kIppColor);
  if (!attr)
    return mojom::ColorModel::kUnknownColorModel;

  const char* const value = ippGetString(attr, 0, nullptr);
  return value ? ColorModelFromIppColor(value)
               : mojom::ColorModel::kUnknownColorModel;
}

std::vector<mojom::ColorModel> SupportedColorModels(
    const CupsOptionProvider& printer) {
  std::vector<mojom::ColorModel> colors;

  std::vector<std::string_view> color_modes =
      printer.GetSupportedOptionValueStrings(kIppColor);
  for (std::string_view color : color_modes) {
    mojom::ColorModel color_model = ColorModelFromIppColor(color);
    if (color_model != mojom::ColorModel::kUnknownColorModel) {
      colors.push_back(color_model);
    }
  }

  return colors;
}

void ExtractColor(const CupsOptionProvider& printer,
                  PrinterSemanticCapsAndDefaults* printer_info) {
  printer_info->bw_model = mojom::ColorModel::kUnknownColorModel;
  printer_info->color_model = mojom::ColorModel::kUnknownColorModel;

  // color and b&w
  std::vector<mojom::ColorModel> color_models = SupportedColorModels(printer);
  for (mojom::ColorModel color : color_models) {
    switch (color) {
      case mojom::ColorModel::kColorModeColor:
        printer_info->color_model = mojom::ColorModel::kColorModeColor;
        break;
      case mojom::ColorModel::kColorModeMonochrome:
        printer_info->bw_model = mojom::ColorModel::kColorModeMonochrome;
        break;
      default:
        // value not needed
        break;
    }
  }

  // changeable
  printer_info->color_changeable =
      (printer_info->color_model != mojom::ColorModel::kUnknownColorModel &&
       printer_info->bw_model != mojom::ColorModel::kUnknownColorModel);

  // default color
  printer_info->color_default =
      DefaultColorModel(printer) == mojom::ColorModel::kColorModeColor;
}

void ExtractDuplexModes(const CupsOptionProvider& printer,
                        PrinterSemanticCapsAndDefaults* printer_info) {
  printer_info->duplex_default = mojom::DuplexMode::kUnknownDuplexMode;
  printer_info->duplex_modes.clear();

  std::vector<std::string_view> duplex_modes =
      printer.GetSupportedOptionValueStrings(kIppDuplex);
  for (std::string_view duplex : duplex_modes) {
    mojom::DuplexMode duplex_mode = DuplexModeFromIpp(duplex);
    if (duplex_mode != mojom::DuplexMode::kUnknownDuplexMode)
      printer_info->duplex_modes.push_back(duplex_mode);
  }

  ipp_attribute_t* attr = printer.GetDefaultOptionValue(kIppDuplex);
  if (attr) {
    const char* const attr_str = ippGetString(attr, 0, nullptr);
    if (attr_str) {
      printer_info->duplex_default = DuplexModeFromIpp(attr_str);
    }
  }
}

void CopiesRange(const CupsOptionProvider& printer,
                 int* lower_bound,
                 int* upper_bound) {
  ipp_attribute_t* attr = printer.GetSupportedOptionValues(kIppCopies);
  if (!attr) {
    *lower_bound = -1;
    *upper_bound = -1;
  }

  *lower_bound = ippGetRange(attr, 0, upper_bound);
}

void ExtractCopies(const CupsOptionProvider& printer,
                   PrinterSemanticCapsAndDefaults* printer_info) {
  // copies
  int lower_bound;
  int upper_bound;
  CopiesRange(printer, &lower_bound, &upper_bound);
  printer_info->copies_max =
      (lower_bound != -1 && upper_bound >= 2) ? upper_bound : 1;
}

// Reads resolution from `attr` and puts into `size` in dots per inch.
std::optional<gfx::Size> GetResolution(ipp_attribute_t* attr, int i) {
  ipp_res_t units;
  int yres;
  int xres = ippGetResolution(attr, i, &yres, &units);
  if (!xres)
    return {};

  switch (units) {
    case IPP_RES_PER_INCH:
      return gfx::Size(xres, yres);
    case IPP_RES_PER_CM:
      return gfx::Size(xres * kCmPerInch, yres * kCmPerInch);
  }
  return {};
}

// Initializes `printer_info.dpis` with available resolutions and
// `printer_info.default_dpi` with default resolution provided by `printer`.
void ExtractResolutions(const CupsOptionProvider& printer,
                        PrinterSemanticCapsAndDefaults* printer_info) {
  // Provide a default DPI if no valid DPI is found.
#if BUILDFLAG(IS_MAC)
  constexpr gfx::Size kDefaultMissingDpi(kDefaultMacDpi, kDefaultMacDpi);
#elif BUILDFLAG(IS_LINUX)
  constexpr gfx::Size kDefaultMissingDpi(kPixelsPerInch, kPixelsPerInch);
#else
  constexpr gfx::Size kDefaultMissingDpi(kDefaultPdfDpi, kDefaultPdfDpi);
#endif

  printer_info->dpis.clear();
  printer_info->default_dpi = gfx::Size();

  ipp_attribute_t* attr = printer.GetSupportedOptionValues(kIppResolution);
  if (!attr) {
    printer_info->dpis.push_back(kDefaultMissingDpi);
    return;
  }

  int num_options = ippGetCount(attr);
  for (int i = 0; i < num_options; i++) {
    std::optional<gfx::Size> size = GetResolution(attr, i);
    if (size)
      printer_info->dpis.push_back(size.value());
  }
  ipp_attribute_t* def_attr = printer.GetDefaultOptionValue(kIppResolution);
  std::optional<gfx::Size> size = GetResolution(def_attr, 0);
  if (size) {
    printer_info->default_dpi = size.value();
  } else if (!printer_info->dpis.empty()) {
    printer_info->default_dpi = printer_info->dpis[0];
  } else {
    printer_info->default_dpi = kDefaultMissingDpi;
  }

  if (printer_info->dpis.empty()) {
    printer_info->dpis.push_back(printer_info->default_dpi);
  }
}

std::optional<PrinterSemanticCapsAndDefaults::Paper>
PaperFromMediaColDatabaseEntry(ipp_t* db_entry) {
  DCHECK(db_entry);

  std::optional<MediaColData> size = ExtractMediaColData(db_entry);
  if (!size) {
    LOG(WARNING) << "Unable to create Paper from media-col-database";
    return std::nullopt;
  }

  if (size->HasVariableWidth()) {
    LOG(WARNING) << "Invalid media-col-database entry:"
                 << " variable widths are not supported.";
    return std::nullopt;
  }

  // Some PPDs (only ones with a custom height range) have a min height of 0,
  // which doesn't work with the printing stack.  If there is a min height of 0
  // (or equal to the margins) change it to some small value.  For entries that
  // have a fixed height, the height will not get modified; if this fixed height
  // is invalid, it will just get rejected below.
  if (size->HasVariableHeight()) {
    size->min_height = base::ClampMax(
        size->min_height,
        base::ClampedNumeric<int>(size->top_margin) + size->bottom_margin + 1);
  }

  if (size->min_width <= 0 || size->min_height <= 0 ||
      size->bottom_margin < 0 || size->top_margin < 0 ||
      size->left_margin < 0 || size->right_margin < 0 ||
      size->min_width <=
          base::ClampedNumeric<int>(size->left_margin) + size->right_margin ||
      size->min_height <=
          base::ClampedNumeric<int>(size->bottom_margin) + size->top_margin ||
      (size->HasVariableHeight() && size->max_height < size->min_height)) {
    LOG(WARNING) << "Invalid media-col-database entry:"
                 << " width=" << size->min_width << "-" << size->max_width
                 << " height=" << size->min_height << "-" << size->max_height
                 << " media-bottom-margin=" << size->bottom_margin
                 << " media-left-margin=" << size->left_margin
                 << " media-right-margin=" << size->right_margin
                 << " media-top-margin=" << size->top_margin;
    return std::nullopt;
  }

  gfx::Size size_um(size->min_width * kMicronsPerPwgUnit,
                    size->min_height * kMicronsPerPwgUnit);
  gfx::Rect printable_area_um = PrintableAreaFromSizeAndPwgMargins(
      size_um, size->bottom_margin, size->left_margin, size->right_margin,
      size->top_margin);
  int max_height_um =
      size->HasVariableHeight() ? size->max_height * kMicronsPerPwgUnit : 0;

  return PrinterSemanticCapsAndDefaults::Paper(
      /*display_name=*/"", /*vendor_id=*/"", size_um, printable_area_um,
      max_height_um, /*has_borderless_variant=*/false
#if BUILDFLAG(IS_CHROMEOS)
      ,
      PaperMargins(size->top_margin * kMicronsPerPwgUnit,
                   size->right_margin * kMicronsPerPwgUnit,
                   size->bottom_margin * kMicronsPerPwgUnit,
                   size->left_margin * kMicronsPerPwgUnit)
#endif  // BUILDFLAG(IS_CHROMEOS)
  );
}

bool PaperIsBorderless(const PrinterSemanticCapsAndDefaults::Paper& paper) {
  return paper.printable_area_um().x() == 0 &&
         paper.printable_area_um().y() == 0 &&
         paper.printable_area_um().width() == paper.size_um().width() &&
         paper.printable_area_um().height() == paper.size_um().height();
}

PrinterSemanticCapsAndDefaults::Papers SupportedPapers(
    const CupsPrinter& printer) {
  auto size_comparer = [](const gfx::Size& a, const gfx::Size& b) {
    auto result = a.width() - b.width();
    if (result == 0) {
      result = a.height() - b.height();
    }
    return result < 0;
  };
  std::map<gfx::Size, PrinterSemanticCapsAndDefaults::Paper,
           decltype(size_comparer)>
      paper_map;

  ipp_attribute_t* attr = printer.GetMediaColDatabase();
  int count = ippGetCount(attr);

  for (int i = 0; i < count; i++) {
    std::optional<PrinterSemanticCapsAndDefaults::Paper> paper_opt =
        PaperFromMediaColDatabaseEntry(ippGetCollection(attr, i));
    if (!paper_opt.has_value()) {
      continue;
    }

    const auto& paper = paper_opt.value();
    auto existing_entry = paper_map.find(paper.size_um());

    if (existing_entry == paper_map.end()) {
      paper_map.emplace(paper.size_um(), paper);
      continue;
    }

    // When a paper size has both bordered and borderless variants, set the
    // printable area according to the bordered variant, and set the flag
    // indicating that a borderless variant exists.
    if (PaperIsBorderless(existing_entry->second)) {
      if (!PaperIsBorderless(paper)) {
        existing_entry->second = paper;
        existing_entry->second.set_has_borderless_variant(true);
      }
    } else if (PaperIsBorderless(paper)) {
      existing_entry->second.set_has_borderless_variant(true);
    }
  }

  PrinterSemanticCapsAndDefaults::Papers parsed_papers;
  parsed_papers.reserve(paper_map.size());
  for (const auto& entry : paper_map) {
    parsed_papers.push_back(entry.second);
  }
  return parsed_papers;
}

// Overrides the given printer's default media type as needed.
void CorrectDefaultMediaType(PrinterSemanticCapsAndDefaults*& printer_info) {
  // Some Canon printers give a proprietary default media type that's frequently
  // unavailable to users.
  if (base::StartsWith(printer_info->default_media_type.vendor_id, "com.canon",
                       base::CompareCase::INSENSITIVE_ASCII)) {
    for (const auto& media_type : printer_info->media_types) {
      if (media_type.vendor_id == "stationery") {
        printer_info->default_media_type = media_type;
        break;
      }
    }
  }
}

void ExtractMediaTypes(const CupsOptionProvider& printer,
                       PrinterSemanticCapsAndDefaults* printer_info) {
  printer_info->media_types.clear();
  printer_info->default_media_type =
      PrinterSemanticCapsAndDefaults::MediaType();
  std::vector<std::string_view> names =
      printer.GetSupportedOptionValueStrings(kIppMediaType);
  if (names.empty()) {
    return;
  }
  printer_info->media_types.reserve(names.size());

  for (std::string_view vendor_id : names) {
    PrinterSemanticCapsAndDefaults::MediaType type;
    type.vendor_id = std::string(vendor_id);

    // This name will be overwritten by its Chromium localization if it's a
    // standard IPP media type. But for non-standard types, the only
    // human-readable name available is this one from the driver (or
    // driverless printer).
    const char* display_name = printer.GetLocalizedOptionValueName(
        kIppMediaType, type.vendor_id.c_str());
    if (display_name) {
      type.display_name = display_name;
    } else {
      type.display_name = std::string(vendor_id);
    }

    printer_info->media_types.push_back(type);
  }

  // Set default media type, or use the first in the list if unavailable.
  DCHECK(!printer_info->media_types.empty());
  printer_info->default_media_type = printer_info->media_types[0];
  ipp_t* media_col_default =
      ippGetCollection(printer.GetDefaultOptionValue(kIppMediaCol), 0);
  const char* media_type_default = ippGetString(
      ippFindAttribute(media_col_default, kIppMediaType, IPP_TAG_KEYWORD), 0,
      nullptr);
  if (media_type_default) {
    // Don't set the "default" media type if it isn't in the list of supported
    // media types for some reason.
    for (const auto& media_type : printer_info->media_types) {
      if (media_type.vendor_id == media_type_default) {
        printer_info->default_media_type = media_type;
      }
    }
  }

  CorrectDefaultMediaType(printer_info);
}

bool CollateCapable(const CupsOptionProvider& printer) {
  std::vector<std::string_view> values =
      printer.GetSupportedOptionValueStrings(kIppCollate);
  return std::ranges::contains(values, kCollated) &&
         std::ranges::contains(values, kUncollated);
}

bool CollateDefault(const CupsOptionProvider& printer) {
  ipp_attribute_t* attr = printer.GetDefaultOptionValue(kIppCollate);
  if (!attr)
    return false;

  const char* const name = ippGetString(attr, 0, nullptr);
  return name && !std::string_view(name).compare(kCollated);
}

#if BUILDFLAG(IS_CHROMEOS)
bool PinSupported(const CupsOptionProvider& printer) {
  ipp_attribute_t* attr = printer.GetSupportedOptionValues(kIppPin);
  if (!attr)
    return false;
  int password_maximum_length_supported = ippGetInteger(attr, 0);
  if (password_maximum_length_supported < kPinMinimumLength)
    return false;

  std::vector<std::string_view> values =
      printer.GetSupportedOptionValueStrings(kIppPinEncryption);
  return std::ranges::contains(values, kPinEncryptionNone);
}

// Returns the number of IPP attributes added to `caps` (not necessarily in
// 1-to-1 correspondence).
size_t AddAttributes(const CupsOptionProvider& printer,
                     const char* attr_group_name,
                     AdvancedCapabilities* caps) {
  ipp_attribute_t* attr = printer.GetSupportedOptionValues(attr_group_name);
  if (!attr)
    return 0;

  int num_options = ippGetCount(attr);
  static const base::NoDestructor<HandlerMap> handlers(GenerateHandlers());
  // The names of attributes that we know are not supported (b/266573545).
  static constexpr auto kOptionsToIgnore =
      base::MakeFixedFlatSet<std::string_view>(
          {"finishings-col", "ipp-attribute-fidelity", "job-name",
           "number-up-layout"});
  std::vector<std::string> unknown_options;
  size_t attr_count = 0;
  for (int i = 0; i < num_options; i++) {
    const char* option_name = ippGetString(attr, i, nullptr);
    if (kOptionsToIgnore.contains(option_name)) {
      continue;
    }
    auto it = handlers->find(option_name);
    if (it == handlers->end()) {
      unknown_options.emplace_back(option_name);
      continue;
    }

    size_t previous_size = caps->size();
    // Run the handler that adds items to `caps` based on option type.
    it->second.Run(printer, option_name, caps);
    if (caps->size() > previous_size)
      attr_count++;
  }
  if (!unknown_options.empty()) {
    LOG(WARNING) << "Unknown IPP options: "
                 << base::JoinString(unknown_options, ", ");
  }
  return attr_count;
}

// Adds the "Input Tray" option to Advanced Attributes.
size_t AddInputTray(const CupsOptionProvider& printer,
                    AdvancedCapabilities* caps) {
  size_t previous_size = caps->size();
  KeywordHandler(printer, kIppMediaSource, caps);
  return caps->size() - previous_size;
}

void ExtractAdvancedCapabilities(const CupsOptionProvider& printer,
                                 PrinterSemanticCapsAndDefaults* printer_info) {
  AdvancedCapabilities* options = &printer_info->advanced_capabilities;
  options->clear();
  size_t attr_count = AddInputTray(printer, options);
  attr_count += AddAttributes(printer, kIppJobAttributes, options);
  attr_count += AddAttributes(printer, kIppDocumentAttributes, options);
  base::UmaHistogramCounts1000("Printing.CUPS.IppAttributesCount", attr_count);
}

// Convert string value to mojom::PrintScalingType
mojom::PrintScalingType PrintScalingTypeFromString(
    const std::string_view value) {
  if (value == "auto") {
    return mojom::PrintScalingType::kAuto;
  }
  if (value == "auto-fit") {
    return mojom::PrintScalingType::kAutoFit;
  }
  if (value == "fill") {
    return mojom::PrintScalingType::kFill;
  }
  if (value == "fit") {
    return mojom::PrintScalingType::kFit;
  }
  if (value == "none") {
    return mojom::PrintScalingType::kNone;
  }

  // Default to unknown for any unrecognized values.
  return mojom::PrintScalingType::kUnknownPrintScalingType;
}

void ExtractPrintScaling(const CupsOptionProvider& printer,
                         PrinterSemanticCapsAndDefaults* printer_info) {
  printer_info->print_scaling_types.clear();
  printer_info->print_scaling_type_default =
      mojom::PrintScalingType::kUnknownPrintScalingType;

  std::vector<std::string_view> values =
      printer.GetSupportedOptionValueStrings(kIppPrintScaling);
  for (const auto& value : values) {
    auto type = PrintScalingTypeFromString(value);
    if (type != mojom::PrintScalingType::kUnknownPrintScalingType) {
      printer_info->print_scaling_types.emplace_back(type);
    }
  }

  if (printer_info->print_scaling_types.empty()) {
    return;
  }

  // Get default value
  ipp_attribute_t* attr = printer.GetDefaultOptionValue(kIppPrintScaling);
  if (const char* const name = ippGetString(attr, 0, nullptr)) {
    printer_info->print_scaling_type_default = PrintScalingTypeFromString(name);
  }

  if (printer_info->print_scaling_type_default ==
          mojom::PrintScalingType::kUnknownPrintScalingType &&
      !printer_info->print_scaling_types.empty()) {
    // If no default is provided, use the first value.
    printer_info->print_scaling_type_default =
        printer_info->print_scaling_types[0];
  }
}
#endif  // BUILDFLAG(IS_CHROMEOS)

}  // namespace

PrinterSemanticCapsAndDefaults::Paper DefaultPaper(const CupsPrinter& printer) {
  ipp_attribute_t* attr = printer.GetDefaultOptionValue(kIppMediaCol);
  if (!attr)
    return PrinterSemanticCapsAndDefaults::Paper();
  ipp_t* media_col_default = ippGetCollection(attr, 0);
  if (!media_col_default) {
    return PrinterSemanticCapsAndDefaults::Paper();
  }

  PrinterSemanticCapsAndDefaults::Paper paper;
  return PaperFromMediaColDatabaseEntry(media_col_default)
      .value_or(PrinterSemanticCapsAndDefaults::Paper());
}

void CapsAndDefaultsFromPrinter(const CupsPrinter& printer,
                                PrinterSemanticCapsAndDefaults* printer_info) {
  // collate
  printer_info->collate_default = CollateDefault(printer);
  printer_info->collate_capable = CollateCapable(printer);

  // paper
  printer_info->default_paper = DefaultPaper(printer);
  printer_info->papers = SupportedPapers(printer);

#if BUILDFLAG(IS_CHROMEOS)
  printer_info->pin_supported = PinSupported(printer);
  ExtractAdvancedCapabilities(printer, printer_info);
  ExtractPrintScaling(printer, printer_info);
#endif  // BUILDFLAG(IS_CHROMEOS)

  ExtractCopies(printer, printer_info);
  ExtractColor(printer, printer_info);
  ExtractDuplexModes(printer, printer_info);
  ExtractResolutions(printer, printer_info);
  ExtractMediaTypes(printer, printer_info);
}

gfx::Rect GetPrintableAreaForSize(const CupsPrinter& printer,
                                  const gfx::Size& size_um) {
  ipp_attribute_t* attr = printer.GetMediaColDatabase();
  int count = ippGetCount(attr);
  gfx::Rect result(0, 0, size_um.width(), size_um.height());

  for (int i = 0; i < count; i++) {
    ipp_t* db_entry = ippGetCollection(attr, i);

    std::optional<PrinterSemanticCapsAndDefaults::Paper> paper_opt =
        PaperFromMediaColDatabaseEntry(db_entry);
    if (!paper_opt.has_value()) {
      continue;
    }

    const auto& paper = paper_opt.value();
    if (paper.size_um() != size_um) {
      continue;
    }

    result = paper.printable_area_um();

    // If this is a borderless size, try to find a non-borderless version.
    if (!PaperIsBorderless(paper)) {
      return result;
    }
  }

  return result;
}

ScopedIppPtr WrapIpp(ipp_t* ipp) {
  return ScopedIppPtr(ipp, IppDeleter());
}

void IppDeleter::operator()(ipp_t* ipp) const {
  ippDelete(ipp);
}

std::optional<MediaColData> ExtractMediaColData(ipp_t* db_entry) {
  if (!db_entry) {
    LOG(WARNING) << "Invalid media-col-database entry: empty entry.";
    return std::nullopt;
  }
  ipp_t* media_size = ippGetCollection(
      ippFindAttribute(db_entry, kIppMediaSize, IPP_TAG_BEGIN_COLLECTION), 0);
  if (!media_size) {
    LOG(WARNING) << "Invalid media-col-database entry: empty media_size.";
    return std::nullopt;
  }

  ipp_attribute_t* bottom_attr =
      ippFindAttribute(db_entry, kIppMediaBottomMargin, IPP_TAG_INTEGER);
  ipp_attribute_t* left_attr =
      ippFindAttribute(db_entry, kIppMediaLeftMargin, IPP_TAG_INTEGER);
  ipp_attribute_t* right_attr =
      ippFindAttribute(db_entry, kIppMediaRightMargin, IPP_TAG_INTEGER);
  ipp_attribute_t* top_attr =
      ippFindAttribute(db_entry, kIppMediaTopMargin, IPP_TAG_INTEGER);
  if (!bottom_attr || !left_attr || !right_attr || !top_attr) {
    LOG(WARNING) << "Invalid media-col-database entry:"
                 << " margins are not present.";
    return std::nullopt;
  }
  int bottom_margin = ippGetInteger(bottom_attr, 0);
  int left_margin = ippGetInteger(left_attr, 0);
  int right_margin = ippGetInteger(right_attr, 0);
  int top_margin = ippGetInteger(top_attr, 0);

  ipp_attribute_t* width_attr =
      ippFindAttribute(media_size, kIppXDimension, IPP_TAG_INTEGER);
  ipp_attribute_t* height_attr =
      ippFindAttribute(media_size, kIppYDimension, IPP_TAG_INTEGER);
  ipp_attribute_t* width_range_attr =
      ippFindAttribute(media_size, kIppXDimension, IPP_TAG_RANGE);
  ipp_attribute_t* height_range_attr =
      ippFindAttribute(media_size, kIppYDimension, IPP_TAG_RANGE);

  // Ensure there is a width and height (or ranges).
  if ((!width_attr && !width_range_attr) ||
      (!height_attr && !height_range_attr)) {
    LOG(WARNING) << "Invalid media-col-database entry:"
                 << " media-size needs x and y (or x and y range).";
    return std::nullopt;
  }

  int min_width = 0;
  int min_height = 0;
  int max_width = 0;
  int max_height = 0;
  if (width_attr) {
    min_width = ippGetInteger(width_attr, 0);
    max_width = min_width;
  } else {
    min_width = ippGetRange(width_range_attr, 0, &max_width);
  }
  if (height_attr) {
    min_height = ippGetInteger(height_attr, 0);
    max_height = min_height;
  } else {
    min_height = ippGetRange(height_range_attr, 0, &max_height);
  }

  if (min_width > max_width) {
    LOG(WARNING) << "Invalid media-col-database entry:"
                 << " min_width (" << min_width << ") > max_width ("
                 << max_width << ").";
    return std::nullopt;
  }
  if (min_height > max_height) {
    LOG(WARNING) << "Invalid media-col-database entry:"
                 << " min_height (" << min_height << ") > max_height ("
                 << max_height << ").";
    return std::nullopt;
  }

#if BUILDFLAG(IS_MAC)
  pwg_media_t* media = pwgMediaForSize(max_width, max_height);
  if (media && (media->width != max_width || media->length != max_height)) {
    // Paper size detected to be close to a standard size.  While the maximum
    // size will be based on this media, must also do checks to adjust the
    // minimum dimensions, as they must not end up exceeding the new maximum.
    if (min_width == max_width || min_width > media->width) {
      min_width = media->width;
    }
    if (min_height == max_height || min_height > media->length) {
      min_height = media->length;
    }
    max_width = media->width;
    max_height = media->length;
  }
#endif

  return MediaColData{min_width,     min_height,  max_width,    max_height,
                      bottom_margin, left_margin, right_margin, top_margin};
}

ScopedIppPtr NewMediaCollection(const MediaColData& data) {
  // Don't create any entries with a variable width.
  if (data.HasVariableWidth()) {
    return nullptr;
  }

  ScopedIppPtr media_col = WrapIpp(ippNew());
  ScopedIppPtr media_size = WrapIpp(ippNew());

  ippAddInteger(media_size.get(), IPP_TAG_PRINTER, IPP_TAG_INTEGER,
                kIppXDimension, data.min_width);
  if (data.HasVariableHeight()) {
    ippAddRange(media_size.get(), IPP_TAG_PRINTER, kIppYDimension,
                data.min_height, data.max_height);
  } else {
    ippAddInteger(media_size.get(), IPP_TAG_PRINTER, IPP_TAG_INTEGER,
                  kIppYDimension, data.min_height);
  }
  ippAddCollection(media_col.get(), IPP_TAG_PRINTER, kIppMediaSize,
                   media_size.get());

  ippAddInteger(media_col.get(), IPP_TAG_PRINTER, IPP_TAG_INTEGER,
                kIppMediaBottomMargin, data.bottom_margin);
  ippAddInteger(media_col.get(), IPP_TAG_PRINTER, IPP_TAG_INTEGER,
                kIppMediaLeftMargin, data.left_margin);
  ippAddInteger(media_col.get(), IPP_TAG_PRINTER, IPP_TAG_INTEGER,
                kIppMediaRightMargin, data.right_margin);
  ippAddInteger(media_col.get(), IPP_TAG_PRINTER, IPP_TAG_INTEGER,
                kIppMediaTopMargin, data.top_margin);

  return media_col;
}

void FilterMediaColSizes(ScopedIppPtr& attributes) {
  if (!attributes) {
    return;
  }

  ipp_attribute_t* media_col_db = ippFindAttribute(
      attributes.get(), kIppMediaColDatabase, IPP_TAG_BEGIN_COLLECTION);
  if (!media_col_db) {
    return;
  }

  // `fixed_widths` is the width for any media-col-database entry that is
  // non-variable.  For instance, if the media-col-database has 4 fixed
  // widthxheight entries and one variable widthxheight entry like so:
  //
  // 58mmx200mm
  // 58mmx2000mm
  // 80mmx200mm
  // 80mmx2000mm
  // 10-80mmx20-2000mm
  //
  // then `fixed_widths` will contain 58 and 80.  After filtering, the following
  // sizes will exist:
  //
  // 58mmx200mm
  // 58mmx2000mm
  // 80mmx200mm
  // 80mmx2000mm
  // 58mmx20-2000mm
  // 80mmx20-2000mm
  std::set<int> fixed_widths;
  std::vector<ScopedIppPtr> new_entries;
  std::vector<MediaColData> variable_height_entries;

  // Loop over all the `media_col_db` entries and either add them to
  // `new_entries`, skip them, or save them in `variable_height_entries` so
  // they can be added later (once all the fixed widths have been discovered).
  for (int i = 0; i < ippGetCount(media_col_db); i++) {
    ipp_t* db_entry = ippGetCollection(media_col_db, i);
    std::optional<MediaColData> size = ExtractMediaColData(db_entry);
    if (!size.has_value()) {
      return;
    }

    // TODO(nmuggli): Consider adding the boundaries of a variable-width entry
    // to `fixed_widths`.
    if (!size->HasVariableWidth()) {
      fixed_widths.insert(size->min_width);
    }

    // Handle four different cases:
    // 1.  Variable width and fixed height.  Skip this - variable widths are not
    // supported.
    // 2.  Fixed width and fixed height.  Add to the new array.
    // 3.  Fixed width and variable height.  Add to the new array.
    // 4.  Variable width and variable height.  Save this entry until after
    // all of the entries are processed.  For each fixed width that fits
    // within this variable width, a new entry will get added with the
    // variable height.

    // Case 1:  skip over this.
    if (size->HasVariableWidth() && !size->HasVariableHeight()) {
      continue;
    }

    // Case 2 and case 3 - add to `new_entries`.
    if (!size->HasVariableWidth()) {
      ScopedIppPtr new_entry = NewMediaCollection(size.value());
      if (new_entry) {
        new_entries.push_back(std::move(new_entry));
      }
      continue;
    }

    // Case 4: Save entry for later processing.
    variable_height_entries.push_back(size.value());
  }

  for (const MediaColData& variable_height_entry : variable_height_entries) {
    // For each fixed width, create a new media size entry for that width and a
    // variable height.
    for (int width : fixed_widths) {
      MediaColData size = variable_height_entry;
      // Ensure `width` fits within the variable width.
      if (width < size.min_width || width > size.max_width) {
        continue;
      }
      size.min_width = width;
      size.max_width = width;
      ScopedIppPtr new_entry = NewMediaCollection(size);
      if (new_entry) {
        new_entries.push_back(std::move(new_entry));
      }
    }
  }

  // Finally, update the attribute that was passed in with the new entries.
  ippDeleteAttribute(attributes.get(), media_col_db);
  std::vector<const ipp_t*> raw_entries(new_entries.size());
  std::ranges::transform(new_entries, raw_entries.begin(), &ScopedIppPtr::get);
  ippAddCollections(attributes.get(), IPP_TAG_PRINTER, kIppMediaColDatabase,
                    raw_entries.size(), raw_entries.data());
}

}  //  namespace printing
