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

#include "mozilla/IntegerRange.h"
#include "mozilla/Maybe.h"
#include "mozilla/StaticPrefs_print.h"
#include "nsCUPSShim.h"
#include "nsPrinterCUPS.h"
#include "nsString.h"
#include "prenv.h"

// Use a local static to initialize the CUPS shim lazily, when it's needed.
// This is used in order to avoid a global constructor.
static nsCUPSShim& CupsShim() {
  static nsCUPSShim sCupsShim;
  return sCupsShim;
}

using PrinterInfo = nsPrinterListBase::PrinterInfo;

/**
 * Retrieves a human-readable name for the printer from CUPS.
 * https://www.cups.org/doc/cupspm.html#basic-destination-information
 */
static void GetDisplayNameForPrinter(const cups_dest_t& aDest,
                                     nsAString& aName) {
// macOS clients expect prettified printer names
// while GTK clients expect non-prettified names.
// If you change this, please change NamedPrinter accordingly.
#ifdef XP_MACOSX
  const char* displayName = CupsShim().cupsGetOption(
      "printer-info", aDest.num_options, aDest.options);
  if (displayName) {
    CopyUTF8toUTF16(mozilla::MakeStringSpan(displayName), aName);
  }
#endif
}

NS_IMETHODIMP
nsPrinterListCUPS::InitPrintSettingsFromPrinter(
    const nsAString& aPrinterName, nsIPrintSettings* aPrintSettings) {
  MOZ_ASSERT(aPrintSettings);

  // Set a default file name.
  nsAutoString filename;
  nsresult rv = aPrintSettings->GetToFileName(filename);
  if (NS_FAILED(rv) || filename.IsEmpty()) {
    const char* path = PR_GetEnv("PWD");
    if (!path) {
      path = PR_GetEnv("HOME");
    }

    if (path) {
      CopyUTF8toUTF16(mozilla::MakeStringSpan(path), filename);
      filename.AppendLiteral("/mozilla.pdf");
    } else {
      filename.AssignLiteral("mozilla.pdf");
    }

    aPrintSettings->SetToFileName(filename);
  }

  aPrintSettings->SetIsInitializedFromPrinter(true);
  return NS_OK;
}

static int CupsDestCallback(void* user_data, unsigned aFlags,
                            cups_dest_t* aDest) {
  MOZ_ASSERT(user_data);
  auto* printerInfoList = static_cast<nsTArray<PrinterInfo>*>(user_data);

  cups_dest_t* ownedDest = nullptr;
  mozilla::DebugOnly<const int> numCopied =
      CupsShim().cupsCopyDest(aDest, 0, &ownedDest);
  MOZ_ASSERT(numCopied == 1);

  nsString name;
  GetDisplayNameForPrinter(*aDest, name);

  printerInfoList->AppendElement(PrinterInfo{std::move(name), ownedDest});

  return aFlags == CUPS_DEST_FLAGS_MORE ? 1 : 0;
}

nsTArray<PrinterInfo> nsPrinterListCUPS::Printers() const {
  if (!CupsShim().InitOkay()) {
    return {};
  }

  auto FreeDestsAndClear = [](nsTArray<PrinterInfo>& aArray) {
    for (auto& info : aArray) {
      CupsShim().cupsFreeDests(1, static_cast<cups_dest_t*>(info.mCupsHandle));
    }
    aArray.Clear();
  };

  nsTArray<PrinterInfo> printerInfoList;
  // cupsGetDests2 returns list of found printers without duplicates, unlike
  // cupsEnumDests
  cups_dest_t* printers = nullptr;
  const auto numPrinters = CupsShim().cupsGetDests2(nullptr, &printers);
  if (numPrinters > 0) {
    for (auto i : mozilla::IntegerRange(0, numPrinters)) {
      cups_dest_t* ownedDest = nullptr;
      mozilla::DebugOnly<const int> numCopied =
          CupsShim().cupsCopyDest(printers + i, 0, &ownedDest);
      MOZ_ASSERT(numCopied == 1);

      nsString name;
      GetDisplayNameForPrinter(*(printers + i), name);
      printerInfoList.AppendElement(PrinterInfo{std::move(name), ownedDest});
    }
    CupsShim().cupsFreeDests(numPrinters, printers);
    return printerInfoList;
  }

  // An error occurred - retry with CUPS_PRINTER_DISCOVERED masked out (since
  // it looks like there are a lot of error cases for that in cupsEnumDests):
  if (CupsShim().cupsEnumDests(
          CUPS_DEST_FLAGS_NONE,
          0 /* 0 timeout should be okay when masking CUPS_PRINTER_DISCOVERED */,
          nullptr /* cancel* */, CUPS_PRINTER_LOCAL,
          CUPS_PRINTER_FAX | CUPS_PRINTER_SCANNER | CUPS_PRINTER_DISCOVERED,
          &CupsDestCallback, &printerInfoList)) {
    return printerInfoList;
  }

  // Another error occurred. Maybe printerInfoList could be partially
  // populated, so perhaps we could return it without clearing it in the hope
  // that there are some usable dests. However, presuambly CUPS doesn't
  // guarantee that any dests that it added are complete and safe to use when
  // an error occurs?
  FreeDestsAndClear(printerInfoList);
  return {};
}

RefPtr<nsIPrinter> nsPrinterListCUPS::CreatePrinter(PrinterInfo aInfo) const {
  return mozilla::MakeRefPtr<nsPrinterCUPS>(
      mCommonPaperInfo, CupsShim(), std::move(aInfo.mName),
      static_cast<cups_dest_t*>(aInfo.mCupsHandle));
}

mozilla::Maybe<PrinterInfo> nsPrinterListCUPS::PrinterByName(
    nsString aPrinterName) const {
  mozilla::Maybe<PrinterInfo> rv;
  if (!CupsShim().InitOkay()) {
    return rv;
  }

  // Will contain the printer, if found. This must be fully owned, and not a
  // member of another array of printers.
  cups_dest_t* printer = nullptr;

#ifdef XP_MACOSX
  // On OS X the printer name given to this function is the readable/display
  // name and not the CUPS name, so we iterate over all the printers for now.
  // See bug 1659807 for one approach to improve perf here.
  {
    nsAutoCString printerName;
    CopyUTF16toUTF8(aPrinterName, printerName);
    cups_dest_t* printers = nullptr;
    const auto numPrinters = CupsShim().cupsGetDests(&printers);
    for (auto i : mozilla::IntegerRange(0, numPrinters)) {
      const char* const displayName = CupsShim().cupsGetOption(
          "printer-info", printers[i].num_options, printers[i].options);
      if (printerName == displayName) {
        // The second arg to CupsShim().cupsCopyDest is called num_dests, but
        // it actually copies num_dests + 1 elements.
        CupsShim().cupsCopyDest(printers + i, 0, &printer);
        break;
      }
    }
    CupsShim().cupsFreeDests(numPrinters, printers);
  }
#else
  // On GTK, we only ever show the CUPS name of printers, so we can use
  // cupsGetNamedDest directly.
  {
    const auto printerName = NS_ConvertUTF16toUTF8(aPrinterName);
    printer = CupsShim().cupsGetNamedDest(CUPS_HTTP_DEFAULT, printerName.get(),
                                          nullptr);
  }
#endif

  if (printer) {
    // Since the printer name had to be passed by-value, we can move the
    // name from that.
    rv.emplace(PrinterInfo{std::move(aPrinterName), printer});
  }
  return rv;
}

mozilla::Maybe<PrinterInfo> nsPrinterListCUPS::PrinterBySystemName(
    nsString aPrinterName) const {
  mozilla::Maybe<PrinterInfo> rv;
  if (!CupsShim().InitOkay()) {
    return rv;
  }

  const auto printerName = NS_ConvertUTF16toUTF8(aPrinterName);
  if (cups_dest_t* const printer = CupsShim().cupsGetNamedDest(
          CUPS_HTTP_DEFAULT, printerName.get(), nullptr)) {
    rv.emplace(PrinterInfo{std::move(aPrinterName), printer});
  }
  return rv;
}

nsresult nsPrinterListCUPS::SystemDefaultPrinterName(nsAString& aName) const {
  aName.Truncate();

  if (!CupsShim().InitOkay()) {
    return NS_OK;
  }

  // Passing in nullptr for the name will return the default, if any.
  cups_dest_t* dest =
      CupsShim().cupsGetNamedDest(CUPS_HTTP_DEFAULT, /* name */ nullptr,
                                  /* instance */ nullptr);
  if (!dest) {
    return NS_OK;
  }

  GetDisplayNameForPrinter(*dest, aName);
  if (aName.IsEmpty()) {
    CopyUTF8toUTF16(mozilla::MakeStringSpan(dest->name), aName);
  }

  CupsShim().cupsFreeDests(1, dest);
  return NS_OK;
}
