/*
 * Copyright (C) 2016 Apple Inc.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */

#include "config.h"
#include "Path.h"

#if USE(DIRECT2D)

#include "AffineTransform.h"
#include "COMPtr.h"
#include "FloatRect.h"
#include "GraphicsContext.h"
#include "IntRect.h"
#include "NotImplemented.h"
#include "StrokeStyleApplier.h"
#include <d2d1.h>
#include <wtf/MathExtras.h>
#include <wtf/RetainPtr.h>
#include <wtf/text/WTFString.h>

namespace WebCore {

static inline ID2D1RenderTarget* scratchRenderTarget()
{
    static COMPtr<ID2D1RenderTarget> renderTarget = adoptCOM(GraphicsContext::defaultRenderTarget());
    return renderTarget.get();
}

Path Path::polygonPathFromPoints(const Vector<FloatPoint>& points)
{
    Path path;
    if (points.size() < 2)
        return path;

    Vector<D2D1_POINT_2F, 32> d2dPoints;
    d2dPoints.reserveInitialCapacity(points.size() - 1);
    for (auto point : points)
        d2dPoints.uncheckedAppend(point);

    path.moveTo(points.first());

    ASSERT(path.activePath());

    path.activePath()->AddLines(d2dPoints.data(), d2dPoints.size());
    path.closeSubpath();

    return path;
}

Path::Path() = default;

Path::~Path() = default;

PlatformPathPtr Path::ensurePlatformPath()
{
    if (!m_path) {
        HRESULT hr = GraphicsContext::systemFactory()->CreateGeometryGroup(D2D1_FILL_MODE_WINDING, nullptr, 0, &m_path);
        ASSERT(SUCCEEDED(hr));
        if (FAILED(hr))
            return nullptr;
    }

    return m_path.get();
}

void Path::appendGeometry(ID2D1Geometry* geometry)
{
    unsigned geometryCount = m_path ? m_path->GetSourceGeometryCount() : 0;
    Vector<ID2D1Geometry*> geometries(geometryCount, nullptr);

    // Note: 'GetSourceGeometries' returns geometries that have a +1 ref count.
    // so they must be released before we return.
    if (geometryCount)
        m_path->GetSourceGeometries(geometries.data(), geometryCount);

    geometry->AddRef();
    geometries.append(geometry);

    auto fillMode = m_path ? m_path->GetFillMode() : D2D1_FILL_MODE_WINDING;

    COMPtr<ID2D1GeometryGroup> protectedPath = m_path;
    m_path = nullptr;

    HRESULT hr = GraphicsContext::systemFactory()->CreateGeometryGroup(fillMode, geometries.data(), geometries.size(), &m_path);
    RELEASE_ASSERT(SUCCEEDED(hr));

    for (auto entry : geometries)
        entry->Release();
}

void Path::createGeometryWithFillMode(WindRule webkitFillMode, COMPtr<ID2D1GeometryGroup>& path) const
{
    RELEASE_ASSERT(m_path);

    auto fillMode = (webkitFillMode == WindRule::EvenOdd) ? D2D1_FILL_MODE_ALTERNATE : D2D1_FILL_MODE_WINDING;

    if (fillMode == m_path->GetFillMode()) {
        path = m_path;
        return;
    }

    unsigned geometryCount = m_path->GetSourceGeometryCount();

    Vector<ID2D1Geometry*> geometries(geometryCount, nullptr);
    ASSERT(geometryCount);

    // Note: 'GetSourceGeometries' returns geometries that have a +1 ref count.
    // so they must be released before we return.
    m_path->GetSourceGeometries(geometries.data(), geometryCount);

    HRESULT hr = GraphicsContext::systemFactory()->CreateGeometryGroup(fillMode, geometries.data(), geometries.size(), &path);
    RELEASE_ASSERT(SUCCEEDED(hr));

    for (auto entry : geometries)
        entry->Release();
}

Path::Path(const Path& other)
{
    if (other.platformPath() && other.activePath()) {
        auto otherPath = other.platformPath();

        unsigned geometryCount = otherPath->GetSourceGeometryCount();

        Vector<ID2D1Geometry*> geometries(geometryCount, nullptr);
        ASSERT(geometryCount);

        // Note: 'GetSourceGeometries' returns geometries that have a +1 ref count.
        // so they must be released before we return.
        otherPath->GetSourceGeometries(geometries.data(), geometryCount);

        HRESULT hr = GraphicsContext::systemFactory()->CreateGeometryGroup(other.m_path->GetFillMode(), geometries.data(), geometryCount, &m_path);
        RELEASE_ASSERT(SUCCEEDED(hr));

        for (auto entry : geometries)
            entry->Release();
    }
}
    
Path::Path(Path&& other)
{
    m_path = other.m_path;
    m_activePath = other.m_activePath;
    m_activePathGeometry = other.m_activePathGeometry;
    other.m_path = nullptr;
    other.m_activePath = nullptr;
    other.m_activePathGeometry = nullptr;
}

Path& Path::operator=(const Path& other)
{
    if (this == &other)
        return *this;
    m_path = other.m_path;
    m_activePath = other.m_activePath;
    m_activePathGeometry = other.m_activePathGeometry;
    return *this;
}

Path& Path::operator=(Path&& other)
{
    if (this == &other)
        return *this;
    m_path = other.m_path;
    m_activePath = other.m_activePath;
    m_activePathGeometry = other.m_activePathGeometry;
    other.m_path = nullptr;
    other.m_activePath = nullptr;
    other.m_activePathGeometry = nullptr;
    return *this;
}

HRESULT Path::initializePathState()
{
    m_path = nullptr;
    m_activePath = nullptr;
    m_activePathGeometry = nullptr;

    GraphicsContext::systemFactory()->CreatePathGeometry(&m_activePathGeometry);

    Vector<ID2D1Geometry*> geometries;
    geometries.append(m_activePathGeometry.get());

    HRESULT hr = GraphicsContext::systemFactory()->CreateGeometryGroup(D2D1_FILL_MODE_WINDING, geometries.data(), geometries.size(), &m_path);
    if (FAILED(hr))
        return hr;

    return m_activePathGeometry->Open(&m_activePath);
}

void Path::drawDidComplete()
{
    FloatPoint currentPoint = this->currentPoint();

    // To maintain proper semantics with CG, we need to clear our Direct2D
    // path objects when a draw has finished.
    HRESULT hr = const_cast<Path*>(this)->initializePathState();

    if (!SUCCEEDED(hr))
        return;

    m_activePath->SetFillMode(D2D1_FILL_MODE_WINDING);

    m_activePath->BeginFigure(currentPoint, D2D1_FIGURE_BEGIN_FILLED);
    ++m_openFigureCount;
}

bool Path::contains(const FloatPoint& point, WindRule rule) const
{
    if (isNull())
        return false;

    if (!fastBoundingRect().contains(point))
        return false;

    BOOL contains;
    if (!SUCCEEDED(m_path->FillContainsPoint(D2D1::Point2F(point.x(), point.y()), nullptr, &contains)))
        return false;

    return contains;
}

bool Path::strokeContains(StrokeStyleApplier* applier, const FloatPoint& point) const
{
    if (isNull())
        return false;

    ASSERT(applier);

    GraphicsContext scratchContext(scratchRenderTarget());
    applier->strokeStyle(&scratchContext);

    BOOL containsPoint = false;
    HRESULT hr = m_path->StrokeContainsPoint(point, scratchContext.strokeThickness(), scratchContext.platformStrokeStyle(), nullptr, 1.0f, &containsPoint);
    if (!SUCCEEDED(hr))
        return false;

    return containsPoint;
}

void Path::closeAnyOpenGeometries()
{
    ASSERT(m_activePath);

    if (!m_openFigureCount)
        return;

    while (m_openFigureCount) {
        m_activePath->EndFigure(D2D1_FIGURE_END_OPEN);
        --m_openFigureCount;
    }

    HRESULT hr = m_activePath->Close();
    ASSERT(SUCCEEDED(hr));
}

void Path::translate(const FloatSize& size)
{
    transform(AffineTransform(1, 0, 0, 1, size.width(), size.height()));
}

void Path::transform(const AffineTransform& transform)
{
    if (transform.isIdentity() || isEmpty())
        return;

    Optional<FloatPoint> currentPoint;
    if (hasCurrentPoint())
        currentPoint = this->currentPoint();

    bool pathIsActive = false;
    if (m_activePath) {
        closeAnyOpenGeometries();
        m_activePath = nullptr;
        m_activePathGeometry = nullptr;
        pathIsActive = true;
    }

    COMPtr<ID2D1TransformedGeometry> transformedPath;
    if (!SUCCEEDED(GraphicsContext::systemFactory()->CreateTransformedGeometry(m_path.get(), transform, &transformedPath)))
        return;

    Vector<ID2D1Geometry*> geometries;
    geometries.append(transformedPath.get());

    if (pathIsActive) {
        GraphicsContext::systemFactory()->CreatePathGeometry(&m_activePathGeometry);
        m_activePathGeometry->Open(&m_activePath);
        geometries.append(m_activePathGeometry.get());
    }

    auto fillMode = m_path->GetFillMode();

    m_path = nullptr;

    HRESULT hr = GraphicsContext::systemFactory()->CreateGeometryGroup(fillMode, geometries.data(), geometries.size(), &m_path);
    RELEASE_ASSERT(SUCCEEDED(hr));

    if (!currentPoint)
        return;

    m_activePath->SetFillMode(fillMode);

    auto transformedPoint = transform.mapPoint(currentPoint.value());
    m_activePath->BeginFigure(transformedPoint, D2D1_FIGURE_BEGIN_FILLED);
    m_openFigureCount = 1;
}

FloatRect Path::boundingRect() const
{
    if (isNull())
        return FloatRect();

    D2D1_RECT_F bounds = { };
    if (!SUCCEEDED(m_path->GetBounds(nullptr, &bounds)))
        return FloatRect();

    return bounds;
}

FloatRect Path::fastBoundingRect() const
{
    if (isNull())
        return FloatRect();

    D2D1_RECT_F bounds = { };
    if (!SUCCEEDED(m_path->GetBounds(nullptr, &bounds)))
        return FloatRect();

    return bounds;
}

FloatRect Path::strokeBoundingRect(StrokeStyleApplier* applier) const
{
    if (isNull())
        return FloatRect();

    if (!applier)
        return boundingRect();

    UNUSED_PARAM(applier);
    notImplemented();

    // Just return regular bounding rect for now.
    return boundingRect();
}

void Path::openFigureAtCurrentPointIfNecessary()
{
    if (m_openFigureCount)
        return;

    m_activePath->SetFillMode(D2D1_FILL_MODE_WINDING);
    m_activePath->BeginFigure(currentPoint(), D2D1_FIGURE_BEGIN_FILLED);
    ++m_openFigureCount;
}

void Path::moveTo(const FloatPoint& point)
{
    if (m_activePath) {
        closeAnyOpenGeometries();
        m_activePath = nullptr;
        m_activePathGeometry = nullptr;
    }

    GraphicsContext::systemFactory()->CreatePathGeometry(&m_activePathGeometry);

    appendGeometry(m_activePathGeometry.get());

    if (!SUCCEEDED(m_activePathGeometry->Open(&m_activePath)))
        return;

    m_activePath->SetFillMode(D2D1_FILL_MODE_WINDING);
    m_activePath->BeginFigure(point, D2D1_FIGURE_BEGIN_FILLED);
    m_openFigureCount = 1;
}

void Path::addLineTo(const FloatPoint& point)
{
    ASSERT(m_activePath.get());

    openFigureAtCurrentPointIfNecessary();
    m_activePath->AddLine(point);
}

void Path::addQuadCurveTo(const FloatPoint& cp, const FloatPoint& p)
{
    ASSERT(m_activePath.get());

    openFigureAtCurrentPointIfNecessary();
    m_activePath->AddQuadraticBezier(D2D1::QuadraticBezierSegment(cp, p));
}

void Path::addBezierCurveTo(const FloatPoint& cp1, const FloatPoint& cp2, const FloatPoint& p)
{
    ASSERT(m_activePath.get());

    openFigureAtCurrentPointIfNecessary();
    m_activePath->AddBezier(D2D1::BezierSegment(cp1, cp2, p));
}

void Path::addArcTo(const FloatPoint& p1, const FloatPoint& p2, float radius)
{
    ASSERT(m_activePath.get());

    FloatPoint p0 = currentPoint();

    if (p1 == p0 || p1 == p2 || WTF::areEssentiallyEqual(radius, 0.0f))
        return addLineTo(p1);

    float direction = (p2.x() - p1.x()) * (p0.y() - p1.y()) + (p2.y() - p1.y()) * (p1.x() - p0.x());
    if (WTF::areEssentiallyEqual(direction, 0.0f))
        return addLineTo(p1);

    auto a2 = toFloatPoint(p0 - p1).lengthSquared();
    auto b2 = toFloatPoint(p1 - p2).lengthSquared();
    auto c2 = toFloatPoint(p0 - p2).lengthSquared();

    double cosx = (a2 + b2 - c2) / (2.0 * std::sqrt(a2 * b2));
    double sinx = std::sqrt(1.0 - cosx * cosx);
    double d = radius / ((1 - cosx) / sinx);

    auto an = toFloatPoint(p1 - p0).scaled(1.0 / std::sqrt(a2));
    auto bn = toFloatPoint(p1 - p2).scaled(1.0 / std::sqrt(b2));

    auto startPoint = toFloatPoint(p1 - an.scaled(d));
    auto p4 = toFloatPoint(p1 - bn.scaled(d));

    bool anticlockwise = (direction < 0);
    an.scale(radius * (anticlockwise ? 1 : -1));

    FloatPoint center(startPoint.x() + an.y(), startPoint.y() - an.x());

    double angle0 = atan2(startPoint.y() - center.y(), startPoint.x() - center.x());
    double angle1 = atan2(p4.y() - center.y(), p4.x() - center.x());

    openFigureAtCurrentPointIfNecessary();
    addLineTo(startPoint);
    addArc(center, radius, angle0, angle1, anticlockwise);
}

static bool equalRadiusWidths(const FloatSize& topLeftRadius, const FloatSize& topRightRadius, const FloatSize& bottomLeftRadius, const FloatSize& bottomRightRadius)
{
    return topLeftRadius.width() == topRightRadius.width()
        && topRightRadius.width() == bottomLeftRadius.width()
        && bottomLeftRadius.width() == bottomRightRadius.width();
}

static bool equalRadiusHeights(const FloatSize& topLeftRadius, const FloatSize& topRightRadius, const FloatSize& bottomLeftRadius, const FloatSize& bottomRightRadius)
{
    return topLeftRadius.height() == bottomLeftRadius.height()
        && bottomLeftRadius.height() == topRightRadius.height()
        && topRightRadius.height() == bottomRightRadius.height();
}

void Path::platformAddPathForRoundedRect(const FloatRect& rect, const FloatSize& topLeftRadius, const FloatSize& topRightRadius, const FloatSize& bottomLeftRadius, const FloatSize& bottomRightRadius)
{
    bool equalWidths = equalRadiusWidths(topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadius);
    bool equalHeights = equalRadiusHeights(topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadius);

    if (equalWidths && equalHeights) {
        // Ensure that CG can render the rounded rect.
        float radiusWidth = topLeftRadius.width();
        float radiusHeight = topLeftRadius.height();
        auto rectToDraw = D2D1::RectF(rect.x(), rect.y(), rect.maxX(), rect.maxY());

        COMPtr<ID2D1RoundedRectangleGeometry> roundRect;
        HRESULT hr = GraphicsContext::systemFactory()->CreateRoundedRectangleGeometry(D2D1::RoundedRect(rectToDraw, radiusWidth, radiusHeight), &roundRect);
        RELEASE_ASSERT(SUCCEEDED(hr));
        appendGeometry(roundRect.get());
        return;
    }

    addBeziersForRoundedRect(rect, topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadius);
}

void Path::closeSubpath()
{
    if (isNull())
        return;

    if (!m_activePath) {
        ASSERT(!m_openFigureCount);
        ASSERT(!m_activePathGeometry);
        return;
    }

    if (!m_openFigureCount)
        return;

    m_activePath->EndFigure(D2D1_FIGURE_END_CLOSED);
    --m_openFigureCount;
    if (m_openFigureCount > 0) {
        ASSERT(m_activePathGeometry);
        return;
    }

    HRESULT hr = m_activePath->Close();
    ASSERT(SUCCEEDED(hr));
}

static FloatPoint arcStart(const FloatPoint& center, float radius, float startAngle)
{
    FloatPoint startingPoint = center;
    float startX = radius * std::cos(startAngle);
    float startY = radius * std::sin(startAngle);
    startingPoint.move(startX, startY);
    return startingPoint;
}

const float twoPi = 2.0f * piFloat;

static void drawArcSection(ID2D1GeometrySink* sink, const FloatPoint& center, float radius, float startAngle, float endAngle, bool anticlockwise)
{
    // Direct2D wants us to specify the end point of the arc, not the center. It will be drawn from
    // whatever the current point in the 'm_activePath' is.
    FloatPoint endPoint = center;
    float endX = radius * std::cos(endAngle);
    float endY = radius * std::sin(endAngle);
    endPoint.move(endX, endY);

    D2D1_SWEEP_DIRECTION direction = anticlockwise ? D2D1_SWEEP_DIRECTION_COUNTER_CLOCKWISE : D2D1_SWEEP_DIRECTION_CLOCKWISE;
    sink->AddArc(D2D1::ArcSegment(endPoint, D2D1::SizeF(radius, radius), 0, direction, D2D1_ARC_SIZE_SMALL));
}

void Path::addArc(const FloatPoint& center, float radius, float startAngle, float endAngle, bool anticlockwise)
{
    auto arcStartPoint = arcStart(center, radius, startAngle);
    if (!m_activePath)
        moveTo(arcStartPoint);
    else if (!areEssentiallyEqual(currentPoint(), arcStartPoint)) {
        // If the arc defined by the center and radius does not intersect the current position,
        // we need to draw a line from the current position to the starting point of the arc.
        addLineTo(arcStartPoint);
    }

    if (WTF::areEssentiallyEqual(std::abs(endAngle - startAngle), twoPi))
        return addEllipse(FloatRect(center.x() - radius, center.y() - radius, 2.0 * radius, 2.0 * radius));

    if (anticlockwise) {
        if (endAngle > startAngle) {
            endAngle -= twoPi * std::ceil((endAngle - startAngle) / twoPi);
            ASSERT(endAngle <= startAngle);
        }
    } else {
        if (startAngle > endAngle) {
            startAngle -= twoPi * std::ceil((startAngle - endAngle) / twoPi);
            ASSERT(startAngle <= endAngle);
        }
    }

    const float delta = anticlockwise ? -piOverTwoFloat : piOverTwoFloat;
    float remainingArcAngle = endAngle - startAngle;

    while ((remainingArcAngle > 0 && remainingArcAngle > delta) || (remainingArcAngle < 0 && remainingArcAngle < delta)) {
        const double currentEndAngle = startAngle + delta;
        drawArcSection(m_activePath.get(), center, radius, startAngle, currentEndAngle, anticlockwise);
        startAngle = currentEndAngle;
        remainingArcAngle -= delta;
    }

    // Handle any remaining part of the arc:
    if (std::abs(remainingArcAngle) > 1e-6)
        drawArcSection(m_activePath.get(), center, radius, startAngle, startAngle + remainingArcAngle, anticlockwise);
}

void Path::addRect(const FloatRect& r)
{
    if (!m_activePath)
        moveTo(r.location());

    COMPtr<ID2D1RectangleGeometry> rectangle;
    HRESULT hr = GraphicsContext::systemFactory()->CreateRectangleGeometry(r, &rectangle);
    RELEASE_ASSERT(SUCCEEDED(hr));
    appendGeometry(rectangle.get());
}

void Path::addEllipse(FloatPoint p, float radiusX, float radiusY, float rotation, float startAngle, float endAngle, bool anticlockwise)
{
    AffineTransform transform;
    transform.translate(p.x(), p.y()).rotate(rad2deg(rotation)).scale(radiusX, radiusY);

    notImplemented();
}

void Path::addEllipse(const FloatRect& r)
{
    if (!m_activePath)
        moveTo(r.location());

    COMPtr<ID2D1EllipseGeometry> ellipse;
    // Note: The radii of the ellipse contained within a rectange are half the width and height of the rect.
    HRESULT hr = GraphicsContext::systemFactory()->CreateEllipseGeometry(D2D1::Ellipse(r.center(), 0.5 * r.width(), 0.5 * r.height()), &ellipse);
    RELEASE_ASSERT(SUCCEEDED(hr));
    appendGeometry(ellipse.get());
}

void Path::addPath(const Path& path, const AffineTransform& transform)
{
    if (!path.platformPath())
        return;

    if (!transform.isInvertible())
        return;

    notImplemented();
}


void Path::clear()
{
    if (isNull())
        return;

    m_path = nullptr;
    m_activePath = nullptr;
    m_activePathGeometry = nullptr;
}

bool Path::isEmpty() const
{
    if (isNull())
        return true;

    if (!m_path->GetSourceGeometryCount())
        return true;

    return false;
}

bool Path::hasCurrentPoint() const
{
    return !isEmpty();
}
    
FloatPoint Path::currentPoint() const 
{
    if (isNull())
        return FloatPoint();

    float length = 0;
    HRESULT hr = m_path->ComputeLength(nullptr, &length);
    if (!SUCCEEDED(hr))
        return FloatPoint();

    D2D1_POINT_2F point = { };
    D2D1_POINT_2F tangent = { };
    hr = m_path->ComputePointAtLength(length, nullptr, &point, &tangent);
    if (!SUCCEEDED(hr))
        return FloatPoint();

    return point;
}

float Path::length() const
{
    float length = 0;
    HRESULT hr = m_path->ComputeLength(nullptr, &length);
    if (!SUCCEEDED(hr))
        return 0;

    return length;
}

void Path::apply(const PathApplierFunction& function) const
{
    if (isNull())
        return;

    notImplemented();
}

}

#endif // USE(DIRECT2D)
