/* -*- 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 "mozilla/HTMLEditor.h"
#include "HTMLEditorObjectResizerUtils.h"

#include "HTMLEditUtils.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/EditorUtils.h"
#include "mozilla/LookAndFeel.h"
#include "mozilla/MathAlgorithms.h"
#include "mozilla/Preferences.h"
#include "mozilla/mozalloc.h"
#include "nsAString.h"
#include "nsAlgorithm.h"
#include "nsCOMPtr.h"
#include "nsDebug.h"
#include "nsError.h"
#include "nsGkAtoms.h"
#include "nsIAtom.h"
#include "nsIContent.h"
#include "nsID.h"
#include "nsIDOMDocument.h"
#include "nsIDOMElement.h"
#include "nsIDOMEvent.h"
#include "nsIDOMEventTarget.h"
#include "nsIDOMMouseEvent.h"
#include "nsIDOMNode.h"
#include "nsIDOMText.h"
#include "nsIDocument.h"
#include "nsIEditor.h"
#include "nsIHTMLObjectResizeListener.h"
#include "nsIHTMLObjectResizer.h"
#include "nsIPresShell.h"
#include "nsISupportsUtils.h"
#include "nsPIDOMWindow.h"
#include "nsReadableUtils.h"
#include "nsString.h"
#include "nsStringFwd.h"
#include "nsSubstringTuple.h"
#include "nscore.h"
#include <algorithm>

class nsISelection;

namespace mozilla {

using namespace dom;

/******************************************************************************
 * mozilla::DocumentResizeEventListener
 ******************************************************************************/

NS_IMPL_ISUPPORTS(DocumentResizeEventListener, nsIDOMEventListener)

DocumentResizeEventListener::DocumentResizeEventListener(nsIHTMLEditor* aEditor)
{
  mEditor = do_GetWeakReference(aEditor);
}

NS_IMETHODIMP
DocumentResizeEventListener::HandleEvent(nsIDOMEvent* aMouseEvent)
{
  nsCOMPtr<nsIHTMLObjectResizer> objectResizer = do_QueryReferent(mEditor);
  if (objectResizer) {
    return objectResizer->RefreshResizers();
  }
  return NS_OK;
}

/******************************************************************************
 * mozilla::ResizerSelectionListener
 ******************************************************************************/

NS_IMPL_ISUPPORTS(ResizerSelectionListener, nsISelectionListener)

ResizerSelectionListener::ResizerSelectionListener(nsIHTMLEditor* aEditor)
{
  mEditor = do_GetWeakReference(aEditor);
}

NS_IMETHODIMP
ResizerSelectionListener::NotifySelectionChanged(nsIDOMDocument* aDOMDocument,
                                                 nsISelection* aSelection,
                                                 int16_t aReason)
{
  if ((aReason & (nsISelectionListener::MOUSEDOWN_REASON |
                  nsISelectionListener::KEYPRESS_REASON |
                  nsISelectionListener::SELECTALL_REASON)) && aSelection) {
    // the selection changed and we need to check if we have to
    // hide and/or redisplay resizing handles
    nsCOMPtr<nsIHTMLEditor> editor = do_QueryReferent(mEditor);
    if (editor) {
      editor->CheckSelectionStateForAnonymousButtons(aSelection);
    }
  }

  return NS_OK;
}

/******************************************************************************
 * mozilla::ResizerMouseMotionListener
 ******************************************************************************/

NS_IMPL_ISUPPORTS(ResizerMouseMotionListener, nsIDOMEventListener)

ResizerMouseMotionListener::ResizerMouseMotionListener(nsIHTMLEditor* aEditor)
{
  mEditor = do_GetWeakReference(aEditor);
}

NS_IMETHODIMP
ResizerMouseMotionListener::HandleEvent(nsIDOMEvent* aMouseEvent)
{
  nsCOMPtr<nsIDOMMouseEvent> mouseEvent ( do_QueryInterface(aMouseEvent) );
  if (!mouseEvent) {
    //non-ui event passed in.  bad things.
    return NS_OK;
  }

  // Don't do anything special if not an HTML object resizer editor
  nsCOMPtr<nsIHTMLObjectResizer> objectResizer = do_QueryReferent(mEditor);
  if (objectResizer) {
    // check if we have to redisplay a resizing shadow
    objectResizer->MouseMove(aMouseEvent);
  }

  return NS_OK;
}

/******************************************************************************
 * mozilla::HTMLEditor
 ******************************************************************************/

already_AddRefed<Element>
HTMLEditor::CreateResizer(int16_t aLocation,
                          nsIDOMNode* aParentNode)
{
  nsCOMPtr<nsIDOMElement> retDOM;
  nsresult rv = CreateAnonymousElement(NS_LITERAL_STRING("span"),
                                       aParentNode,
                                       NS_LITERAL_STRING("mozResizer"),
                                       false,
                                       getter_AddRefs(retDOM));

  NS_ENSURE_SUCCESS(rv, nullptr);
  NS_ENSURE_TRUE(retDOM, nullptr);

  // add the mouse listener so we can detect a click on a resizer
  nsCOMPtr<nsIDOMEventTarget> evtTarget = do_QueryInterface(retDOM);
  evtTarget->AddEventListener(NS_LITERAL_STRING("mousedown"), mEventListener,
                              true);

  nsAutoString locationStr;
  switch (aLocation) {
    case nsIHTMLObjectResizer::eTopLeft:
      locationStr = kTopLeft;
      break;
    case nsIHTMLObjectResizer::eTop:
      locationStr = kTop;
      break;
    case nsIHTMLObjectResizer::eTopRight:
      locationStr = kTopRight;
      break;

    case nsIHTMLObjectResizer::eLeft:
      locationStr = kLeft;
      break;
    case nsIHTMLObjectResizer::eRight:
      locationStr = kRight;
      break;

    case nsIHTMLObjectResizer::eBottomLeft:
      locationStr = kBottomLeft;
      break;
    case nsIHTMLObjectResizer::eBottom:
      locationStr = kBottom;
      break;
    case nsIHTMLObjectResizer::eBottomRight:
      locationStr = kBottomRight;
      break;
  }

  nsCOMPtr<Element> ret = do_QueryInterface(retDOM);
  rv = ret->SetAttr(kNameSpaceID_None, nsGkAtoms::anonlocation, locationStr,
                    true);
  NS_ENSURE_SUCCESS(rv, nullptr);
  return ret.forget();
}

already_AddRefed<Element>
HTMLEditor::CreateShadow(nsIDOMNode* aParentNode,
                         nsIDOMElement* aOriginalObject)
{
  // let's create an image through the element factory
  nsAutoString name;
  if (HTMLEditUtils::IsImage(aOriginalObject)) {
    name.AssignLiteral("img");
  } else {
    name.AssignLiteral("span");
  }
  nsCOMPtr<nsIDOMElement> retDOM;
  CreateAnonymousElement(name, aParentNode,
                         NS_LITERAL_STRING("mozResizingShadow"), true,
                         getter_AddRefs(retDOM));

  NS_ENSURE_TRUE(retDOM, nullptr);

  nsCOMPtr<Element> ret = do_QueryInterface(retDOM);
  return ret.forget();
}

already_AddRefed<Element>
HTMLEditor::CreateResizingInfo(nsIDOMNode* aParentNode)
{
  // let's create an info box through the element factory
  nsCOMPtr<nsIDOMElement> retDOM;
  CreateAnonymousElement(NS_LITERAL_STRING("span"), aParentNode,
                         NS_LITERAL_STRING("mozResizingInfo"), true,
                         getter_AddRefs(retDOM));

  nsCOMPtr<Element> ret = do_QueryInterface(retDOM);
  return ret.forget();
}

nsresult
HTMLEditor::SetAllResizersPosition()
{
  NS_ENSURE_TRUE(mTopLeftHandle, NS_ERROR_FAILURE);

  int32_t x = mResizedObjectX;
  int32_t y = mResizedObjectY;
  int32_t w = mResizedObjectWidth;
  int32_t h = mResizedObjectHeight;

  // now let's place all the resizers around the image

  // get the size of resizers
  nsAutoString value;
  float resizerWidth, resizerHeight;
  nsCOMPtr<nsIAtom> dummyUnit;
  mCSSEditUtils->GetComputedProperty(*mTopLeftHandle, *nsGkAtoms::width,
                                     value);
  mCSSEditUtils->ParseLength(value, &resizerWidth, getter_AddRefs(dummyUnit));
  mCSSEditUtils->GetComputedProperty(*mTopLeftHandle, *nsGkAtoms::height,
                                     value);
  mCSSEditUtils->ParseLength(value, &resizerHeight, getter_AddRefs(dummyUnit));

  int32_t rw  = (int32_t)((resizerWidth + 1) / 2);
  int32_t rh =  (int32_t)((resizerHeight+ 1) / 2);

  SetAnonymousElementPosition(x-rw,     y-rh, static_cast<nsIDOMElement*>(GetAsDOMNode(mTopLeftHandle)));
  SetAnonymousElementPosition(x+w/2-rw, y-rh, static_cast<nsIDOMElement*>(GetAsDOMNode(mTopHandle)));
  SetAnonymousElementPosition(x+w-rw-1, y-rh, static_cast<nsIDOMElement*>(GetAsDOMNode(mTopRightHandle)));

  SetAnonymousElementPosition(x-rw,     y+h/2-rh, static_cast<nsIDOMElement*>(GetAsDOMNode(mLeftHandle)));
  SetAnonymousElementPosition(x+w-rw-1, y+h/2-rh, static_cast<nsIDOMElement*>(GetAsDOMNode(mRightHandle)));

  SetAnonymousElementPosition(x-rw,     y+h-rh-1, static_cast<nsIDOMElement*>(GetAsDOMNode(mBottomLeftHandle)));
  SetAnonymousElementPosition(x+w/2-rw, y+h-rh-1, static_cast<nsIDOMElement*>(GetAsDOMNode(mBottomHandle)));
  SetAnonymousElementPosition(x+w-rw-1, y+h-rh-1, static_cast<nsIDOMElement*>(GetAsDOMNode(mBottomRightHandle)));

  return NS_OK;
}

NS_IMETHODIMP
HTMLEditor::RefreshResizers()
{
  // nothing to do if resizers are not displayed...
  NS_ENSURE_TRUE(mResizedObject, NS_OK);

  nsresult rv =
    GetPositionAndDimensions(
      static_cast<nsIDOMElement*>(GetAsDOMNode(mResizedObject)),
      mResizedObjectX,
      mResizedObjectY,
      mResizedObjectWidth,
      mResizedObjectHeight,
      mResizedObjectBorderLeft,
      mResizedObjectBorderTop,
      mResizedObjectMarginLeft,
      mResizedObjectMarginTop);

  NS_ENSURE_SUCCESS(rv, rv);
  rv = SetAllResizersPosition();
  NS_ENSURE_SUCCESS(rv, rv);
  return SetShadowPosition(mResizingShadow, mResizedObject,
                           mResizedObjectX, mResizedObjectY);
}

NS_IMETHODIMP
HTMLEditor::ShowResizers(nsIDOMElement* aResizedElement)
{
  nsresult rv = ShowResizersInner(aResizedElement);
  if (NS_FAILED(rv)) {
    HideResizers();
  }
  return rv;
}

nsresult
HTMLEditor::ShowResizersInner(nsIDOMElement* aResizedElement)
{
  NS_ENSURE_ARG_POINTER(aResizedElement);

  nsCOMPtr<nsIDOMNode> parentNode;
  nsresult rv = aResizedElement->GetParentNode(getter_AddRefs(parentNode));
  NS_ENSURE_SUCCESS(rv, rv);

  if (mResizedObject) {
    NS_ERROR("call HideResizers first");
    return NS_ERROR_UNEXPECTED;
  }

  nsCOMPtr<nsINode> resizedNode = do_QueryInterface(aResizedElement);
  if (NS_WARN_IF(!IsDescendantOfEditorRoot(resizedNode))) {
    return NS_ERROR_UNEXPECTED;
  }

  mResizedObject = do_QueryInterface(aResizedElement);
  NS_ENSURE_STATE(mResizedObject);

  // The resizers and the shadow will be anonymous siblings of the element.
  mTopLeftHandle = CreateResizer(nsIHTMLObjectResizer::eTopLeft, parentNode);
  NS_ENSURE_TRUE(mTopLeftHandle, NS_ERROR_FAILURE);
  mTopHandle = CreateResizer(nsIHTMLObjectResizer::eTop, parentNode);
  NS_ENSURE_TRUE(mTopHandle, NS_ERROR_FAILURE);
  mTopRightHandle = CreateResizer(nsIHTMLObjectResizer::eTopRight, parentNode);
  NS_ENSURE_TRUE(mTopRightHandle, NS_ERROR_FAILURE);

  mLeftHandle = CreateResizer(nsIHTMLObjectResizer::eLeft, parentNode);
  NS_ENSURE_TRUE(mLeftHandle, NS_ERROR_FAILURE);
  mRightHandle = CreateResizer(nsIHTMLObjectResizer::eRight, parentNode);
  NS_ENSURE_TRUE(mRightHandle, NS_ERROR_FAILURE);

  mBottomLeftHandle = CreateResizer(nsIHTMLObjectResizer::eBottomLeft,  parentNode);
  NS_ENSURE_TRUE(mBottomLeftHandle, NS_ERROR_FAILURE);
  mBottomHandle = CreateResizer(nsIHTMLObjectResizer::eBottom,      parentNode);
  NS_ENSURE_TRUE(mBottomHandle, NS_ERROR_FAILURE);
  mBottomRightHandle = CreateResizer(nsIHTMLObjectResizer::eBottomRight, parentNode);
  NS_ENSURE_TRUE(mBottomRightHandle, NS_ERROR_FAILURE);

  rv = GetPositionAndDimensions(aResizedElement,
                                mResizedObjectX,
                                mResizedObjectY,
                                mResizedObjectWidth,
                                mResizedObjectHeight,
                                mResizedObjectBorderLeft,
                                mResizedObjectBorderTop,
                                mResizedObjectMarginLeft,
                                mResizedObjectMarginTop);
  NS_ENSURE_SUCCESS(rv, rv);

  // and let's set their absolute positions in the document
  rv = SetAllResizersPosition();
  NS_ENSURE_SUCCESS(rv, rv);

  // now, let's create the resizing shadow
  mResizingShadow = CreateShadow(parentNode, aResizedElement);
  NS_ENSURE_TRUE(mResizingShadow, NS_ERROR_FAILURE);
  // and set its position
  rv = SetShadowPosition(mResizingShadow, mResizedObject,
                         mResizedObjectX, mResizedObjectY);
  NS_ENSURE_SUCCESS(rv, rv);

  // and then the resizing info tooltip
  mResizingInfo = CreateResizingInfo(parentNode);
  NS_ENSURE_TRUE(mResizingInfo, NS_ERROR_FAILURE);

  // and listen to the "resize" event on the window first, get the
  // window from the document...
  nsCOMPtr<nsIDocument> doc = GetDocument();
  NS_ENSURE_TRUE(doc, NS_ERROR_NULL_POINTER);

  nsCOMPtr<nsIDOMEventTarget> target = do_QueryInterface(doc->GetWindow());
  if (!target) {
    return NS_ERROR_NULL_POINTER;
  }

  mResizeEventListenerP = new DocumentResizeEventListener(this);
  if (!mResizeEventListenerP) {
    return NS_ERROR_OUT_OF_MEMORY;
  }
  rv = target->AddEventListener(NS_LITERAL_STRING("resize"),
                                mResizeEventListenerP, false);
  // XXX Even when it failed to add event listener, should we need to set
  //     _moz_resizing attribute?
  aResizedElement->SetAttribute(NS_LITERAL_STRING("_moz_resizing"), NS_LITERAL_STRING("true"));
  return rv;
}

NS_IMETHODIMP
HTMLEditor::HideResizers()
{
  NS_ENSURE_TRUE(mResizedObject, NS_OK);

  // get the presshell's document observer interface.
  nsCOMPtr<nsIPresShell> ps = GetPresShell();
  // We allow the pres shell to be null; when it is, we presume there
  // are no document observers to notify, but we still want to
  // UnbindFromTree.

  nsCOMPtr<nsIContent> parentContent;

  if (mTopLeftHandle) {
    parentContent = mTopLeftHandle->GetParent();
  }

  NS_NAMED_LITERAL_STRING(mousedown, "mousedown");

  RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
                             mTopLeftHandle, parentContent, ps);
  mTopLeftHandle = nullptr;

  RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
                             mTopHandle, parentContent, ps);
  mTopHandle = nullptr;

  RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
                             mTopRightHandle, parentContent, ps);
  mTopRightHandle = nullptr;

  RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
                             mLeftHandle, parentContent, ps);
  mLeftHandle = nullptr;

  RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
                             mRightHandle, parentContent, ps);
  mRightHandle = nullptr;

  RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
                             mBottomLeftHandle, parentContent, ps);
  mBottomLeftHandle = nullptr;

  RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
                             mBottomHandle, parentContent, ps);
  mBottomHandle = nullptr;

  RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
                             mBottomRightHandle, parentContent, ps);
  mBottomRightHandle = nullptr;

  RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
                             mResizingShadow, parentContent, ps);
  mResizingShadow = nullptr;

  RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
                             mResizingInfo, parentContent, ps);
  mResizingInfo = nullptr;

  if (mActivatedHandle) {
    mActivatedHandle->UnsetAttr(kNameSpaceID_None, nsGkAtoms::_moz_activated,
                                true);
    mActivatedHandle = nullptr;
  }

  // don't forget to remove the listeners !

  nsCOMPtr<nsIDOMEventTarget> target = GetDOMEventTarget();

  if (target && mMouseMotionListenerP) {
    DebugOnly<nsresult> rv =
      target->RemoveEventListener(NS_LITERAL_STRING("mousemove"),
                                  mMouseMotionListenerP, true);
    NS_ASSERTION(NS_SUCCEEDED(rv), "failed to remove mouse motion listener");
  }
  mMouseMotionListenerP = nullptr;

  nsCOMPtr<nsIDocument> doc = GetDocument();
  if (!doc) {
    return NS_ERROR_NULL_POINTER;
  }
  target = do_QueryInterface(doc->GetWindow());
  if (!target) {
    return NS_ERROR_NULL_POINTER;
  }

  if (mResizeEventListenerP) {
    DebugOnly<nsresult> rv =
      target->RemoveEventListener(NS_LITERAL_STRING("resize"),
                                  mResizeEventListenerP, false);
    NS_ASSERTION(NS_SUCCEEDED(rv), "failed to remove resize event listener");
  }
  mResizeEventListenerP = nullptr;

  mResizedObject->UnsetAttr(kNameSpaceID_None, nsGkAtoms::_moz_resizing, true);
  mResizedObject = nullptr;

  return NS_OK;
}

void
HTMLEditor::HideShadowAndInfo()
{
  if (mResizingShadow) {
    mResizingShadow->SetAttr(kNameSpaceID_None, nsGkAtoms::_class,
                             NS_LITERAL_STRING("hidden"), true);
  }
  if (mResizingInfo) {
    mResizingInfo->SetAttr(kNameSpaceID_None, nsGkAtoms::_class,
                           NS_LITERAL_STRING("hidden"), true);
  }
}

nsresult
HTMLEditor::StartResizing(nsIDOMElement* aHandle)
{
  // First notify the listeners if any
  for (auto& listener : mObjectResizeEventListeners) {
    listener->OnStartResizing(static_cast<nsIDOMElement*>(GetAsDOMNode(mResizedObject)));
  }

  mIsResizing = true;
  mActivatedHandle = do_QueryInterface(aHandle);
  NS_ENSURE_STATE(mActivatedHandle || !aHandle);
  mActivatedHandle->SetAttr(kNameSpaceID_None, nsGkAtoms::_moz_activated,
                            NS_LITERAL_STRING("true"), true);

  // do we want to preserve ratio or not?
  bool preserveRatio = HTMLEditUtils::IsImage(mResizedObject) &&
    Preferences::GetBool("editor.resizing.preserve_ratio", true);

  // the way we change the position/size of the shadow depends on
  // the handle
  nsAutoString locationStr;
  aHandle->GetAttribute(NS_LITERAL_STRING("anonlocation"), locationStr);
  if (locationStr.Equals(kTopLeft)) {
    SetResizeIncrements(1, 1, -1, -1, preserveRatio);
  } else if (locationStr.Equals(kTop)) {
    SetResizeIncrements(0, 1, 0, -1, false);
  } else if (locationStr.Equals(kTopRight)) {
    SetResizeIncrements(0, 1, 1, -1, preserveRatio);
  } else if (locationStr.Equals(kLeft)) {
    SetResizeIncrements(1, 0, -1, 0, false);
  } else if (locationStr.Equals(kRight)) {
    SetResizeIncrements(0, 0, 1, 0, false);
  } else if (locationStr.Equals(kBottomLeft)) {
    SetResizeIncrements(1, 0, -1, 1, preserveRatio);
  } else if (locationStr.Equals(kBottom)) {
    SetResizeIncrements(0, 0, 0, 1, false);
  } else if (locationStr.Equals(kBottomRight)) {
    SetResizeIncrements(0, 0, 1, 1, preserveRatio);
  }

  // make the shadow appear
  mResizingShadow->UnsetAttr(kNameSpaceID_None, nsGkAtoms::_class, true);

  // position it
  mCSSEditUtils->SetCSSPropertyPixels(*mResizingShadow, *nsGkAtoms::width,
                                      mResizedObjectWidth);
  mCSSEditUtils->SetCSSPropertyPixels(*mResizingShadow, *nsGkAtoms::height,
                                      mResizedObjectHeight);

  // add a mouse move listener to the editor
  nsresult result = NS_OK;
  if (!mMouseMotionListenerP) {
    mMouseMotionListenerP = new ResizerMouseMotionListener(this);
    if (!mMouseMotionListenerP) {
      return NS_ERROR_OUT_OF_MEMORY;
    }

    nsCOMPtr<nsIDOMEventTarget> target = GetDOMEventTarget();
    NS_ENSURE_TRUE(target, NS_ERROR_FAILURE);

    result = target->AddEventListener(NS_LITERAL_STRING("mousemove"),
                                      mMouseMotionListenerP, true);
    NS_ASSERTION(NS_SUCCEEDED(result),
                 "failed to register mouse motion listener");
  }
  return result;
}

NS_IMETHODIMP
HTMLEditor::MouseDown(int32_t aClientX,
                      int32_t aClientY,
                      nsIDOMElement* aTarget,
                      nsIDOMEvent* aEvent)
{
  bool anonElement = false;
  if (aTarget && NS_SUCCEEDED(aTarget->HasAttribute(NS_LITERAL_STRING("_moz_anonclass"), &anonElement)))
    // we caught a click on an anonymous element
    if (anonElement) {
      nsAutoString anonclass;
      nsresult rv =
        aTarget->GetAttribute(NS_LITERAL_STRING("_moz_anonclass"), anonclass);
      NS_ENSURE_SUCCESS(rv, rv);
      if (anonclass.EqualsLiteral("mozResizer")) {
        // and that element is a resizer, let's start resizing!
        aEvent->PreventDefault();

        mOriginalX = aClientX;
        mOriginalY = aClientY;
        return StartResizing(aTarget);
      }
      if (anonclass.EqualsLiteral("mozGrabber")) {
        // and that element is a grabber, let's start moving the element!
        mOriginalX = aClientX;
        mOriginalY = aClientY;
        return GrabberClicked();
      }
    }
  return NS_OK;
}

NS_IMETHODIMP
HTMLEditor::MouseUp(int32_t aClientX,
                    int32_t aClientY,
                    nsIDOMElement* aTarget)
{
  if (mIsResizing) {
    // we are resizing and release the mouse button, so let's
    // end the resizing process
    mIsResizing = false;
    HideShadowAndInfo();
    SetFinalSize(aClientX, aClientY);
  } else if (mIsMoving || mGrabberClicked) {
    if (mIsMoving) {
      mPositioningShadow->SetAttr(kNameSpaceID_None, nsGkAtoms::_class,
                                  NS_LITERAL_STRING("hidden"), true);
      SetFinalPosition(aClientX, aClientY);
    }
    if (mGrabberClicked) {
      EndMoving();
    }
  }
  return NS_OK;
}

void
HTMLEditor::SetResizeIncrements(int32_t aX,
                                int32_t aY,
                                int32_t aW,
                                int32_t aH,
                                bool aPreserveRatio)
{
  mXIncrementFactor = aX;
  mYIncrementFactor = aY;
  mWidthIncrementFactor = aW;
  mHeightIncrementFactor = aH;
  mPreserveRatio = aPreserveRatio;
}

nsresult
HTMLEditor::SetResizingInfoPosition(int32_t aX,
                                    int32_t aY,
                                    int32_t aW,
                                    int32_t aH)
{
  nsCOMPtr<nsIDOMDocument> domdoc = GetDOMDocument();

  // Determine the position of the resizing info box based upon the new
  // position and size of the element (aX, aY, aW, aH), and which
  // resizer is the "activated handle".  For example, place the resizing
  // info box at the bottom-right corner of the new element, if the element
  // is being resized by the bottom-right resizer.
  int32_t infoXPosition;
  int32_t infoYPosition;

  if (mActivatedHandle == mTopLeftHandle ||
      mActivatedHandle == mLeftHandle ||
      mActivatedHandle == mBottomLeftHandle) {
    infoXPosition = aX;
  } else if (mActivatedHandle == mTopHandle ||
             mActivatedHandle == mBottomHandle) {
    infoXPosition = aX + (aW / 2);
  } else {
    // should only occur when mActivatedHandle is one of the 3 right-side
    // handles, but this is a reasonable default if it isn't any of them (?)
    infoXPosition = aX + aW;
  }

  if (mActivatedHandle == mTopLeftHandle ||
      mActivatedHandle == mTopHandle ||
      mActivatedHandle == mTopRightHandle) {
    infoYPosition = aY;
  } else if (mActivatedHandle == mLeftHandle ||
             mActivatedHandle == mRightHandle) {
    infoYPosition = aY + (aH / 2);
  } else {
    // should only occur when mActivatedHandle is one of the 3 bottom-side
    // handles, but this is a reasonable default if it isn't any of them (?)
    infoYPosition = aY + aH;
  }

  // Offset info box by 20 so it's not directly under the mouse cursor.
  const int mouseCursorOffset = 20;
  mCSSEditUtils->SetCSSPropertyPixels(*mResizingInfo, *nsGkAtoms::left,
                                      infoXPosition + mouseCursorOffset);
  mCSSEditUtils->SetCSSPropertyPixels(*mResizingInfo, *nsGkAtoms::top,
                                      infoYPosition + mouseCursorOffset);

  nsCOMPtr<nsIContent> textInfo = mResizingInfo->GetFirstChild();
  ErrorResult erv;
  if (textInfo) {
    mResizingInfo->RemoveChild(*textInfo, erv);
    NS_ENSURE_TRUE(!erv.Failed(), erv.StealNSResult());
    textInfo = nullptr;
  }

  nsAutoString widthStr, heightStr, diffWidthStr, diffHeightStr;
  widthStr.AppendInt(aW);
  heightStr.AppendInt(aH);
  int32_t diffWidth  = aW - mResizedObjectWidth;
  int32_t diffHeight = aH - mResizedObjectHeight;
  if (diffWidth > 0) {
    diffWidthStr.Assign('+');
  }
  if (diffHeight > 0) {
    diffHeightStr.Assign('+');
  }
  diffWidthStr.AppendInt(diffWidth);
  diffHeightStr.AppendInt(diffHeight);

  nsAutoString info(widthStr + NS_LITERAL_STRING(" x ") + heightStr +
                    NS_LITERAL_STRING(" (") + diffWidthStr +
                    NS_LITERAL_STRING(", ") + diffHeightStr +
                    NS_LITERAL_STRING(")"));

  nsCOMPtr<nsIDOMText> nodeAsText;
  nsresult rv = domdoc->CreateTextNode(info, getter_AddRefs(nodeAsText));
  NS_ENSURE_SUCCESS(rv, rv);
  textInfo = do_QueryInterface(nodeAsText);
  mResizingInfo->AppendChild(*textInfo, erv);
  NS_ENSURE_TRUE(!erv.Failed(), erv.StealNSResult());

  return mResizingInfo->UnsetAttr(kNameSpaceID_None, nsGkAtoms::_class, true);
}

nsresult
HTMLEditor::SetShadowPosition(Element* aShadow,
                              Element* aOriginalObject,
                              int32_t aOriginalObjectX,
                              int32_t aOriginalObjectY)
{
  SetAnonymousElementPosition(aOriginalObjectX, aOriginalObjectY, static_cast<nsIDOMElement*>(GetAsDOMNode(aShadow)));

  if (HTMLEditUtils::IsImage(aOriginalObject)) {
    nsAutoString imageSource;
    aOriginalObject->GetAttr(kNameSpaceID_None, nsGkAtoms::src, imageSource);
    nsresult rv = aShadow->SetAttr(kNameSpaceID_None, nsGkAtoms::src,
                                   imageSource, true);
    NS_ENSURE_SUCCESS(rv, rv);
  }
  return NS_OK;
}

int32_t
HTMLEditor::GetNewResizingIncrement(int32_t aX,
                                    int32_t aY,
                                    int32_t aID)
{
  int32_t result = 0;
  if (!mPreserveRatio) {
    switch (aID) {
      case kX:
      case kWidth:
        result = aX - mOriginalX;
        break;
      case kY:
      case kHeight:
        result = aY - mOriginalY;
        break;
    }
    return result;
  }

  int32_t xi = (aX - mOriginalX) * mWidthIncrementFactor;
  int32_t yi = (aY - mOriginalY) * mHeightIncrementFactor;
  float objectSizeRatio =
              ((float)mResizedObjectWidth) / ((float)mResizedObjectHeight);
  result = (xi > yi) ? xi : yi;
  switch (aID) {
    case kX:
    case kWidth:
      if (result == yi)
        result = (int32_t) (((float) result) * objectSizeRatio);
      result = (int32_t) (((float) result) * mWidthIncrementFactor);
      break;
    case kY:
    case kHeight:
      if (result == xi)
        result =  (int32_t) (((float) result) / objectSizeRatio);
      result = (int32_t) (((float) result) * mHeightIncrementFactor);
      break;
  }
  return result;
}

int32_t
HTMLEditor::GetNewResizingX(int32_t aX,
                            int32_t aY)
{
  int32_t resized = mResizedObjectX +
                    GetNewResizingIncrement(aX, aY, kX) * mXIncrementFactor;
  int32_t max =   mResizedObjectX + mResizedObjectWidth;
  return std::min(resized, max);
}

int32_t
HTMLEditor::GetNewResizingY(int32_t aX,
                            int32_t aY)
{
  int32_t resized = mResizedObjectY +
                    GetNewResizingIncrement(aX, aY, kY) * mYIncrementFactor;
  int32_t max =   mResizedObjectY + mResizedObjectHeight;
  return std::min(resized, max);
}

int32_t
HTMLEditor::GetNewResizingWidth(int32_t aX,
                                int32_t aY)
{
  int32_t resized = mResizedObjectWidth +
                     GetNewResizingIncrement(aX, aY, kWidth) *
                         mWidthIncrementFactor;
  return std::max(resized, 1);
}

int32_t
HTMLEditor::GetNewResizingHeight(int32_t aX,
                                 int32_t aY)
{
  int32_t resized = mResizedObjectHeight +
                     GetNewResizingIncrement(aX, aY, kHeight) *
                         mHeightIncrementFactor;
  return std::max(resized, 1);
}

NS_IMETHODIMP
HTMLEditor::MouseMove(nsIDOMEvent* aMouseEvent)
{
  NS_NAMED_LITERAL_STRING(leftStr, "left");
  NS_NAMED_LITERAL_STRING(topStr, "top");

  if (mIsResizing) {
    // we are resizing and the mouse pointer's position has changed
    // we have to resdisplay the shadow
    nsCOMPtr<nsIDOMMouseEvent> mouseEvent ( do_QueryInterface(aMouseEvent) );
    int32_t clientX, clientY;
    mouseEvent->GetClientX(&clientX);
    mouseEvent->GetClientY(&clientY);

    int32_t newX = GetNewResizingX(clientX, clientY);
    int32_t newY = GetNewResizingY(clientX, clientY);
    int32_t newWidth  = GetNewResizingWidth(clientX, clientY);
    int32_t newHeight = GetNewResizingHeight(clientX, clientY);

    mCSSEditUtils->SetCSSPropertyPixels(*mResizingShadow, *nsGkAtoms::left,
                                        newX);
    mCSSEditUtils->SetCSSPropertyPixels(*mResizingShadow, *nsGkAtoms::top,
                                        newY);
    mCSSEditUtils->SetCSSPropertyPixels(*mResizingShadow, *nsGkAtoms::width,
                                        newWidth);
    mCSSEditUtils->SetCSSPropertyPixels(*mResizingShadow, *nsGkAtoms::height,
                                        newHeight);

    return SetResizingInfoPosition(newX, newY, newWidth, newHeight);
  }

  if (mGrabberClicked) {
    nsCOMPtr<nsIDOMMouseEvent> mouseEvent ( do_QueryInterface(aMouseEvent) );
    int32_t clientX, clientY;
    mouseEvent->GetClientX(&clientX);
    mouseEvent->GetClientY(&clientY);

    int32_t xThreshold =
      LookAndFeel::GetInt(LookAndFeel::eIntID_DragThresholdX, 1);
    int32_t yThreshold =
      LookAndFeel::GetInt(LookAndFeel::eIntID_DragThresholdY, 1);

    if (DeprecatedAbs(clientX - mOriginalX) * 2 >= xThreshold ||
        DeprecatedAbs(clientY - mOriginalY) * 2 >= yThreshold) {
      mGrabberClicked = false;
      StartMoving(nullptr);
    }
  }
  if (mIsMoving) {
    nsCOMPtr<nsIDOMMouseEvent> mouseEvent ( do_QueryInterface(aMouseEvent) );
    int32_t clientX, clientY;
    mouseEvent->GetClientX(&clientX);
    mouseEvent->GetClientY(&clientY);

    int32_t newX = mPositionedObjectX + clientX - mOriginalX;
    int32_t newY = mPositionedObjectY + clientY - mOriginalY;

    SnapToGrid(newX, newY);

    mCSSEditUtils->SetCSSPropertyPixels(*mPositioningShadow, *nsGkAtoms::left,
                                        newX);
    mCSSEditUtils->SetCSSPropertyPixels(*mPositioningShadow, *nsGkAtoms::top,
                                        newY);
  }
  return NS_OK;
}

void
HTMLEditor::SetFinalSize(int32_t aX,
                         int32_t aY)
{
  if (!mResizedObject) {
    // paranoia
    return;
  }

  if (mActivatedHandle) {
    mActivatedHandle->UnsetAttr(kNameSpaceID_None, nsGkAtoms::_moz_activated, true);
    mActivatedHandle = nullptr;
  }

  // we have now to set the new width and height of the resized object
  // we don't set the x and y position because we don't control that in
  // a normal HTML layout
  int32_t left   = GetNewResizingX(aX, aY);
  int32_t top    = GetNewResizingY(aX, aY);
  int32_t width  = GetNewResizingWidth(aX, aY);
  int32_t height = GetNewResizingHeight(aX, aY);
  bool setWidth  = !mResizedObjectIsAbsolutelyPositioned || (width != mResizedObjectWidth);
  bool setHeight = !mResizedObjectIsAbsolutelyPositioned || (height != mResizedObjectHeight);

  int32_t x, y;
  x = left - ((mResizedObjectIsAbsolutelyPositioned) ? mResizedObjectBorderLeft+mResizedObjectMarginLeft : 0);
  y = top - ((mResizedObjectIsAbsolutelyPositioned) ? mResizedObjectBorderTop+mResizedObjectMarginTop : 0);

  // we want one transaction only from a user's point of view
  AutoEditBatch batchIt(this);

  NS_NAMED_LITERAL_STRING(widthStr,  "width");
  NS_NAMED_LITERAL_STRING(heightStr, "height");

  nsCOMPtr<Element> resizedObject = do_QueryInterface(mResizedObject);
  NS_ENSURE_TRUE(resizedObject, );
  if (mResizedObjectIsAbsolutelyPositioned) {
    if (setHeight) {
      mCSSEditUtils->SetCSSPropertyPixels(*resizedObject, *nsGkAtoms::top, y);
    }
    if (setWidth) {
      mCSSEditUtils->SetCSSPropertyPixels(*resizedObject, *nsGkAtoms::left, x);
    }
  }
  if (IsCSSEnabled() || mResizedObjectIsAbsolutelyPositioned) {
    if (setWidth && mResizedObject->HasAttr(kNameSpaceID_None, nsGkAtoms::width)) {
      RemoveAttribute(static_cast<nsIDOMElement*>(GetAsDOMNode(mResizedObject)), widthStr);
    }

    if (setHeight && mResizedObject->HasAttr(kNameSpaceID_None,
                                             nsGkAtoms::height)) {
      RemoveAttribute(static_cast<nsIDOMElement*>(GetAsDOMNode(mResizedObject)), heightStr);
    }

    if (setWidth) {
      mCSSEditUtils->SetCSSPropertyPixels(*resizedObject, *nsGkAtoms::width,
                                          width);
    }
    if (setHeight) {
      mCSSEditUtils->SetCSSPropertyPixels(*resizedObject, *nsGkAtoms::height,
                                          height);
    }
  } else {
    // we use HTML size and remove all equivalent CSS properties

    // we set the CSS width and height to remove it later,
    // triggering an immediate reflow; otherwise, we have problems
    // with asynchronous reflow
    if (setWidth) {
      mCSSEditUtils->SetCSSPropertyPixels(*resizedObject, *nsGkAtoms::width,
                                          width);
    }
    if (setHeight) {
      mCSSEditUtils->SetCSSPropertyPixels(*resizedObject, *nsGkAtoms::height,
                                          height);
    }
    if (setWidth) {
      nsAutoString w;
      w.AppendInt(width);
      SetAttribute(static_cast<nsIDOMElement*>(GetAsDOMNode(mResizedObject)), widthStr, w);
    }
    if (setHeight) {
      nsAutoString h;
      h.AppendInt(height);
      SetAttribute(static_cast<nsIDOMElement*>(GetAsDOMNode(mResizedObject)), heightStr, h);
    }

    if (setWidth) {
      mCSSEditUtils->RemoveCSSProperty(*resizedObject, *nsGkAtoms::width,
                                       EmptyString());
    }
    if (setHeight) {
      mCSSEditUtils->RemoveCSSProperty(*resizedObject, *nsGkAtoms::height,
                                       EmptyString());
    }
  }
  // finally notify the listeners if any
  for (auto& listener : mObjectResizeEventListeners) {
    listener->OnEndResizing(static_cast<nsIDOMElement*>(GetAsDOMNode(mResizedObject)),
                            mResizedObjectWidth, mResizedObjectHeight, width,
                            height);
  }

  // keep track of that size
  mResizedObjectWidth  = width;
  mResizedObjectHeight = height;

  RefreshResizers();
}

NS_IMETHODIMP
HTMLEditor::GetResizedObject(nsIDOMElement** aResizedObject)
{
  nsCOMPtr<nsIDOMElement> ret = static_cast<nsIDOMElement*>(GetAsDOMNode(mResizedObject));
  ret.forget(aResizedObject);
  return NS_OK;
}

NS_IMETHODIMP
HTMLEditor::GetObjectResizingEnabled(bool* aIsObjectResizingEnabled)
{
  *aIsObjectResizingEnabled = mIsObjectResizingEnabled;
  return NS_OK;
}

NS_IMETHODIMP
HTMLEditor::SetObjectResizingEnabled(bool aObjectResizingEnabled)
{
  mIsObjectResizingEnabled = aObjectResizingEnabled;
  return NS_OK;
}

NS_IMETHODIMP
HTMLEditor::AddObjectResizeEventListener(nsIHTMLObjectResizeListener* aListener)
{
  NS_ENSURE_ARG_POINTER(aListener);
  if (mObjectResizeEventListeners.Contains(aListener)) {
    /* listener already registered */
    NS_ASSERTION(false,
                 "trying to register an already registered object resize event listener");
    return NS_OK;
  }
  mObjectResizeEventListeners.AppendElement(*aListener);
  return NS_OK;
}

NS_IMETHODIMP
HTMLEditor::RemoveObjectResizeEventListener(
              nsIHTMLObjectResizeListener* aListener)
{
  NS_ENSURE_ARG_POINTER(aListener);
  if (!mObjectResizeEventListeners.Contains(aListener)) {
    /* listener was not registered */
    NS_ASSERTION(false,
                 "trying to remove an object resize event listener that was not already registered");
    return NS_OK;
  }
  mObjectResizeEventListeners.RemoveElement(aListener);
  return NS_OK;
}

} // namespace mozilla
