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

#include "mozilla/gfx/PrintTargetCG.h"
#ifdef MOZ_ENABLE_SKIA_PDF
#  include "mozilla/gfx/PrintTargetSkPDF.h"
#endif
#include "mozilla/Preferences.h"
#include "mozilla/RefPtr.h"
#include "nsCRT.h"
#include "nsDirectoryServiceDefs.h"
#include "nsILocalFileMac.h"
#include <unistd.h>

#include "nsQueryObject.h"
#include "nsIServiceManager.h"
#include "nsPrintSettingsX.h"

// This must be the last include:
#include "nsObjCExceptions.h"

using namespace mozilla;
using mozilla::gfx::IntSize;
using mozilla::gfx::PrintTarget;
using mozilla::gfx::PrintTargetCG;
#ifdef MOZ_ENABLE_SKIA_PDF
using mozilla::gfx::PrintTargetSkPDF;
#endif
using mozilla::gfx::SurfaceFormat;

nsDeviceContextSpecX::nsDeviceContextSpecX()
    : mPrintSession(NULL),
      mPageFormat(kPMNoPageFormat),
      mPrintSettings(kPMNoPrintSettings)
#ifdef MOZ_ENABLE_SKIA_PDF
      ,
      mPrintViaSkPDF(false)
#endif
{
}

nsDeviceContextSpecX::~nsDeviceContextSpecX() {
  NS_OBJC_BEGIN_TRY_ABORT_BLOCK;

  if (mPrintSession) ::PMRelease(mPrintSession);

  NS_OBJC_END_TRY_ABORT_BLOCK;
}

NS_IMPL_ISUPPORTS(nsDeviceContextSpecX, nsIDeviceContextSpec)

NS_IMETHODIMP nsDeviceContextSpecX::Init(nsIWidget* aWidget, nsIPrintSettings* aPS,
                                         bool aIsPrintPreview) {
  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;

  RefPtr<nsPrintSettingsX> settings(do_QueryObject(aPS));
  if (!settings) return NS_ERROR_NO_INTERFACE;

  bool toFile;
  settings->GetPrintToFile(&toFile);

  bool toPrinter = !toFile && !aIsPrintPreview;
  if (!toPrinter) {
    double width, height;
    settings->GetFilePageSize(&width, &height);
    settings->SetCocoaPaperSize(width, height);
  }

  mPrintSession = settings->GetPMPrintSession();
  ::PMRetain(mPrintSession);
  mPageFormat = settings->GetPMPageFormat();
  mPrintSettings = settings->GetPMPrintSettings();

#ifdef MOZ_ENABLE_SKIA_PDF
  nsAutoString printViaPdf;
  mozilla::Preferences::GetString("print.print_via_pdf_encoder", printViaPdf);
  if (printViaPdf.EqualsLiteral("skia-pdf")) {
    // Annoyingly, PMPrinterPrintWithFile does not pay attention to the
    // kPMDestination* value set in the PMPrintSession; it always sends the PDF
    // to the specified printer.  This means that if we create the PDF using
    // SkPDF then we need to manually handle user actions like "Open PDF in
    // Preview" and "Save as PDF...".
    // TODO: Currently we do not support using SkPDF for kPMDestinationFax or
    // kPMDestinationProcessPDF ("Add PDF to iBooks, etc.), and we only support
    // it for kPMDestinationFile if the destination file is a PDF.
    // XXX Could PMWorkflowSubmitPDFWithSettings/PMPrinterPrintWithProvider help?
    OSStatus status = noErr;
    PMDestinationType destination;
    status = ::PMSessionGetDestinationType(mPrintSession, mPrintSettings, &destination);
    if (status == noErr) {
      if (destination == kPMDestinationPrinter || destination == kPMDestinationPreview) {
        mPrintViaSkPDF = true;
      } else if (destination == kPMDestinationFile) {
        CFURLRef destURL;
        status = ::PMSessionCopyDestinationLocation(mPrintSession, mPrintSettings, &destURL);
        if (status == noErr) {
          CFStringRef destPathRef = CFURLCopyFileSystemPath(destURL, kCFURLPOSIXPathStyle);
          NSString* destPath = (NSString*)destPathRef;
          NSString* destPathExt = [destPath pathExtension];
          if ([destPathExt isEqualToString:@"pdf"]) {
            mPrintViaSkPDF = true;
          }
        }
      }
    }
  }
#endif

  return NS_OK;

  NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
}

NS_IMETHODIMP nsDeviceContextSpecX::BeginDocument(const nsAString& aTitle,
                                                  const nsAString& aPrintToFileName,
                                                  int32_t aStartPage, int32_t aEndPage) {
  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;

  // Print Core of Application Service sent print job with names exceeding
  // 255 bytes. This is a workaround until fix it.
  // (https://openradar.appspot.com/34428043)
  nsAutoString adjustedTitle;
  PrintTarget::AdjustPrintJobNameForIPP(aTitle, adjustedTitle);

  if (!adjustedTitle.IsEmpty()) {
    CFStringRef cfString = ::CFStringCreateWithCharacters(
        NULL, reinterpret_cast<const UniChar*>(adjustedTitle.BeginReading()),
        adjustedTitle.Length());
    if (cfString) {
      ::PMPrintSettingsSetJobName(mPrintSettings, cfString);
      ::CFRelease(cfString);
    }
  }

  return NS_OK;

  NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
}

NS_IMETHODIMP nsDeviceContextSpecX::EndDocument() {
  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;

#ifdef MOZ_ENABLE_SKIA_PDF
  if (mPrintViaSkPDF) {
    OSStatus status = noErr;

    nsCOMPtr<nsILocalFileMac> tmpPDFFile = do_QueryInterface(mTempFile);
    if (!tmpPDFFile) {
      return NS_ERROR_FAILURE;
    }
    CFURLRef pdfURL;
    nsresult rv = tmpPDFFile->GetCFURL(&pdfURL);
    NS_ENSURE_SUCCESS(rv, rv);

    PMDestinationType destination;
    status = ::PMSessionGetDestinationType(mPrintSession, mPrintSettings, &destination);

    switch (destination) {
      case kPMDestinationPrinter: {
        PMPrinter currentPrinter = NULL;
        status = ::PMSessionGetCurrentPrinter(mPrintSession, &currentPrinter);
        if (status != noErr) {
          return NS_ERROR_FAILURE;
        }
        CFStringRef mimeType = CFSTR("application/pdf");
        status =
            ::PMPrinterPrintWithFile(currentPrinter, mPrintSettings, mPageFormat, mimeType, pdfURL);
        break;
      }
      case kPMDestinationPreview: {
        // XXXjwatt Or should we use CocoaFileUtils::RevealFileInFinder(pdfURL);
        CFStringRef pdfPath = CFURLCopyFileSystemPath(pdfURL, kCFURLPOSIXPathStyle);
        NSString* path = (NSString*)pdfPath;
        NSWorkspace* ws = [NSWorkspace sharedWorkspace];
        [ws openFile:path];
        break;
      }
      case kPMDestinationFile: {
        CFURLRef destURL;
        status = ::PMSessionCopyDestinationLocation(mPrintSession, mPrintSettings, &destURL);
        if (status == noErr) {
          CFStringRef sourcePathRef = CFURLCopyFileSystemPath(pdfURL, kCFURLPOSIXPathStyle);
          NSString* sourcePath = (NSString*)sourcePathRef;
#  ifdef DEBUG
          CFStringRef destPathRef = CFURLCopyFileSystemPath(destURL, kCFURLPOSIXPathStyle);
          NSString* destPath = (NSString*)destPathRef;
          NSString* destPathExt = [destPath pathExtension];
          MOZ_ASSERT([destPathExt isEqualToString:@"pdf"],
                     "nsDeviceContextSpecX::Init only allows '.pdf' for now");
          // We could use /usr/sbin/cupsfilter to convert the PDF to PS, but
          // currently we don't.
#  endif
          NSFileManager* fileManager = [NSFileManager defaultManager];
          if ([fileManager fileExistsAtPath:sourcePath]) {
            NSURL* src = static_cast<NSURL*>(pdfURL);
            NSURL* dest = static_cast<NSURL*>(destURL);
            bool ok = [fileManager replaceItemAtURL:dest
                                      withItemAtURL:src
                                     backupItemName:nil
                                            options:NSFileManagerItemReplacementUsingNewMetadataOnly
                                   resultingItemURL:nil
                                              error:nil];
            if (!ok) {
              return NS_ERROR_FAILURE;
            }
          }
        }
        break;
      }
      default:
        MOZ_ASSERT_UNREACHABLE("nsDeviceContextSpecX::Init doesn't set "
                               "mPrintViaSkPDF for other values");
    }

    return (status == noErr) ? NS_OK : NS_ERROR_FAILURE;
  }
#endif

  return NS_OK;

  NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
}

void nsDeviceContextSpecX::GetPaperRect(double* aTop, double* aLeft, double* aBottom,
                                        double* aRight) {
  NS_OBJC_BEGIN_TRY_ABORT_BLOCK;

  PMRect paperRect;
  ::PMGetAdjustedPaperRect(mPageFormat, &paperRect);

  *aTop = paperRect.top;
  *aLeft = paperRect.left;
  *aBottom = paperRect.bottom;
  *aRight = paperRect.right;

  NS_OBJC_END_TRY_ABORT_BLOCK;
}

already_AddRefed<PrintTarget> nsDeviceContextSpecX::MakePrintTarget() {
  double top, left, bottom, right;
  GetPaperRect(&top, &left, &bottom, &right);
  const double width = right - left;
  const double height = bottom - top;
  IntSize size = IntSize::Floor(width, height);

#ifdef MOZ_ENABLE_SKIA_PDF
  if (mPrintViaSkPDF) {
    nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(mTempFile));
    NS_ENSURE_SUCCESS(rv, nullptr);
    nsAutoCString tempPath("tmp-printing.pdf");
    mTempFile->AppendNative(tempPath);
    rv = mTempFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
    NS_ENSURE_SUCCESS(rv, nullptr);
    mTempFile->GetNativePath(tempPath);
    auto stream = MakeUnique<SkFILEWStream>(tempPath.get());
    return PrintTargetSkPDF::CreateOrNull(std::move(stream), size);
  }
#endif

  return PrintTargetCG::CreateOrNull(mPrintSession, mPageFormat, mPrintSettings, size);
}
