/* -*- Mode: C++; tab-width: 2; 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 "nsPrintSettingsX.h"
#include "nsObjCExceptions.h"

#include "plbase64.h"

#include "nsCocoaUtils.h"
#include "nsXULAppAPI.h"

#include "mozilla/Preferences.h"
#include "mozilla/StaticPrefs_print.h"
#include "nsPrinterCUPS.h"

using namespace mozilla;

NS_IMPL_ISUPPORTS_INHERITED(nsPrintSettingsX, nsPrintSettings, nsPrintSettingsX)

nsPrintSettingsX::nsPrintSettingsX() {
  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;

  mDestination = kPMDestinationInvalid;

  NS_OBJC_END_TRY_IGNORE_BLOCK;
}

already_AddRefed<nsIPrintSettings> CreatePlatformPrintSettings(
    const PrintSettingsInitializer& aSettings) {
  RefPtr<nsPrintSettings> settings = new nsPrintSettingsX();
  settings->InitWithInitializer(aSettings);
  settings->SetDefaultFileName();
  return settings.forget();
}

nsPrintSettingsX& nsPrintSettingsX::operator=(const nsPrintSettingsX& rhs) {
  if (this == &rhs) {
    return *this;
  }

  nsPrintSettings::operator=(rhs);

  mDestination = rhs.mDestination;
  mDisposition = rhs.mDisposition;

  // We don't copy mSystemPrintInfo here, so any copied printSettings will start
  // out without a wrapped printInfo, just using our internal settings. The
  // system printInfo is used *only* by the nsPrintSettingsX to which it was
  // originally passed (when the user ran a system print UI dialog).

  return *this;
}

nsresult nsPrintSettingsX::_Clone(nsIPrintSettings** _retval) {
  NS_ENSURE_ARG_POINTER(_retval);
  auto newSettings = MakeRefPtr<nsPrintSettingsX>();
  *newSettings = *this;
  newSettings.forget(_retval);
  return NS_OK;
}

NS_IMETHODIMP nsPrintSettingsX::_Assign(nsIPrintSettings* aPS) {
  nsPrintSettingsX* printSettingsX = static_cast<nsPrintSettingsX*>(aPS);
  if (!printSettingsX) {
    return NS_ERROR_UNEXPECTED;
  }
  *this = *printSettingsX;
  return NS_OK;
}

struct KnownMonochromeSetting {
  const NSString* mName;
  const NSString* mValue;
};

#define DECLARE_KNOWN_MONOCHROME_SETTING(key_, value_) \
  { @key_, @value_ }                                   \
  ,
static const KnownMonochromeSetting kKnownMonochromeSettings[] = {
    CUPS_EACH_MONOCHROME_PRINTER_SETTING(DECLARE_KNOWN_MONOCHROME_SETTING)};
#undef DECLARE_KNOWN_MONOCHROME_SETTING

NSPrintInfo* nsPrintSettingsX::CreateOrCopyPrintInfo(bool aWithScaling) {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;

  // If we have a printInfo that came from the system print UI, use it so that
  // any printer-specific settings we don't know about will still be used.
  if (mSystemPrintInfo) {
    NSPrintInfo* sysPrintInfo = [mSystemPrintInfo copy];
    // Any required scaling will be done by Gecko, so we don't want it here.
    [sysPrintInfo setScalingFactor:1.0f];
    return sysPrintInfo;
  }

  // Note that the app shared `sharedPrintInfo` object is special!  The system
  // print dialog and print settings dialog update it with the values chosen
  // by the user.  Using that object here to initialize new nsPrintSettingsX
  // objects could mask bugs in our code where we fail to save and/or restore
  // print settings ourselves (e.g., bug 1636725).  On other platforms we only
  // initialize new nsPrintSettings objects from the settings that we save to
  // prefs.  Perhaps we should stop using sharedPrintInfo here for consistency?
  NSPrintInfo* printInfo = [[NSPrintInfo sharedPrintInfo] copy];

  NSSize paperSize;
  if (GetSheetOrientation() == kPortraitOrientation) {
    paperSize.width = CocoaPointsFromPaperSize(mPaperWidth);
    paperSize.height = CocoaPointsFromPaperSize(mPaperHeight);
  } else {
    paperSize.width = CocoaPointsFromPaperSize(mPaperHeight);
    paperSize.height = CocoaPointsFromPaperSize(mPaperWidth);
  }
  [printInfo setPaperSize:paperSize];

  if (paperSize.width > paperSize.height) {
    [printInfo setOrientation:NSPaperOrientationLandscape];
  } else {
    [printInfo setOrientation:NSPaperOrientationPortrait];
  }

  [printInfo setTopMargin:mUnwriteableMargin.top];
  [printInfo setRightMargin:mUnwriteableMargin.right];
  [printInfo setBottomMargin:mUnwriteableMargin.bottom];
  [printInfo setLeftMargin:mUnwriteableMargin.left];

  // If the printer name is the name of our pseudo print-to-PDF printer, the
  // following `setPrinter` call will fail silently, since macOS doesn't know
  // anything about it. That's OK, because mPrinter is our canonical source of
  // truth.
  // Actually, it seems Mac OS X 10.12 (the oldest version of Mac that we
  // support) hangs if the printer name is not recognized. For now we explicitly
  // check for our print-to-PDF printer, but that is not ideal since we should
  // really localize the name of this printer at some point. Once we drop
  // support for 10.12 we should remove this check.
  if (!mPrinter.EqualsLiteral("Mozilla Save to PDF")) {
    [printInfo setPrinter:[NSPrinter printerWithName:nsCocoaUtils::ToNSString(
                                                         mPrinter)]];
  }

  // Scaling is handled by gecko, we do NOT want the cocoa printing system to
  // add a second scaling on top of that. So we only set the true scaling factor
  // here if the caller explicitly asked for it.
  [printInfo setScalingFactor:CGFloat(aWithScaling ? mScaling : 1.0f)];

  const bool allPages = mPageRanges.IsEmpty();

  NSMutableDictionary* dict = [printInfo dictionary];
  [dict setObject:[NSNumber numberWithInt:mNumCopies] forKey:NSPrintCopies];
  [dict setObject:[NSNumber numberWithBool:allPages] forKey:NSPrintAllPages];

  int32_t start = 1;
  int32_t end = 1;
  for (size_t i = 0; i < mPageRanges.Length(); i += 2) {
    start = std::min(start, mPageRanges[i]);
    end = std::max(end, mPageRanges[i + 1]);
  }

  [dict setObject:[NSNumber numberWithInt:start] forKey:NSPrintFirstPage];
  [dict setObject:[NSNumber numberWithInt:end] forKey:NSPrintLastPage];

  NSURL* jobSavingURL = nullptr;
  if (!mToFileName.IsEmpty()) {
    jobSavingURL =
        [NSURL fileURLWithPath:nsCocoaUtils::ToNSString(mToFileName)];
    if (jobSavingURL) {
      // Note: the PMPrintSettingsSetJobName call in nsPrintDialogServiceX::Show
      // seems to mean that we get a sensible file name pre-populated in the
      // dialog there, although our mToFileName is expected to be a full path,
      // and it's less clear where the rest of the path (the directory to save
      // to) in nsPrintDialogServiceX::Show comes from (perhaps from the use
      // of `sharedPrintInfo` to initialize new nsPrintSettingsX objects).
      [dict setObject:jobSavingURL forKey:NSPrintJobSavingURL];
    }
  }

  if (mDisposition.IsEmpty()) {
    // NOTE: It's unclear what to do for kOutputDestinationStream but this is
    // only for the native print dialog where that can't happen.
    if (mOutputDestination == kOutputDestinationFile) {
      [printInfo setJobDisposition:NSPrintSaveJob];
    } else {
      [printInfo setJobDisposition:NSPrintSpoolJob];
    }
  } else {
    [printInfo setJobDisposition:nsCocoaUtils::ToNSString(mDisposition)];
  }

  PMDuplexMode duplexSetting;
  switch (mDuplex) {
    default:
      // This can't happen :) but if it does, we treat it as "none".
      MOZ_FALLTHROUGH_ASSERT("Unknown duplex value");
    case kDuplexNone:
      duplexSetting = kPMDuplexNone;
      break;
    case kDuplexFlipOnLongEdge:
      duplexSetting = kPMDuplexNoTumble;
      break;
    case kDuplexFlipOnShortEdge:
      duplexSetting = kPMDuplexTumble;
      break;
  }

  NSMutableDictionary* printSettings = [printInfo printSettings];
  [printSettings setObject:[NSNumber numberWithUnsignedShort:duplexSetting]
                    forKey:@"com_apple_print_PrintSettings_PMDuplexing"];

  if (mDestination != kPMDestinationInvalid) {
    // Required to support PDF-workflow destinations such as Save to Web
    // Receipts.
    [printSettings
        setObject:[NSNumber numberWithUnsignedShort:mDestination]
           forKey:@"com_apple_print_PrintSettings_PMDestinationType"];
    if (jobSavingURL) {
      [printSettings
          setObject:[jobSavingURL absoluteString]
             forKey:@"com_apple_print_PrintSettings_PMOutputFilename"];
    }
  }

  if (StaticPrefs::print_cups_monochrome_enabled() && !GetPrintInColor()) {
    for (const auto& setting : kKnownMonochromeSettings) {
      [printSettings setObject:setting.mValue forKey:setting.mName];
    }
    auto applySetting = [&](const nsACString& aKey, const nsACString& aValue) {
      [printSettings setObject:nsCocoaUtils::ToNSString(aValue)
                        forKey:nsCocoaUtils::ToNSString(aKey)];
    };
    nsPrinterCUPS::ForEachExtraMonochromeSetting(applySetting);
  }

  return printInfo;

  NS_OBJC_END_TRY_BLOCK_RETURN(nullptr);
}

void nsPrintSettingsX::SetFromPrintInfo(NSPrintInfo* aPrintInfo,
                                        bool aAdoptPrintInfo) {
  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;

  // Set page-size/margins.
  NSSize paperSize = [aPrintInfo paperSize];
  const bool areSheetsOfPaperPortraitMode =
      ([aPrintInfo orientation] == NSPaperOrientationPortrait);

  // If our MacOS print settings say that we're producing portrait-mode sheets
  // of paper, then our page format must also be portrait-mode; unless we've
  // got a pages-per-sheet value with orthogonal pages/sheets, in which case
  // it's reversed.
  const bool arePagesPortraitMode =
      (areSheetsOfPaperPortraitMode != HasOrthogonalSheetsAndPages());

  if (arePagesPortraitMode) {
    mOrientation = nsIPrintSettings::kPortraitOrientation;
    SetPaperWidth(PaperSizeFromCocoaPoints(paperSize.width));
    SetPaperHeight(PaperSizeFromCocoaPoints(paperSize.height));
  } else {
    mOrientation = nsIPrintSettings::kLandscapeOrientation;
    SetPaperWidth(PaperSizeFromCocoaPoints(paperSize.height));
    SetPaperHeight(PaperSizeFromCocoaPoints(paperSize.width));
  }

  mUnwriteableMargin.top = static_cast<int32_t>([aPrintInfo topMargin]);
  mUnwriteableMargin.right = static_cast<int32_t>([aPrintInfo rightMargin]);
  mUnwriteableMargin.bottom = static_cast<int32_t>([aPrintInfo bottomMargin]);
  mUnwriteableMargin.left = static_cast<int32_t>([aPrintInfo leftMargin]);

  if (aAdoptPrintInfo) {
    // Keep a reference to the printInfo; it may have settings that we don't
    // know how to handle otherwise.
    if (mSystemPrintInfo != aPrintInfo) {
      if (mSystemPrintInfo) {
        [mSystemPrintInfo release];
      }
      mSystemPrintInfo = aPrintInfo;
      [mSystemPrintInfo retain];
    }
  } else {
    // Clear any stored printInfo.
    if (mSystemPrintInfo) {
      [mSystemPrintInfo release];
      mSystemPrintInfo = nullptr;
    }
  }

  nsCocoaUtils::GetStringForNSString([[aPrintInfo printer] name], mPrinter);

  // Only get the scaling value if shrink-to-fit is not selected:
  bool isShrinkToFitChecked;
  GetShrinkToFit(&isShrinkToFitChecked);
  if (!isShrinkToFitChecked) {
    // Limit scaling precision to whole percentage values.
    mScaling = round(double([aPrintInfo scalingFactor]) * 100.0) / 100.0;
  }

  mOutputDestination = [&] {
    if ([aPrintInfo jobDisposition] == NSPrintSaveJob) {
      return kOutputDestinationFile;
    }
    return kOutputDestinationPrinter;
  }();

  NSDictionary* dict = [aPrintInfo dictionary];
  const char* filePath =
      [[dict objectForKey:NSPrintJobSavingURL] fileSystemRepresentation];
  if (filePath && *filePath) {
    CopyUTF8toUTF16(Span(filePath, strlen(filePath)), mToFileName);
  }

  nsCocoaUtils::GetStringForNSString([aPrintInfo jobDisposition], mDisposition);

  mNumCopies = [[dict objectForKey:NSPrintCopies] intValue];
  mPageRanges.Clear();
  if (![[dict objectForKey:NSPrintAllPages] boolValue]) {
    mPageRanges.AppendElement([[dict objectForKey:NSPrintFirstPage] intValue]);
    mPageRanges.AppendElement([[dict objectForKey:NSPrintLastPage] intValue]);
  }

  NSDictionary* printSettings = [aPrintInfo printSettings];
  NSNumber* value =
      [printSettings objectForKey:@"com_apple_print_PrintSettings_PMDuplexing"];
  if (value) {
    PMDuplexMode duplexSetting = [value unsignedShortValue];
    switch (duplexSetting) {
      default:
        // An unknown value is treated as None.
        MOZ_FALLTHROUGH_ASSERT("Unknown duplex value");
      case kPMDuplexNone:
        mDuplex = kDuplexNone;
        break;
      case kPMDuplexNoTumble:
        mDuplex = kDuplexFlipOnLongEdge;
        break;
      case kPMDuplexTumble:
        mDuplex = kDuplexFlipOnShortEdge;
        break;
    }
  } else {
    // By default a printSettings dictionary doesn't initially contain the
    // duplex key at all, so this is not an error; its absence just means no
    // duplexing has been requested, so we return kDuplexNone.
    mDuplex = kDuplexNone;
  }

  value = [printSettings
      objectForKey:@"com_apple_print_PrintSettings_PMDestinationType"];
  if (value) {
    mDestination = [value unsignedShortValue];
  }

  const bool color = [&] {
    if (StaticPrefs::print_cups_monochrome_enabled()) {
      for (const auto& setting : kKnownMonochromeSettings) {
        NSString* value = [printSettings objectForKey:setting.mName];
        if (!value) {
          continue;
        }
        if ([setting.mValue isEqualToString:value]) {
          return false;
        }
      }
    }
    return true;
  }();

  SetPrintInColor(color);

  SetIsInitializedFromPrinter(true);

  NS_OBJC_END_TRY_IGNORE_BLOCK;
}
