/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 "AccessibleCaret.h"

#include "AccessibleCaretLogger.h"
#include "mozilla/FloatingPoint.h"
#include "mozilla/Preferences.h"
#include "mozilla/ToString.h"
#include "nsCanvasFrame.h"
#include "nsCaret.h"
#include "nsDOMTokenList.h"
#include "nsIFrame.h"

namespace mozilla {
using namespace dom;

#undef AC_LOG
#define AC_LOG(message, ...)                                                   \
  AC_LOG_BASE("AccessibleCaret (%p): " message, this, ##__VA_ARGS__);

#undef AC_LOGV
#define AC_LOGV(message, ...)                                                  \
  AC_LOGV_BASE("AccessibleCaret (%p): " message, this, ##__VA_ARGS__);

NS_IMPL_ISUPPORTS(AccessibleCaret::DummyTouchListener, nsIDOMEventListener)

float AccessibleCaret::sWidth = 0.0f;
float AccessibleCaret::sHeight = 0.0f;
float AccessibleCaret::sMarginLeft = 0.0f;
float AccessibleCaret::sBarWidth = 0.0f;

NS_NAMED_LITERAL_STRING(AccessibleCaret::sTextOverlayElementId, "text-overlay");
NS_NAMED_LITERAL_STRING(AccessibleCaret::sCaretImageElementId, "image");
NS_NAMED_LITERAL_STRING(AccessibleCaret::sSelectionBarElementId, "bar");

#define AC_PROCESS_ENUM_TO_STREAM(e) case(e): aStream << #e; break;
std::ostream&
operator<<(std::ostream& aStream, const AccessibleCaret::Appearance& aAppearance)
{
  using Appearance = AccessibleCaret::Appearance;
  switch (aAppearance) {
    AC_PROCESS_ENUM_TO_STREAM(Appearance::None);
    AC_PROCESS_ENUM_TO_STREAM(Appearance::Normal);
    AC_PROCESS_ENUM_TO_STREAM(Appearance::NormalNotShown);
    AC_PROCESS_ENUM_TO_STREAM(Appearance::Left);
    AC_PROCESS_ENUM_TO_STREAM(Appearance::Right);
  }
  return aStream;
}

std::ostream&
operator<<(std::ostream& aStream,
           const AccessibleCaret::PositionChangedResult& aResult)
{
  using PositionChangedResult = AccessibleCaret::PositionChangedResult;
  switch (aResult) {
    AC_PROCESS_ENUM_TO_STREAM(PositionChangedResult::NotChanged);
    AC_PROCESS_ENUM_TO_STREAM(PositionChangedResult::Changed);
    AC_PROCESS_ENUM_TO_STREAM(PositionChangedResult::Invisible);
  }
  return aStream;
}
#undef AC_PROCESS_ENUM_TO_STREAM

// -----------------------------------------------------------------------------
// Implementation of AccessibleCaret methods

AccessibleCaret::AccessibleCaret(nsIPresShell* aPresShell)
  : mPresShell(aPresShell)
{
  // Check all resources required.
  if (mPresShell) {
    MOZ_ASSERT(RootFrame());
    MOZ_ASSERT(mPresShell->GetDocument());
    MOZ_ASSERT(mPresShell->GetCanvasFrame());
    MOZ_ASSERT(mPresShell->GetCanvasFrame()->GetCustomContentContainer());

    InjectCaretElement(mPresShell->GetDocument());
  }

  static bool prefsAdded = false;
  if (!prefsAdded) {
    Preferences::AddFloatVarCache(&sWidth, "layout.accessiblecaret.width");
    Preferences::AddFloatVarCache(&sHeight, "layout.accessiblecaret.height");
    Preferences::AddFloatVarCache(&sMarginLeft, "layout.accessiblecaret.margin-left");
    Preferences::AddFloatVarCache(&sBarWidth, "layout.accessiblecaret.bar.width");
    prefsAdded = true;
  }
}

AccessibleCaret::~AccessibleCaret()
{
  if (mPresShell) {
    RemoveCaretElement(mPresShell->GetDocument());
  }
}

void
AccessibleCaret::SetAppearance(Appearance aAppearance)
{
  if (mAppearance == aAppearance) {
    return;
  }

  ErrorResult rv;
  CaretElement()->ClassList()->Remove(AppearanceString(mAppearance), rv);
  MOZ_ASSERT(!rv.Failed(), "Remove old appearance failed!");

  CaretElement()->ClassList()->Add(AppearanceString(aAppearance), rv);
  MOZ_ASSERT(!rv.Failed(), "Add new appearance failed!");

  AC_LOG("%s: %s -> %s", __FUNCTION__, ToString(mAppearance).c_str(),
         ToString(aAppearance).c_str());

  mAppearance = aAppearance;

  // Need to reset rect since the cached rect will be compared in SetPosition.
  if (mAppearance == Appearance::None) {
    mImaginaryCaretRect = nsRect();
    mZoomLevel = 0.0f;
  }
}

void
AccessibleCaret::SetSelectionBarEnabled(bool aEnabled)
{
  if (mSelectionBarEnabled == aEnabled) {
    return;
  }

  AC_LOG("Set selection bar %s", aEnabled ? "Enabled" : "Disabled");

  ErrorResult rv;
  CaretElement()->ClassList()->Toggle(NS_LITERAL_STRING("no-bar"),
                                      Optional<bool>(!aEnabled), rv);
  MOZ_ASSERT(!rv.Failed());

  mSelectionBarEnabled = aEnabled;
}

/* static */ nsAutoString
AccessibleCaret::AppearanceString(Appearance aAppearance)
{
  nsAutoString string;
  switch (aAppearance) {
  case Appearance::None:
  case Appearance::NormalNotShown:
    string = NS_LITERAL_STRING("none");
    break;
  case Appearance::Normal:
    string = NS_LITERAL_STRING("normal");
    break;
  case Appearance::Right:
    string = NS_LITERAL_STRING("right");
    break;
  case Appearance::Left:
    string = NS_LITERAL_STRING("left");
    break;
  }
  return string;
}

bool
AccessibleCaret::Intersects(const AccessibleCaret& aCaret) const
{
  MOZ_ASSERT(mPresShell == aCaret.mPresShell);

  if (!IsVisuallyVisible() || !aCaret.IsVisuallyVisible()) {
    return false;
  }

  nsRect rect = nsLayoutUtils::GetRectRelativeToFrame(CaretElement(), RootFrame());
  nsRect rhsRect = nsLayoutUtils::GetRectRelativeToFrame(aCaret.CaretElement(), RootFrame());
  return rect.Intersects(rhsRect);
}

bool
AccessibleCaret::Contains(const nsPoint& aPoint, TouchArea aTouchArea) const
{
  if (!IsVisuallyVisible()) {
    return false;
  }

  nsRect textOverlayRect =
    nsLayoutUtils::GetRectRelativeToFrame(TextOverlayElement(), RootFrame());
  nsRect caretImageRect =
    nsLayoutUtils::GetRectRelativeToFrame(CaretImageElement(), RootFrame());

  if (aTouchArea == TouchArea::CaretImage) {
    return caretImageRect.Contains(aPoint);
  }

  MOZ_ASSERT(aTouchArea == TouchArea::Full, "Unexpected TouchArea type!");
  return textOverlayRect.Contains(aPoint) || caretImageRect.Contains(aPoint);
}

void
AccessibleCaret::EnsureApzAware()
{
  // If the caret element was cloned, the listener might have been lost. So
  // if that's the case we register a dummy listener if there isn't one on
  // the element already.
  if (!CaretElement()->IsApzAware()) {
    CaretElement()->AddEventListener(NS_LITERAL_STRING("touchstart"),
                                     mDummyTouchListener, false);
  }
}

void
AccessibleCaret::InjectCaretElement(nsIDocument* aDocument)
{
  ErrorResult rv;
  nsCOMPtr<Element> element = CreateCaretElement(aDocument);
  mCaretElementHolder = aDocument->InsertAnonymousContent(*element, rv);

  MOZ_ASSERT(!rv.Failed(), "Insert anonymous content should not fail!");
  MOZ_ASSERT(mCaretElementHolder.get(), "We must have anonymous content!");

  // InsertAnonymousContent will clone the element to make an AnonymousContent.
  // Since event listeners are not being cloned when cloning a node, we need to
  // add the listener here.
  EnsureApzAware();
}

already_AddRefed<Element>
AccessibleCaret::CreateCaretElement(nsIDocument* aDocument) const
{
  // Content structure of AccessibleCaret
  // <div class="moz-accessiblecaret">  <- CaretElement()
  //   <div id="text-overlay"           <- TextOverlayElement()
  //   <div id="image">                 <- CaretImageElement()
  //   <div id="bar">                   <- SelectionBarElement()

  ErrorResult rv;
  nsCOMPtr<Element> parent = aDocument->CreateHTMLElement(nsGkAtoms::div);
  parent->ClassList()->Add(NS_LITERAL_STRING("moz-accessiblecaret"), rv);
  parent->ClassList()->Add(NS_LITERAL_STRING("none"), rv);
  parent->ClassList()->Add(NS_LITERAL_STRING("no-bar"), rv);

  auto CreateAndAppendChildElement = [aDocument, &parent](
    const nsLiteralString& aElementId)
  {
    nsCOMPtr<Element> child = aDocument->CreateHTMLElement(nsGkAtoms::div);
    child->SetAttr(kNameSpaceID_None, nsGkAtoms::id, aElementId, true);
    parent->AppendChildTo(child, false);
  };

  CreateAndAppendChildElement(sTextOverlayElementId);
  CreateAndAppendChildElement(sCaretImageElementId);
  CreateAndAppendChildElement(sSelectionBarElementId);

  return parent.forget();
}

void
AccessibleCaret::RemoveCaretElement(nsIDocument* aDocument)
{
  CaretElement()->RemoveEventListener(NS_LITERAL_STRING("touchstart"),
                                      mDummyTouchListener, false);

  ErrorResult rv;
  aDocument->RemoveAnonymousContent(*mCaretElementHolder, rv);
  // It's OK rv is failed since nsCanvasFrame might not exists now.
  rv.SuppressException();
}

AccessibleCaret::PositionChangedResult
AccessibleCaret::SetPosition(nsIFrame* aFrame, int32_t aOffset)
{
  if (!CustomContentContainerFrame()) {
    return PositionChangedResult::NotChanged;
  }

  nsRect imaginaryCaretRectInFrame =
    nsCaret::GetGeometryForFrame(aFrame, aOffset, nullptr);

  imaginaryCaretRectInFrame =
    nsLayoutUtils::ClampRectToScrollFrames(aFrame, imaginaryCaretRectInFrame);

  if (imaginaryCaretRectInFrame.IsEmpty()) {
    // Don't bother to set the caret position since it's invisible.
    mImaginaryCaretRect = nsRect();
    mZoomLevel = 0.0f;
    return PositionChangedResult::Invisible;
  }

  nsRect imaginaryCaretRect = imaginaryCaretRectInFrame;
  nsLayoutUtils::TransformRect(aFrame, RootFrame(), imaginaryCaretRect);
  float zoomLevel = GetZoomLevel();

  if (imaginaryCaretRect.IsEqualEdges(mImaginaryCaretRect) &&
      FuzzyEqualsMultiplicative(zoomLevel, mZoomLevel)) {
    return PositionChangedResult::NotChanged;
  }

  mImaginaryCaretRect = imaginaryCaretRect;
  mZoomLevel = zoomLevel;

  // SetCaretElementStyle() requires the input rect relative to container frame.
  nsRect imaginaryCaretRectInContainerFrame = imaginaryCaretRectInFrame;
  nsLayoutUtils::TransformRect(aFrame, CustomContentContainerFrame(),
                               imaginaryCaretRectInContainerFrame);
  SetCaretElementStyle(imaginaryCaretRectInContainerFrame, mZoomLevel);

  return PositionChangedResult::Changed;
}

nsIFrame*
AccessibleCaret::CustomContentContainerFrame() const
{
  nsCanvasFrame* canvasFrame = mPresShell->GetCanvasFrame();
  Element* container = canvasFrame->GetCustomContentContainer();
  nsIFrame* containerFrame = container->GetPrimaryFrame();
  return containerFrame;
}

void
AccessibleCaret::SetCaretElementStyle(const nsRect& aRect, float aZoomLevel)
{
  nsPoint position = CaretElementPosition(aRect);
  nsAutoString styleStr;
  styleStr.AppendPrintf("left: %dpx; top: %dpx; "
                        "width: %.2fpx; height: %.2fpx; margin-left: %.2fpx",
                        nsPresContext::AppUnitsToIntCSSPixels(position.x),
                        nsPresContext::AppUnitsToIntCSSPixels(position.y),
                        sWidth / aZoomLevel,
                        sHeight / aZoomLevel,
                        sMarginLeft / aZoomLevel);

  CaretElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::style, styleStr, true);
  AC_LOG("%s: %s", __FUNCTION__, NS_ConvertUTF16toUTF8(styleStr).get());

  // Set style string for children.
  SetTextOverlayElementStyle(aRect, aZoomLevel);
  SetCaretImageElementStyle(aRect, aZoomLevel);
  SetSelectionBarElementStyle(aRect, aZoomLevel);
}

void
AccessibleCaret::SetTextOverlayElementStyle(const nsRect& aRect,
                                            float aZoomLevel)
{
  nsAutoString styleStr;
  styleStr.AppendPrintf("height: %dpx;",
                        nsPresContext::AppUnitsToIntCSSPixels(aRect.height));
  TextOverlayElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::style, styleStr,
                                true);
  AC_LOG("%s: %s", __FUNCTION__, NS_ConvertUTF16toUTF8(styleStr).get());
}

void
AccessibleCaret::SetCaretImageElementStyle(const nsRect& aRect,
                                           float aZoomLevel)
{
  nsAutoString styleStr;
  styleStr.AppendPrintf("margin-top: %dpx;",
                        nsPresContext::AppUnitsToIntCSSPixels(aRect.height));
  CaretImageElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::style, styleStr,
                               true);
  AC_LOG("%s: %s", __FUNCTION__, NS_ConvertUTF16toUTF8(styleStr).get());
}

void
AccessibleCaret::SetSelectionBarElementStyle(const nsRect& aRect,
                                             float aZoomLevel)
{
  nsAutoString styleStr;
  styleStr.AppendPrintf("height: %dpx; width: %.2fpx;",
                        nsPresContext::AppUnitsToIntCSSPixels(aRect.height),
                        sBarWidth / aZoomLevel);
  SelectionBarElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::style, styleStr,
                                 true);
  AC_LOG("%s: %s", __FUNCTION__, NS_ConvertUTF16toUTF8(styleStr).get());
}

float
AccessibleCaret::GetZoomLevel()
{
  // Full zoom on desktop.
  float fullZoom = mPresShell->GetPresContext()->GetFullZoom();

  // Pinch-zoom on B2G or fennec.
  float resolution = mPresShell->GetCumulativeResolution();

  return fullZoom * resolution;
}

} // namespace mozilla
