/* -*- Mode: C++; tab-width: 20; 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 "PathD2D.h"
#include "HelpersD2D.h"
#include <math.h>
#include "DrawTargetD2D.h"
#include "Logging.h"

namespace mozilla {
namespace gfx {

// This class exists as a wrapper for ID2D1SimplifiedGeometry sink, it allows
// a geometry to be duplicated into a geometry sink, while removing the final
// figure end and thus allowing a figure that was implicitly closed to be
// continued.
class OpeningGeometrySink : public ID2D1SimplifiedGeometrySink
{
public:
  OpeningGeometrySink(ID2D1SimplifiedGeometrySink *aSink)
    : mSink(aSink)
    , mNeedsFigureEnded(false)
  {
  }

  HRESULT STDMETHODCALLTYPE QueryInterface(const IID &aIID, void **aPtr)
  {
    if (!aPtr) {
      return E_POINTER;
    }

    if (aIID == IID_IUnknown) {
      *aPtr = static_cast<IUnknown*>(this);
      return S_OK;
    } else if (aIID == IID_ID2D1SimplifiedGeometrySink) {
      *aPtr = static_cast<ID2D1SimplifiedGeometrySink*>(this);
      return S_OK;
    }

    return E_NOINTERFACE;
  }

  ULONG STDMETHODCALLTYPE AddRef()
  {
    return 1;
  }

  ULONG STDMETHODCALLTYPE Release()
  {
    return 1;
  }

  // We ignore SetFillMode, the copier will decide.
  STDMETHOD_(void, SetFillMode)(D2D1_FILL_MODE aMode)
  { EnsureFigureEnded(); return; }
  STDMETHOD_(void, BeginFigure)(D2D1_POINT_2F aPoint, D2D1_FIGURE_BEGIN aBegin)
  { EnsureFigureEnded(); return mSink->BeginFigure(aPoint, aBegin); }
  STDMETHOD_(void, AddLines)(const D2D1_POINT_2F *aLines, UINT aCount)
  { EnsureFigureEnded(); return mSink->AddLines(aLines, aCount); }
  STDMETHOD_(void, AddBeziers)(const D2D1_BEZIER_SEGMENT *aSegments, UINT aCount)
  { EnsureFigureEnded(); return mSink->AddBeziers(aSegments, aCount); }
  STDMETHOD(Close)()
  { /* Should never be called! */ return S_OK; }
  STDMETHOD_(void, SetSegmentFlags)(D2D1_PATH_SEGMENT aFlags)
  { return mSink->SetSegmentFlags(aFlags); }

  // This function is special - it's the reason this class exists.
  // It needs to intercept the very last endfigure. So that a user can
  // continue writing to this sink as if they never stopped.
  STDMETHOD_(void, EndFigure)(D2D1_FIGURE_END aEnd)
  {
    if (aEnd == D2D1_FIGURE_END_CLOSED) {
      return mSink->EndFigure(aEnd);
    } else {
      mNeedsFigureEnded = true;
    }
  }
private:
  void EnsureFigureEnded()
  {
    if (mNeedsFigureEnded) {
      mSink->EndFigure(D2D1_FIGURE_END_OPEN);
      mNeedsFigureEnded = false;
    }
  }

  ID2D1SimplifiedGeometrySink *mSink;
  bool mNeedsFigureEnded;
};

class StreamingGeometrySink : public ID2D1SimplifiedGeometrySink
{
public:
  StreamingGeometrySink(PathSink *aSink)
    : mSink(aSink)
  {
  }

  HRESULT STDMETHODCALLTYPE QueryInterface(const IID &aIID, void **aPtr)
  {
    if (!aPtr) {
      return E_POINTER;
    }

    if (aIID == IID_IUnknown) {
      *aPtr = static_cast<IUnknown*>(this);
      return S_OK;
    } else if (aIID == IID_ID2D1SimplifiedGeometrySink) {
      *aPtr = static_cast<ID2D1SimplifiedGeometrySink*>(this);
      return S_OK;
    }

    return E_NOINTERFACE;
  }

  ULONG STDMETHODCALLTYPE AddRef()
  {
    return 1;
  }

  ULONG STDMETHODCALLTYPE Release()
  {
    return 1;
  }

  // We ignore SetFillMode, this depends on the destination sink.
  STDMETHOD_(void, SetFillMode)(D2D1_FILL_MODE aMode)
  { return; }
  STDMETHOD_(void, BeginFigure)(D2D1_POINT_2F aPoint, D2D1_FIGURE_BEGIN aBegin)
  { mSink->MoveTo(ToPoint(aPoint)); }
  STDMETHOD_(void, AddLines)(const D2D1_POINT_2F *aLines, UINT aCount)
  { for (UINT i = 0; i < aCount; i++) { mSink->LineTo(ToPoint(aLines[i])); } }
  STDMETHOD_(void, AddBeziers)(const D2D1_BEZIER_SEGMENT *aSegments, UINT aCount)
  {
    for (UINT i = 0; i < aCount; i++) {
      mSink->BezierTo(ToPoint(aSegments[i].point1), ToPoint(aSegments[i].point2), ToPoint(aSegments[i].point3));
    }
  }
  STDMETHOD(Close)()
  { /* Should never be called! */ return S_OK; }
  STDMETHOD_(void, SetSegmentFlags)(D2D1_PATH_SEGMENT aFlags)
  { /* Should never be called! */ }

  STDMETHOD_(void, EndFigure)(D2D1_FIGURE_END aEnd)
  {
    if (aEnd == D2D1_FIGURE_END_CLOSED) {
      return mSink->Close();
    }
  }
private:

  PathSink *mSink;
};

PathBuilderD2D::~PathBuilderD2D()
{
}

void
PathBuilderD2D::MoveTo(const Point &aPoint)
{
  if (mFigureActive) {
    mSink->EndFigure(D2D1_FIGURE_END_OPEN);
    mFigureActive = false;
  }
  EnsureActive(aPoint);
  mCurrentPoint = aPoint;
}

void
PathBuilderD2D::LineTo(const Point &aPoint)
{
  EnsureActive(aPoint);
  mSink->AddLine(D2DPoint(aPoint));

  mCurrentPoint = aPoint;
}

void
PathBuilderD2D::BezierTo(const Point &aCP1,
                         const Point &aCP2,
                         const Point &aCP3)
  {
  EnsureActive(aCP1);
  mSink->AddBezier(D2D1::BezierSegment(D2DPoint(aCP1),
                                       D2DPoint(aCP2),
                                       D2DPoint(aCP3)));

  mCurrentPoint = aCP3;
}

void
PathBuilderD2D::QuadraticBezierTo(const Point &aCP1,
                                  const Point &aCP2)
{
  EnsureActive(aCP1);
  mSink->AddQuadraticBezier(D2D1::QuadraticBezierSegment(D2DPoint(aCP1),
                                                         D2DPoint(aCP2)));

  mCurrentPoint = aCP2;
}

void
PathBuilderD2D::Close()
{
  if (mFigureActive) {
    mSink->EndFigure(D2D1_FIGURE_END_CLOSED);

    mFigureActive = false;

    EnsureActive(mBeginPoint);
  }
}

void
PathBuilderD2D::Arc(const Point &aOrigin, Float aRadius, Float aStartAngle,
                 Float aEndAngle, bool aAntiClockwise)
{
  if (aAntiClockwise && aStartAngle < aEndAngle) {
    // D2D does things a little differently, and draws the arc by specifying an
    // beginning and an end point. This means the circle will be the wrong way
    // around if the start angle is smaller than the end angle. It might seem
    // tempting to invert aAntiClockwise but that would change the sweeping
    // direction of the arc so instead we exchange start/begin.
    Float oldStart = aStartAngle;
    aStartAngle = aEndAngle;
    aEndAngle = oldStart;
  }

  // XXX - Workaround for now, D2D does not appear to do the desired thing when
  // the angle sweeps a complete circle.
  bool fullCircle = false;
  if (aEndAngle - aStartAngle >= 2 * M_PI) {
    fullCircle = true;
    aEndAngle = Float(aStartAngle + M_PI * 1.9999);
  } else if (aStartAngle - aEndAngle >= 2 * M_PI) {
    fullCircle = true;
    aStartAngle = Float(aEndAngle + M_PI * 1.9999);
  }

  Point startPoint;
  startPoint.x = aOrigin.x + aRadius * cos(aStartAngle);
  startPoint.y = aOrigin.y + aRadius * sin(aStartAngle);

  if (!mFigureActive) {
    EnsureActive(startPoint);
  } else {
    mSink->AddLine(D2DPoint(startPoint));
  }

  Point endPoint;
  endPoint.x = aOrigin.x + aRadius * cosf(aEndAngle);
  endPoint.y = aOrigin.y + aRadius * sinf(aEndAngle);

  D2D1_ARC_SIZE arcSize = D2D1_ARC_SIZE_SMALL;
  D2D1_SWEEP_DIRECTION direction =
    aAntiClockwise ? D2D1_SWEEP_DIRECTION_COUNTER_CLOCKWISE :
                     D2D1_SWEEP_DIRECTION_CLOCKWISE;

  // if startPoint and endPoint of our circle are too close there are D2D issues
  // with drawing the circle as a single arc
  const Float kEpsilon = 1e-5f;
  if (!fullCircle ||
      (std::abs(startPoint.x - endPoint.x) +
       std::abs(startPoint.y - endPoint.y) > kEpsilon)) {

    if (aAntiClockwise) {
      if (aStartAngle - aEndAngle > M_PI) {
        arcSize = D2D1_ARC_SIZE_LARGE;
      }
    } else {
      if (aEndAngle - aStartAngle > M_PI) {
        arcSize = D2D1_ARC_SIZE_LARGE;
      }
    }

    mSink->AddArc(D2D1::ArcSegment(D2DPoint(endPoint),
                                   D2D1::SizeF(aRadius, aRadius),
                                   0.0f,
                                   direction,
                                   arcSize));
  }
  else {
    // our first workaround attempt didn't work, so instead draw the circle as
    // two half-circles
    Float midAngle = aEndAngle > aStartAngle ?
      Float(aStartAngle + M_PI) : Float(aEndAngle + M_PI);
    Point midPoint;
    midPoint.x = aOrigin.x + aRadius * cosf(midAngle);
    midPoint.y = aOrigin.y + aRadius * sinf(midAngle);

    mSink->AddArc(D2D1::ArcSegment(D2DPoint(midPoint),
                                   D2D1::SizeF(aRadius, aRadius),
                                   0.0f,
                                   direction,
                                   arcSize));

    // if the adjusted endPoint computed above is used here and endPoint !=
    // startPoint then this half of the circle won't render...
    mSink->AddArc(D2D1::ArcSegment(D2DPoint(startPoint),
                                   D2D1::SizeF(aRadius, aRadius),
                                   0.0f,
                                   direction,
                                   arcSize));
  }

  mCurrentPoint = endPoint;
}

Point
PathBuilderD2D::CurrentPoint() const
{
  return mCurrentPoint;
}

void
PathBuilderD2D::EnsureActive(const Point &aPoint)
{
  if (!mFigureActive) {
    mSink->BeginFigure(D2DPoint(aPoint), D2D1_FIGURE_BEGIN_FILLED);
    mBeginPoint = aPoint;
    mFigureActive = true;
  }
}

already_AddRefed<Path>
PathBuilderD2D::Finish()
{
  if (mFigureActive) {
    mSink->EndFigure(D2D1_FIGURE_END_OPEN);
  }

  HRESULT hr = mSink->Close();
  if (FAILED(hr)) {
    gfxDebug() << "Failed to close PathSink. Code: " << hexa(hr);
    return nullptr;
  }

  return MakeAndAddRef<PathD2D>(mGeometry, mFigureActive, mCurrentPoint, mFillRule, mBackendType);
}

already_AddRefed<PathBuilder>
PathD2D::CopyToBuilder(FillRule aFillRule) const
{
  return TransformedCopyToBuilder(Matrix(), aFillRule);
}

already_AddRefed<PathBuilder>
PathD2D::TransformedCopyToBuilder(const Matrix &aTransform, FillRule aFillRule) const
{
  RefPtr<ID2D1PathGeometry> path;
  HRESULT hr = DrawTargetD2D::factory()->CreatePathGeometry(getter_AddRefs(path));

  if (FAILED(hr)) {
    gfxWarning() << "Failed to create PathGeometry. Code: " << hexa(hr);
    return nullptr;
  }

  RefPtr<ID2D1GeometrySink> sink;
  hr = path->Open(getter_AddRefs(sink));
  if (FAILED(hr)) {
    gfxWarning() << "Failed to open Geometry for writing. Code: " << hexa(hr);
    return nullptr;
  }

  if (aFillRule == FillRule::FILL_WINDING) {
    sink->SetFillMode(D2D1_FILL_MODE_WINDING);
  }

  if (mEndedActive) {
    OpeningGeometrySink wrapSink(sink);
    mGeometry->Simplify(D2D1_GEOMETRY_SIMPLIFICATION_OPTION_CUBICS_AND_LINES,
                        D2DMatrix(aTransform),
                        &wrapSink);
  } else {
    mGeometry->Simplify(D2D1_GEOMETRY_SIMPLIFICATION_OPTION_CUBICS_AND_LINES,
                        D2DMatrix(aTransform),
                        sink);
  }

  RefPtr<PathBuilderD2D> pathBuilder = new PathBuilderD2D(sink, path, aFillRule, mBackendType);
  
  pathBuilder->mCurrentPoint = aTransform * mEndPoint;
  
  if (mEndedActive) {
    pathBuilder->mFigureActive = true;
  }

  return pathBuilder.forget();
}

void
PathD2D::StreamToSink(PathSink *aSink) const
{
  HRESULT hr;

  StreamingGeometrySink sink(aSink);

  hr = mGeometry->Simplify(D2D1_GEOMETRY_SIMPLIFICATION_OPTION_CUBICS_AND_LINES,
                           D2D1::IdentityMatrix(), &sink);

  if (FAILED(hr)) {
    gfxWarning() << "Failed to stream D2D path to sink. Code: " << hexa(hr);
    return;
  }
}
 
bool
PathD2D::ContainsPoint(const Point &aPoint, const Matrix &aTransform) const
{
  BOOL result;

  HRESULT hr = mGeometry->FillContainsPoint(D2DPoint(aPoint), D2DMatrix(aTransform), 0.001f, &result);

  if (FAILED(hr)) {
    // Log
    return false;
  }

  return !!result;
}

bool
PathD2D::StrokeContainsPoint(const StrokeOptions &aStrokeOptions,
                             const Point &aPoint,
                             const Matrix &aTransform) const
{
  BOOL result;

  RefPtr<ID2D1StrokeStyle> strokeStyle = CreateStrokeStyleForOptions(aStrokeOptions);
  HRESULT hr = mGeometry->StrokeContainsPoint(D2DPoint(aPoint),
                                              aStrokeOptions.mLineWidth,
                                              strokeStyle,
                                              D2DMatrix(aTransform),
                                              &result);

  if (FAILED(hr)) {
    // Log
    return false;
  }

  return !!result;
}

Rect
PathD2D::GetBounds(const Matrix &aTransform) const
{
  D2D1_RECT_F d2dBounds;

  HRESULT hr = mGeometry->GetBounds(D2DMatrix(aTransform), &d2dBounds);

  Rect bounds = ToRect(d2dBounds);
  if (FAILED(hr) || !bounds.IsFinite()) {
    gfxWarning() << "Failed to get stroked bounds for path. Code: " << hexa(hr);
    return Rect();
  }

  return bounds;
}

Rect
PathD2D::GetStrokedBounds(const StrokeOptions &aStrokeOptions,
                          const Matrix &aTransform) const
{
  D2D1_RECT_F d2dBounds;

  RefPtr<ID2D1StrokeStyle> strokeStyle = CreateStrokeStyleForOptions(aStrokeOptions);
  HRESULT hr =
    mGeometry->GetWidenedBounds(aStrokeOptions.mLineWidth, strokeStyle,
                                D2DMatrix(aTransform), &d2dBounds);

  Rect bounds = ToRect(d2dBounds);
  if (FAILED(hr) || !bounds.IsFinite()) {
    gfxWarning() << "Failed to get stroked bounds for path. Code: " << hexa(hr);
    return Rect();
  }

  return bounds;
}

}
}
