/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 "nsPrinterWin.h"

#include <algorithm>
#include <windows.h>
#include <winspool.h>

#include "mozilla/Array.h"
#include "mozilla/dom/Promise.h"
#include "nsPaper.h"
#include "nsPrintSettingsImpl.h"
#include "nsPrintSettingsWin.h"
#include "nsWindowsHelpers.h"
#include "PrintBackgroundTask.h"
#include "WinUtils.h"

using namespace mozilla;
using namespace mozilla::gfx;
using namespace mozilla::widget;
using mozilla::PrintSettingsInitializer;
using mozilla::dom::Promise;

static const double kPointsPerTenthMM = 72.0 / 254.0;
static const double kPointsPerInch = 72.0;

nsPrinterWin::nsPrinterWin(const CommonPaperInfoArray* aArray,
                           const nsAString& aName)
    : nsPrinterBase(aArray),
      mName(aName),
      mDefaultDevmodeWStorage("nsPrinterWin::mDefaultDevmodeWStorage") {}

// static
already_AddRefed<nsPrinterWin> nsPrinterWin::Create(
    const CommonPaperInfoArray* aArray, const nsAString& aName) {
  return do_AddRef(new nsPrinterWin(aArray, aName));
}

template <class T>
static nsTArray<T> GetDeviceCapabilityArray(const LPWSTR aPrinterName,
                                            WORD aCapabilityID,
                                            mozilla::Mutex& aDriverMutex,
                                            int& aCount) {
  MOZ_ASSERT(aCount >= 0, "Possibly passed aCount from previous error case.");

  nsTArray<T> caps;

  // We only want to access printer drivers in the parent process.
  if (!XRE_IsParentProcess()) {
    return caps;
  }

  // Both the call to get the size and the call to actually populate the array
  // are relatively expensive, so as sometimes the lengths of the arrays that we
  // retrieve depend on each other we allow a count to be passed in to save the
  // first call. As we allocate double the count anyway this should allay any
  // safety worries.
  if (!aCount) {
    // Passing nullptr as the port here seems to work. Given that we would have
    // to OpenPrinter with just the name anyway to get the port that makes
    // sense. Also, the printer set-up seems to stop you from having two
    // printers with the same name. Note: this (and the call below) are blocking
    // calls, which could be slow.
    MutexAutoLock autoLock(aDriverMutex);
    aCount = ::DeviceCapabilitiesW(aPrinterName, nullptr, aCapabilityID,
                                   nullptr, nullptr);
    if (aCount <= 0) {
      return caps;
    }
  }

  // As DeviceCapabilitiesW doesn't take a size, there is a greater risk of the
  // buffer being overflowed, so we over-allocate for safety.
  caps.SetLength(aCount * 2);
  MutexAutoLock autoLock(aDriverMutex);
  int count =
      ::DeviceCapabilitiesW(aPrinterName, nullptr, aCapabilityID,
                            reinterpret_cast<LPWSTR>(caps.Elements()), nullptr);
  if (count <= 0) {
    caps.Clear();
    return caps;
  }

  // We know from bug 1673708 that sometimes the final array returned is smaller
  // than the required array count. Assert here to see if this is reproduced on
  // test servers.
  MOZ_ASSERT(count == aCount, "Different array count returned than expected.");

  // Note that TruncateLength will crash if count > caps.Length().
  caps.TruncateLength(count);
  return caps;
}

static void DevmodeToSettingsInitializer(
    const nsString& aPrinterName, const DEVMODEW* aDevmode,
    mozilla::Mutex& aDriverMutex,
    PrintSettingsInitializer& aSettingsInitializer) {
  aSettingsInitializer.mPrinter.Assign(aPrinterName);

  HDC dc;
  {
    MutexAutoLock autoLock(aDriverMutex);
    dc = ::CreateICW(nullptr, aPrinterName.get(), nullptr, aDevmode);
  }
  nsAutoHDC printerDc(dc);
  MOZ_ASSERT(printerDc, "CreateICW failed");
  if (!printerDc) {
    return;
  }

  if (aDevmode->dmFields & DM_PAPERSIZE) {
    aSettingsInitializer.mPaperInfo.mId.Truncate();
    aSettingsInitializer.mPaperInfo.mId.AppendInt(aDevmode->dmPaperSize);
    // If it is not a paper size we know about, the unit will remain unchanged.
    nsPrintSettingsWin::PaperSizeUnitFromDmPaperSize(
        aDevmode->dmPaperSize, aSettingsInitializer.mPaperSizeUnit);
  }

  int pixelsPerInchY = ::GetDeviceCaps(printerDc, LOGPIXELSY);
  int physicalHeight = ::GetDeviceCaps(printerDc, PHYSICALHEIGHT);
  double heightInInches = double(physicalHeight) / pixelsPerInchY;
  int pixelsPerInchX = ::GetDeviceCaps(printerDc, LOGPIXELSX);
  int physicalWidth = ::GetDeviceCaps(printerDc, PHYSICALWIDTH);
  double widthInches = double(physicalWidth) / pixelsPerInchX;
  if (aDevmode->dmFields & DM_ORIENTATION &&
      aDevmode->dmOrientation == DMORIENT_LANDSCAPE) {
    std::swap(widthInches, heightInInches);
  }
  aSettingsInitializer.mPaperInfo.mSize.SizeTo(widthInches * kPointsPerInch,
                                               heightInInches * kPointsPerInch);

  gfx::MarginDouble margin =
      WinUtils::GetUnwriteableMarginsForDeviceInInches(printerDc);
  aSettingsInitializer.mPaperInfo.mUnwriteableMargin = Some(MarginDouble{
      margin.top * kPointsPerInch, margin.right * kPointsPerInch,
      margin.bottom * kPointsPerInch, margin.left * kPointsPerInch});

  // Using Y to match existing code for print scaling calculations.
  aSettingsInitializer.mResolution = pixelsPerInchY;

  if (aDevmode->dmFields & DM_COLOR) {
    // See comment for PrintSettingsInitializer.mPrintInColor
    aSettingsInitializer.mPrintInColor =
        aDevmode->dmColor != DMCOLOR_MONOCHROME;
  }

  if (aDevmode->dmFields & DM_ORIENTATION) {
    aSettingsInitializer.mSheetOrientation =
        int32_t(aDevmode->dmOrientation == DMORIENT_PORTRAIT
                    ? nsPrintSettings::kPortraitOrientation
                    : nsPrintSettings::kLandscapeOrientation);
  }

  if (aDevmode->dmFields & DM_COPIES) {
    aSettingsInitializer.mNumCopies = aDevmode->dmCopies;
  }

  if (aDevmode->dmFields & DM_DUPLEX) {
    switch (aDevmode->dmDuplex) {
      default:
        MOZ_FALLTHROUGH_ASSERT("bad value for dmDuplex field");
      case DMDUP_SIMPLEX:
        aSettingsInitializer.mDuplex = nsPrintSettings::kDuplexNone;
        break;
      case DMDUP_VERTICAL:
        aSettingsInitializer.mDuplex = nsPrintSettings::kDuplexFlipOnLongEdge;
        break;
      case DMDUP_HORIZONTAL:
        aSettingsInitializer.mDuplex = nsPrintSettings::kDuplexFlipOnShortEdge;
        break;
    }
  }
}

NS_IMETHODIMP
nsPrinterWin::GetName(nsAString& aName) {
  aName.Assign(mName);
  return NS_OK;
}

NS_IMETHODIMP
nsPrinterWin::GetSystemName(nsAString& aName) {
  aName.Assign(mName);
  return NS_OK;
}

namespace mozilla {
template <>
void ResolveOrReject(Promise& aPromise, nsPrinterWin& aPrinter,
                     const PrintSettingsInitializer& aResult) {
  aPromise.MaybeResolve(
      RefPtr<nsIPrintSettings>(CreatePlatformPrintSettings(aResult)));
}
}  // namespace mozilla

NS_IMETHODIMP nsPrinterWin::CopyFromWithValidation(
    nsIPrintSettings* aSettingsToCopyFrom, JSContext* aCx,
    Promise** aResultPromise) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aResultPromise);

  PrintSettingsInitializer settingsInitializer =
      aSettingsToCopyFrom->GetSettingsInitializer();
  return PrintBackgroundTaskPromise(*this, aCx, aResultPromise,
                                    &nsPrinterWin::GetValidatedSettings,
                                    settingsInitializer);
}

bool nsPrinterWin::SupportsDuplex() const {
  MutexAutoLock autoLock(mDriverMutex);
  return ::DeviceCapabilitiesW(mName.get(), nullptr, DC_DUPLEX, nullptr,
                               nullptr) == 1;
}

bool nsPrinterWin::SupportsColor() const {
  MutexAutoLock autoLock(mDriverMutex);
  return ::DeviceCapabilitiesW(mName.get(), nullptr, DC_COLORDEVICE, nullptr,
                               nullptr) == 1;
}

bool nsPrinterWin::SupportsMonochrome() const {
  if (!SupportsColor()) {
    return true;
  }

  nsHPRINTER hPrinter = nullptr;
  if (NS_WARN_IF(!::OpenPrinterW(mName.get(), &hPrinter, nullptr))) {
    return false;
  }
  nsAutoPrinter autoPrinter(hPrinter);

  nsTArray<uint8_t> devmodeWStorage = CopyDefaultDevmodeW();
  if (devmodeWStorage.IsEmpty()) {
    return false;
  }

  auto* devmode = reinterpret_cast<DEVMODEW*>(devmodeWStorage.Elements());

  devmode->dmFields |= DM_COLOR;
  devmode->dmColor = DMCOLOR_MONOCHROME;
  // Try to modify the devmode settings and see if the setting sticks.
  //
  // This has been the only reliable way to detect it that we've found.
  MutexAutoLock autoLock(mDriverMutex);
  LONG ret =
      ::DocumentPropertiesW(nullptr, autoPrinter.get(), mName.get(), devmode,
                            devmode, DM_IN_BUFFER | DM_OUT_BUFFER);
  if (ret != IDOK) {
    return false;
  }
  return !(devmode->dmFields & DM_COLOR) ||
         devmode->dmColor == DMCOLOR_MONOCHROME;
}

bool nsPrinterWin::SupportsCollation() const {
  MutexAutoLock autoLock(mDriverMutex);
  return ::DeviceCapabilitiesW(mName.get(), nullptr, DC_COLLATE, nullptr,
                               nullptr) == 1;
}

nsPrinterBase::PrinterInfo nsPrinterWin::CreatePrinterInfo() const {
  return PrinterInfo{PaperList(), DefaultSettings()};
}

mozilla::gfx::MarginDouble nsPrinterWin::GetMarginsForPaper(
    nsString aPaperId) const {
  gfx::MarginDouble margin;

  nsTArray<uint8_t> devmodeWStorage = CopyDefaultDevmodeW();
  if (devmodeWStorage.IsEmpty()) {
    return margin;
  }

  auto* devmode = reinterpret_cast<DEVMODEW*>(devmodeWStorage.Elements());

  devmode->dmFields = DM_PAPERSIZE;
  devmode->dmPaperSize = _wtoi((const wchar_t*)aPaperId.BeginReading());
  HDC dc;
  {
    MutexAutoLock autoLock(mDriverMutex);
    dc = ::CreateICW(nullptr, mName.get(), nullptr, devmode);
  }
  nsAutoHDC printerDc(dc);
  MOZ_ASSERT(printerDc, "CreateICW failed");
  if (!printerDc) {
    return margin;
  }
  margin = WinUtils::GetUnwriteableMarginsForDeviceInInches(printerDc);
  margin.top *= kPointsPerInch;
  margin.right *= kPointsPerInch;
  margin.bottom *= kPointsPerInch;
  margin.left *= kPointsPerInch;

  return margin;
}

nsTArray<uint8_t> nsPrinterWin::CopyDefaultDevmodeW() const {
  nsTArray<uint8_t> devmodeStorageW;

  auto devmodeStorageWLock = mDefaultDevmodeWStorage.Lock();
  if (devmodeStorageWLock->IsEmpty()) {
    nsHPRINTER hPrinter = nullptr;
    // OpenPrinter could fail if, for example, the printer has been removed
    // or otherwise become inaccessible since it was selected.
    if (NS_WARN_IF(!::OpenPrinterW(mName.get(), &hPrinter, nullptr))) {
      return devmodeStorageW;
    }
    nsAutoPrinter autoPrinter(hPrinter);
    // Allocate devmode storage of the correct size.
    MutexAutoLock autoLock(mDriverMutex);
    LONG bytesNeeded = ::DocumentPropertiesW(nullptr, autoPrinter.get(),
                                             mName.get(), nullptr, nullptr, 0);
    // Note that we must cast the sizeof() to a signed type so that comparison
    // with the signed, potentially-negative bytesNeeded will work!
    MOZ_ASSERT(bytesNeeded >= LONG(sizeof(DEVMODEW)),
               "DocumentPropertiesW failed to get valid size");
    if (bytesNeeded < LONG(sizeof(DEVMODEW))) {
      return devmodeStorageW;
    }

    // Allocate extra space in case of bad drivers that return a too-small
    // result from DocumentProperties.
    // (See https://bugzilla.mozilla.org/show_bug.cgi?id=1664530#c5)
    if (!devmodeStorageWLock->SetLength(bytesNeeded * 2, fallible)) {
      return devmodeStorageW;
    }

    memset(devmodeStorageWLock->Elements(), 0, devmodeStorageWLock->Length());
    auto* devmode =
        reinterpret_cast<DEVMODEW*>(devmodeStorageWLock->Elements());
    LONG ret = ::DocumentPropertiesW(nullptr, autoPrinter.get(), mName.get(),
                                     devmode, nullptr, DM_OUT_BUFFER);
    MOZ_ASSERT(ret == IDOK, "DocumentPropertiesW failed");
    // Make sure that the lengths in the DEVMODEW make sense.
    if (ret != IDOK || devmode->dmSize != sizeof(DEVMODEW) ||
        devmode->dmSize + devmode->dmDriverExtra >
            devmodeStorageWLock->Length()) {
      // Clear mDefaultDevmodeWStorage to make sure we try again next time.
      devmodeStorageWLock->Clear();
      return devmodeStorageW;
    }
  }

  devmodeStorageW.Assign(devmodeStorageWLock.ref());
  return devmodeStorageW;
}

nsTArray<mozilla::PaperInfo> nsPrinterWin::PaperList() const {
  // Paper IDs are returned as WORDs.
  int requiredArrayCount = 0;
  auto paperIds = GetDeviceCapabilityArray<WORD>(
      mName.get(), DC_PAPERS, mDriverMutex, requiredArrayCount);
  if (!paperIds.Length()) {
    return {};
  }

  // Paper names are returned in 64 long character buffers.
  auto paperNames = GetDeviceCapabilityArray<Array<wchar_t, 64>>(
      mName.get(), DC_PAPERNAMES, mDriverMutex, requiredArrayCount);
  // Check that we have the same number of names as IDs.
  if (paperNames.Length() != paperIds.Length()) {
    return {};
  }

  // Paper sizes are returned as POINT structs with a tenth of a millimeter as
  // the unit.
  auto paperSizes = GetDeviceCapabilityArray<POINT>(
      mName.get(), DC_PAPERSIZE, mDriverMutex, requiredArrayCount);
  // Check that we have the same number of sizes as IDs.
  if (paperSizes.Length() != paperIds.Length()) {
    return {};
  }

  nsTArray<mozilla::PaperInfo> paperList;
  paperList.SetCapacity(paperNames.Length());
  for (size_t i = 0; i < paperNames.Length(); ++i) {
    // Paper names are null terminated unless they are 64 characters long.
    auto firstNull =
        std::find(paperNames[i].cbegin(), paperNames[i].cend(), L'\0');
    auto nameLength = firstNull - paperNames[i].cbegin();
    double width = paperSizes[i].x * kPointsPerTenthMM;
    double height = paperSizes[i].y * kPointsPerTenthMM;

    // Skip if no name or invalid size.
    if (!nameLength || width <= 0 || height <= 0) {
      continue;
    }

    // Windows paper IDs are 16-bit integers; we stringify them to store in the
    // PaperInfo.mId field.
    nsString paperIdString;
    paperIdString.AppendInt(paperIds[i]);

    // We don't resolve the margins eagerly because they're really expensive (on
    // the order of seconds for some drivers).
    nsDependentSubstring name(paperNames[i].cbegin(), nameLength);
    paperList.AppendElement(mozilla::PaperInfo(paperIdString, nsString(name),
                                               {width, height}, Nothing()));
  }

  return paperList;
}

PrintSettingsInitializer nsPrinterWin::DefaultSettings() const {
  nsTArray<uint8_t> devmodeWStorage = CopyDefaultDevmodeW();
  if (devmodeWStorage.IsEmpty()) {
    return {};
  }

  const auto* devmode =
      reinterpret_cast<const DEVMODEW*>(devmodeWStorage.Elements());

  PrintSettingsInitializer settingsInitializer;
  DevmodeToSettingsInitializer(mName, devmode, mDriverMutex,
                               settingsInitializer);
  settingsInitializer.mDevmodeWStorage = std::move(devmodeWStorage);
  return settingsInitializer;
}

PrintSettingsInitializer nsPrinterWin::GetValidatedSettings(
    PrintSettingsInitializer aSettingsToValidate) const {
  // This function validates the settings by relying on the printer driver
  // rejecting any invalid settings and resetting them to valid values.

  // Create a copy of the default DEVMODE for this printer.
  nsTArray<uint8_t> devmodeWStorage = CopyDefaultDevmodeW();
  if (devmodeWStorage.IsEmpty()) {
    return aSettingsToValidate;
  }

  nsHPRINTER hPrinter = nullptr;
  if (NS_WARN_IF(!::OpenPrinterW(mName.get(), &hPrinter, nullptr))) {
    return aSettingsToValidate;
  }
  nsAutoPrinter autoPrinter(hPrinter);

  // Copy the settings from aSettingsToValidate into our DEVMODE.
  DEVMODEW* devmode = reinterpret_cast<DEVMODEW*>(devmodeWStorage.Elements());
  if (!aSettingsToValidate.mPaperInfo.mId.IsEmpty()) {
    devmode->dmPaperSize = _wtoi(
        (const wchar_t*)aSettingsToValidate.mPaperInfo.mId.BeginReading());
    devmode->dmFields |= DM_PAPERSIZE;
  } else {
    devmode->dmPaperSize = 0;
    devmode->dmFields &= ~DM_PAPERSIZE;
  }

  devmode->dmFields |= DM_COLOR;
  devmode->dmColor =
      aSettingsToValidate.mPrintInColor ? DMCOLOR_COLOR : DMCOLOR_MONOCHROME;

  // Note: small page sizes can be required here for sticker, label and slide
  // printers etc. see bug 1271900.
  if (aSettingsToValidate.mPaperInfo.mSize.height > 0) {
    devmode->dmPaperLength = std::round(
        aSettingsToValidate.mPaperInfo.mSize.height / kPointsPerTenthMM);
    devmode->dmFields |= DM_PAPERLENGTH;
  } else {
    devmode->dmPaperLength = 0;
    devmode->dmFields &= ~DM_PAPERLENGTH;
  }

  if (aSettingsToValidate.mPaperInfo.mSize.width > 0) {
    devmode->dmPaperWidth = std::round(
        aSettingsToValidate.mPaperInfo.mSize.width / kPointsPerTenthMM);
    devmode->dmFields |= DM_PAPERWIDTH;
  } else {
    devmode->dmPaperWidth = 0;
    devmode->dmFields &= ~DM_PAPERWIDTH;
  }

  // Setup Orientation
  devmode->dmOrientation = aSettingsToValidate.mSheetOrientation ==
                                   nsPrintSettings::kPortraitOrientation
                               ? DMORIENT_PORTRAIT
                               : DMORIENT_LANDSCAPE;
  devmode->dmFields |= DM_ORIENTATION;

  // Setup Number of Copies
  devmode->dmCopies = aSettingsToValidate.mNumCopies;
  devmode->dmFields |= DM_COPIES;

  // Setup Simplex/Duplex mode
  devmode->dmFields |= DM_DUPLEX;
  switch (aSettingsToValidate.mDuplex) {
    case nsPrintSettings::kDuplexNone:
      devmode->dmDuplex = DMDUP_SIMPLEX;
      break;
    case nsPrintSettings::kDuplexFlipOnLongEdge:
      devmode->dmDuplex = DMDUP_VERTICAL;
      break;
    case nsPrintSettings::kDuplexFlipOnShortEdge:
      devmode->dmDuplex = DMDUP_HORIZONTAL;
      break;
    default:
      MOZ_ASSERT_UNREACHABLE("bad value for duplex option");
      break;
  }

  // Apply the settings in the DEVMODE to the printer and retrieve the updated
  // DEVMODE back into the same structure.
  LONG ret;
  {
    MutexAutoLock autoLock(mDriverMutex);
    ret = ::DocumentPropertiesW(nullptr, autoPrinter.get(), mName.get(),
                                devmode, devmode, DM_IN_BUFFER | DM_OUT_BUFFER);
  }
  if (ret != IDOK) {
    return aSettingsToValidate;
  }

  // Copy the settings back from the DEVMODE into aSettingsToValidate and
  // return.
  DevmodeToSettingsInitializer(mName, devmode, mDriverMutex,
                               aSettingsToValidate);
  aSettingsToValidate.mDevmodeWStorage = std::move(devmodeWStorage);
  return aSettingsToValidate;
}
