/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set sw=2 ts=2 expandtab: */
/* 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 "nsDeviceContext.h"
#include <algorithm>  // for max
#include "gfxContext.h"
#include "gfxImageSurface.h"  // for gfxImageSurface
#include "gfxPoint.h"         // for gfxSize
#include "gfxTextRun.h"       // for gfxFontGroup
#include "mozilla/LookAndFeel.h"
#include "mozilla/gfx/PathHelpers.h"
#include "mozilla/gfx/PrintTarget.h"
#include "mozilla/Preferences.h"  // for Preferences
#include "mozilla/Services.h"     // for GetObserverService
#include "mozilla/StaticPrefs_layout.h"
#include "mozilla/mozalloc.h"       // for operator new
#include "mozilla/widget/Screen.h"  // for Screen
#include "nsCRT.h"                  // for nsCRT
#include "nsDebug.h"                // for NS_ASSERTION, etc
#include "nsFont.h"                 // for nsFont
#include "nsFontCache.h"            // for nsFontCache
#include "nsFontMetrics.h"          // for nsFontMetrics
#include "nsAtom.h"                 // for nsAtom, NS_Atomize
#include "nsID.h"
#include "nsIDeviceContextSpec.h"   // for nsIDeviceContextSpec
#include "nsLanguageAtomService.h"  // for nsLanguageAtomService
#include "nsIObserver.h"            // for nsIObserver, etc
#include "nsIObserverService.h"     // for nsIObserverService
#include "nsISupportsImpl.h"        // for MOZ_COUNT_CTOR, etc
#include "nsISupportsUtils.h"       // for NS_ADDREF, NS_RELEASE
#include "nsIWidget.h"              // for nsIWidget, NS_NATIVE_WINDOW
#include "nsRect.h"                 // for nsRect
#include "nsServiceManagerUtils.h"  // for do_GetService
#include "nsString.h"               // for nsDependentString
#include "nsTArray.h"               // for nsTArray, nsTArray_Impl
#include "nsThreadUtils.h"          // for NS_IsMainThread
#include "mozilla/gfx/Logging.h"
#include "mozilla/widget/ScreenManager.h"  // for ScreenManager

using namespace mozilla;
using namespace mozilla::gfx;
using mozilla::widget::ScreenManager;

nsDeviceContext::nsDeviceContext()
    : mWidth(0),
      mHeight(0),
      mAppUnitsPerDevPixel(-1),
      mAppUnitsPerDevPixelAtUnitFullZoom(-1),
      mAppUnitsPerPhysicalInch(-1),
      mFullZoom(1.0f),
      mPrintingScale(1.0f),
      mPrintingTranslate(gfxPoint(0, 0)),
      mIsCurrentlyPrintingDoc(false) {
  MOZ_ASSERT(NS_IsMainThread(), "nsDeviceContext created off main thread");
}

nsDeviceContext::~nsDeviceContext() = default;

void nsDeviceContext::SetDPI() {
  float dpi;

  // Use the printing DC to determine DPI values, if we have one.
  if (mDeviceContextSpec) {
    dpi = mDeviceContextSpec->GetDPI();
    mPrintingScale = mDeviceContextSpec->GetPrintingScale();
    mPrintingTranslate = mDeviceContextSpec->GetPrintingTranslate();
    mAppUnitsPerDevPixelAtUnitFullZoom =
        NS_lround((AppUnitsPerCSSPixel() * 96) / dpi);
  } else {
    // A value of -1 means use the maximum of 96 and the system DPI.
    // A value of 0 means use the system DPI. A positive value is used as the
    // DPI. This sets the physical size of a device pixel and thus controls the
    // interpretation of physical units.
    int32_t prefDPI = StaticPrefs::layout_css_dpi();
    if (prefDPI > 0) {
      dpi = prefDPI;
    } else if (mWidget) {
      dpi = mWidget->GetDPI();
      MOZ_ASSERT(dpi > 0);
      if (prefDPI < 0) {
        dpi = std::max(96.0f, dpi);
      }
    } else {
      dpi = 96.0f;
    }

    CSSToLayoutDeviceScale scale =
        mWidget ? mWidget->GetDefaultScale() : CSSToLayoutDeviceScale(1.0);
    MOZ_ASSERT(scale.scale > 0.0);
    mAppUnitsPerDevPixelAtUnitFullZoom =
        std::max(1, NS_lround(AppUnitsPerCSSPixel() / scale.scale));
  }

  NS_ASSERTION(dpi != -1.0, "no dpi set");

  mAppUnitsPerPhysicalInch =
      NS_lround(dpi * mAppUnitsPerDevPixelAtUnitFullZoom);
  UpdateAppUnitsForFullZoom();
}

void nsDeviceContext::Init(nsIWidget* aWidget) {
  if (mIsInitialized && mWidget == aWidget) {
    return;
  }

  // We can't assert |!mIsInitialized| here since EndSwapDocShellsForDocument
  // re-initializes nsDeviceContext objects.  We can only assert in
  // InitForPrinting (below).
  mIsInitialized = true;

  mWidget = aWidget;
  SetDPI();
}

// XXX This is only for printing. We should make that obvious in the name.
UniquePtr<gfxContext> nsDeviceContext::CreateRenderingContext() {
  return CreateRenderingContextCommon(/* not a reference context */ false);
}

UniquePtr<gfxContext> nsDeviceContext::CreateReferenceRenderingContext() {
  return CreateRenderingContextCommon(/* a reference context */ true);
}

UniquePtr<gfxContext> nsDeviceContext::CreateRenderingContextCommon(
    bool aWantReferenceContext) {
  MOZ_ASSERT(IsPrinterContext());
  MOZ_ASSERT(mWidth > 0 && mHeight > 0);

  if (NS_WARN_IF(!mPrintTarget)) {
    // Printing canceled already.
    return nullptr;
  }

  RefPtr<gfx::DrawTarget> dt;
  if (aWantReferenceContext) {
    dt = mPrintTarget->GetReferenceDrawTarget();
  } else {
    // This will be null if printing a page from the parent process.
    RefPtr<DrawEventRecorder> recorder;
    mDeviceContextSpec->GetDrawEventRecorder(getter_AddRefs(recorder));
    dt = mPrintTarget->MakeDrawTarget(gfx::IntSize(mWidth, mHeight), recorder);
  }

  if (!dt || !dt->IsValid()) {
    gfxCriticalNote << "Failed to create draw target in device context sized "
                    << mWidth << "x" << mHeight << " and pointer "
                    << hexa(mPrintTarget);
    return nullptr;
  }

  dt->AddUserData(&sDisablePixelSnapping, (void*)0x1, nullptr);

  auto pContext = MakeUnique<gfxContext>(dt);

  gfxMatrix transform;
  transform.PreTranslate(mPrintingTranslate);
  transform.PreScale(mPrintingScale, mPrintingScale);
  pContext->SetMatrixDouble(transform);
  return pContext;
}

uint32_t nsDeviceContext::GetDepth() {
  RefPtr<widget::Screen> screen = FindScreen();
  if (!screen) {
    ScreenManager& screenManager = ScreenManager::GetSingleton();
    screen = screenManager.GetPrimaryScreen();
    MOZ_ASSERT(screen);
  }
  int32_t depth = 0;
  screen->GetColorDepth(&depth);
  return uint32_t(depth);
}

dom::ScreenColorGamut nsDeviceContext::GetColorGamut() {
  RefPtr<widget::Screen> screen = FindScreen();
  if (!screen) {
    auto& screenManager = ScreenManager::GetSingleton();
    screen = screenManager.GetPrimaryScreen();
    MOZ_ASSERT(screen);
  }
  dom::ScreenColorGamut colorGamut;
  screen->GetColorGamut(&colorGamut);
  return colorGamut;
}

hal::ScreenOrientation nsDeviceContext::GetScreenOrientationType() {
  RefPtr<widget::Screen> screen = FindScreen();
  if (!screen) {
    auto& screenManager = ScreenManager::GetSingleton();
    screen = screenManager.GetPrimaryScreen();
    MOZ_ASSERT(screen);
  }
  return screen->GetOrientationType();
}

uint16_t nsDeviceContext::GetScreenOrientationAngle() {
  RefPtr<widget::Screen> screen = FindScreen();
  if (!screen) {
    auto& screenManager = ScreenManager::GetSingleton();
    screen = screenManager.GetPrimaryScreen();
    MOZ_ASSERT(screen);
  }
  return screen->GetOrientationAngle();
}

nsresult nsDeviceContext::GetDeviceSurfaceDimensions(nscoord& aWidth,
                                                     nscoord& aHeight) {
  if (IsPrinterContext()) {
    aWidth = mWidth;
    aHeight = mHeight;
  } else {
    nsRect area;
    ComputeFullAreaUsingScreen(&area);
    aWidth = area.Width();
    aHeight = area.Height();
  }

  return NS_OK;
}

nsresult nsDeviceContext::GetRect(nsRect& aRect) {
  if (IsPrinterContext()) {
    aRect.SetRect(0, 0, mWidth, mHeight);
  } else
    ComputeFullAreaUsingScreen(&aRect);

  return NS_OK;
}

nsresult nsDeviceContext::GetClientRect(nsRect& aRect) {
  if (IsPrinterContext()) {
    aRect.SetRect(0, 0, mWidth, mHeight);
  } else
    ComputeClientRectUsingScreen(&aRect);

  return NS_OK;
}

nsresult nsDeviceContext::InitForPrinting(nsIDeviceContextSpec* aDevice) {
  NS_ENSURE_ARG_POINTER(aDevice);

  MOZ_ASSERT(!mIsInitialized,
             "Only initialize once, immediately after construction");

  // We don't set mIsInitialized here. The Init() call below does that.

  mPrintTarget = aDevice->MakePrintTarget();
  if (!mPrintTarget) {
    return NS_ERROR_FAILURE;
  }

  mDeviceContextSpec = aDevice;

  Init(nullptr);

  if (!CalcPrintingSize()) {
    return NS_ERROR_FAILURE;
  }

  return NS_OK;
}

nsresult nsDeviceContext::BeginDocument(const nsAString& aTitle,
                                        const nsAString& aPrintToFileName,
                                        int32_t aStartPage, int32_t aEndPage) {
  MOZ_DIAGNOSTIC_ASSERT(!mIsCurrentlyPrintingDoc,
                        "Mismatched BeginDocument/EndDocument calls");

  nsresult rv = mPrintTarget->BeginPrinting(aTitle, aPrintToFileName,
                                            aStartPage, aEndPage);

  if (NS_SUCCEEDED(rv)) {
    if (mDeviceContextSpec) {
      rv = mDeviceContextSpec->BeginDocument(aTitle, aPrintToFileName,
                                             aStartPage, aEndPage);
    }
    mIsCurrentlyPrintingDoc = true;
  }

  // Warn about any failure (except user cancelling):
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv) || rv == NS_ERROR_ABORT,
                       "nsDeviceContext::BeginDocument failed");

  return rv;
}

RefPtr<PrintEndDocumentPromise> nsDeviceContext::EndDocument() {
  MOZ_DIAGNOSTIC_ASSERT(mIsCurrentlyPrintingDoc,
                        "Mismatched BeginDocument/EndDocument calls");
  MOZ_DIAGNOSTIC_ASSERT(mPrintTarget);

  mIsCurrentlyPrintingDoc = false;

  if (mPrintTarget) {
    auto result = mPrintTarget->EndPrinting();
    if (NS_FAILED(result)) {
      return PrintEndDocumentPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE,
                                                      __func__);
    }
    mPrintTarget->Finish();
    mPrintTarget = nullptr;
  }

  if (mDeviceContextSpec) {
    return mDeviceContextSpec->EndDocument();
  }

  return PrintEndDocumentPromise::CreateAndResolve(true, __func__);
}

nsresult nsDeviceContext::AbortDocument() {
  MOZ_DIAGNOSTIC_ASSERT(mIsCurrentlyPrintingDoc,
                        "Mismatched BeginDocument/EndDocument calls");

  nsresult rv = mPrintTarget->AbortPrinting();
  mIsCurrentlyPrintingDoc = false;

  if (mDeviceContextSpec) {
    Unused << mDeviceContextSpec->EndDocument();
  }

  mPrintTarget = nullptr;

  return rv;
}

nsresult nsDeviceContext::BeginPage() {
  MOZ_DIAGNOSTIC_ASSERT(!mIsCurrentlyPrintingDoc || mPrintTarget,
                        "What nulled out our print target while printing?");
  if (mDeviceContextSpec) {
    MOZ_TRY(mDeviceContextSpec->BeginPage());
  }
  if (mPrintTarget) {
    MOZ_TRY(mPrintTarget->BeginPage());
  }
  return NS_OK;
}

nsresult nsDeviceContext::EndPage() {
  MOZ_DIAGNOSTIC_ASSERT(!mIsCurrentlyPrintingDoc || mPrintTarget,
                        "What nulled out our print target while printing?");
  if (mPrintTarget) {
    MOZ_TRY(mPrintTarget->EndPage());
  }
  if (mDeviceContextSpec) {
    MOZ_TRY(mDeviceContextSpec->EndPage());
  }
  return NS_OK;
}

void nsDeviceContext::ComputeClientRectUsingScreen(nsRect* outRect) {
  // we always need to recompute the clientRect
  // because the window may have moved onto a different screen. In the single
  // monitor case, we only need to do the computation if we haven't done it
  // once already, and remember that we have because we're assured it won't
  // change.
  if (RefPtr<widget::Screen> screen = FindScreen()) {
    *outRect = LayoutDeviceIntRect::ToAppUnits(screen->GetAvailRect(),
                                               AppUnitsPerDevPixel());
  }
}

void nsDeviceContext::ComputeFullAreaUsingScreen(nsRect* outRect) {
  // if we have more than one screen, we always need to recompute the clientRect
  // because the window may have moved onto a different screen. In the single
  // monitor case, we only need to do the computation if we haven't done it
  // once already, and remember that we have because we're assured it won't
  // change.
  if (RefPtr<widget::Screen> screen = FindScreen()) {
    *outRect = LayoutDeviceIntRect::ToAppUnits(screen->GetRect(),
                                               AppUnitsPerDevPixel());
    mWidth = outRect->Width();
    mHeight = outRect->Height();
  }
}

//
// FindScreen
//
// Determines which screen intersects the largest area of the given surface.
//
already_AddRefed<widget::Screen> nsDeviceContext::FindScreen() {
  if (!mWidget) {
    return nullptr;
  }

  CheckDPIChange();

  if (RefPtr<widget::Screen> screen = mWidget->GetWidgetScreen()) {
    return screen.forget();
  }

  ScreenManager& screenManager = ScreenManager::GetSingleton();
  return screenManager.GetPrimaryScreen();
}

bool nsDeviceContext::CalcPrintingSize() {
  gfxSize size(mPrintTarget->GetSize());
  // For printing, CSS inches and physical inches are identical
  // so it doesn't matter which we use here
  mWidth = NSToCoordRound(size.width * AppUnitsPerPhysicalInch() /
                          POINTS_PER_INCH_FLOAT);
  mHeight = NSToCoordRound(size.height * AppUnitsPerPhysicalInch() /
                           POINTS_PER_INCH_FLOAT);

  return (mWidth > 0 && mHeight > 0);
}

bool nsDeviceContext::CheckDPIChange() {
  int32_t oldDevPixels = mAppUnitsPerDevPixelAtUnitFullZoom;
  int32_t oldInches = mAppUnitsPerPhysicalInch;

  SetDPI();

  return oldDevPixels != mAppUnitsPerDevPixelAtUnitFullZoom ||
         oldInches != mAppUnitsPerPhysicalInch;
}

bool nsDeviceContext::SetFullZoom(float aScale) {
  if (aScale <= 0) {
    MOZ_ASSERT_UNREACHABLE("Invalid full zoom value");
    return false;
  }
  int32_t oldAppUnitsPerDevPixel = mAppUnitsPerDevPixel;
  mFullZoom = aScale;
  UpdateAppUnitsForFullZoom();
  return oldAppUnitsPerDevPixel != mAppUnitsPerDevPixel;
}

static int32_t ApplyFullZoom(int32_t aUnzoomedAppUnits, float aFullZoom) {
  if (aFullZoom == 1.0f) {
    return aUnzoomedAppUnits;
  }
  return std::max(1, NSToIntRound(float(aUnzoomedAppUnits) / aFullZoom));
}

int32_t nsDeviceContext::AppUnitsPerDevPixelInTopLevelChromePage() const {
  // The only zoom that applies to chrome pages is the system zoom, if any.
  return ApplyFullZoom(mAppUnitsPerDevPixelAtUnitFullZoom,
                       LookAndFeel::SystemZoomSettings().mFullZoom);
}

void nsDeviceContext::UpdateAppUnitsForFullZoom() {
  mAppUnitsPerDevPixel =
      ApplyFullZoom(mAppUnitsPerDevPixelAtUnitFullZoom, mFullZoom);
  // adjust mFullZoom to reflect appunit rounding
  mFullZoom = float(mAppUnitsPerDevPixelAtUnitFullZoom) / mAppUnitsPerDevPixel;
}

DesktopToLayoutDeviceScale nsDeviceContext::GetDesktopToDeviceScale() {
  if (RefPtr<widget::Screen> screen = FindScreen()) {
    return screen->GetDesktopToLayoutDeviceScale();
  }
  return DesktopToLayoutDeviceScale(1.0);
}
