/*
 * (C) 1999 Lars Knoll (knoll@kde.org)
 * (C) 2000 Dirk Mueller (mueller@kde.org)
 * Copyright (C) 2004-2015 Apple Inc. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * along with this library; see the file COPYING.LIB.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 *
 */

#include "config.h"
#include "TextDecorationPainter.h"

#include "DashArray.h"
#include "FontCascade.h"
#include "GraphicsContext.h"
#include "InlineTextBoxStyle.h"
#include "RenderStyle.h"
#include "RenderText.h"
#include "ShadowData.h"
#include "TextRun.h"

namespace WebCore {

/*
 * Draw one cubic Bezier curve and repeat the same pattern long the the decoration's axis.
 * The start point (p1), controlPoint1, controlPoint2 and end point (p2) of the Bezier curve
 * form a diamond shape:
 *
 *                              step
 *                         |-----------|
 *
 *                   controlPoint1
 *                         +
 *
 *
 *                  . .
 *                .     .
 *              .         .
 * (x1, y1) p1 +           .            + p2 (x2, y2) - <--- Decoration's axis
 *                          .         .               |
 *                            .     .                 |
 *                              . .                   | controlPointDistance
 *                                                    |
 *                                                    |
 *                         +                          -
 *                   controlPoint2
 *
 *             |-----------|
 *                 step
 */
static void strokeWavyTextDecoration(GraphicsContext& context, const FloatRect& rect, float fontSize)
{
    auto wavyStrokeParameters = getWavyStrokeParameters(fontSize);

    FloatPoint p1 = rect.minXMinYCorner();
    FloatPoint p2 = rect.maxXMinYCorner();

    // Extent the wavy line before and after the text so it can cover the whole length.
    p1.setX(p1.x() - 2 * wavyStrokeParameters.step);
    p2.setX(p2.x() + 2 * wavyStrokeParameters.step);

    auto bounds = rect;
    // Offset the bounds and set extra height to ensure the whole wavy line is covered.
    bounds.setY(bounds.y() - wavyStrokeParameters.controlPointDistance);
    bounds.setHeight(bounds.height() + 2 * wavyStrokeParameters.controlPointDistance);
    // Clip the extra wavy line added before
    GraphicsContextStateSaver stateSaver(context);
    context.clip(bounds);

    context.adjustLineToPixelBoundaries(p1, p2, rect.height(), context.strokeStyle());

    Path path;
    path.moveTo(p1);

    ASSERT(p1.y() == p2.y());

    float yAxis = p1.y();
    float x1 = std::min(p1.x(), p2.x());
    float x2 = std::max(p1.x(), p2.x());

    FloatPoint controlPoint1(0, yAxis + wavyStrokeParameters.controlPointDistance);
    FloatPoint controlPoint2(0, yAxis - wavyStrokeParameters.controlPointDistance);

    for (float x = x1; x + 2 * wavyStrokeParameters.step <= x2;) {
        controlPoint1.setX(x + wavyStrokeParameters.step);
        controlPoint2.setX(x + wavyStrokeParameters.step);
        x += 2 * wavyStrokeParameters.step;
        path.addBezierCurveTo(controlPoint1, controlPoint2, FloatPoint(x, yAxis));
    }

    context.setShouldAntialias(true);
    context.setStrokeThickness(rect.height());
    context.strokePath(path);
}

static bool compareTuples(std::pair<float, float> l, std::pair<float, float> r)
{
    return l.first < r.first;
}

static DashArray translateIntersectionPointsToSkipInkBoundaries(const DashArray& intersections, float dilationAmount, float totalWidth)
{
    ASSERT(!(intersections.size() % 2));
    
    // Step 1: Make pairs so we can sort based on range starting-point. We dilate the ranges in this step as well.
    Vector<std::pair<float, float>> tuples;
    for (auto i = intersections.begin(); i != intersections.end(); i++, i++)
        tuples.append(std::make_pair(*i - dilationAmount, *(i + 1) + dilationAmount));
    std::sort(tuples.begin(), tuples.end(), &compareTuples);

    // Step 2: Deal with intersecting ranges.
    Vector<std::pair<float, float>> intermediateTuples;
    if (tuples.size() >= 2) {
        intermediateTuples.append(*tuples.begin());
        for (auto i = tuples.begin() + 1; i != tuples.end(); i++) {
            float& firstEnd = intermediateTuples.last().second;
            float secondStart = i->first;
            float secondEnd = i->second;
            if (secondStart <= firstEnd && secondEnd <= firstEnd) {
                // Ignore this range completely
            } else if (secondStart <= firstEnd)
                firstEnd = secondEnd;
            else
                intermediateTuples.append(*i);
        }
    } else
        intermediateTuples = tuples;

    // Step 3: Output the space between the ranges, but only if the space warrants an underline.
    float previous = 0;
    DashArray result;
    for(int i = 0; i < intermediateTuples.size(); ++i) {
    	const auto& tuple = intermediateTuples[i];
        if (tuple.first - previous > dilationAmount) {
            result.append(previous);
            result.append(tuple.first);
        }
        previous = tuple.second;
    }
    if (totalWidth - previous > dilationAmount) {
        result.append(previous);
        result.append(totalWidth);
    }
    
    return result;
}

static StrokeStyle textDecorationStyleToStrokeStyle(TextDecorationStyle decorationStyle)
{
    StrokeStyle strokeStyle = SolidStroke;
    switch (decorationStyle) {
    case TextDecorationStyleSolid:
        strokeStyle = SolidStroke;
        break;
    case TextDecorationStyleDouble:
        strokeStyle = DoubleStroke;
        break;
    case TextDecorationStyleDotted:
        strokeStyle = DottedStroke;
        break;
    case TextDecorationStyleDashed:
        strokeStyle = DashedStroke;
        break;
    case TextDecorationStyleWavy:
        strokeStyle = WavyStroke;
        break;
    }

    return strokeStyle;
}

TextDecorationPainter::TextDecorationPainter(GraphicsContext& context, TextDecoration decoration, const RenderText& renderer, bool isFirstLine, const FontCascade& font, Color textFillColor, Color textStrokeColor, float textStrokeWidth)
    : m_context(context)
    , m_decoration(decoration)
    , m_wavyOffset(wavyOffsetFromDecoration())
    , m_isPrinting(renderer.document().printing())
	, m_width(0)
	, m_isHorizontal(true)
	, m_shadow(0)
	, m_inlineTextBox(0)
	, m_font(font)
    , m_lineStyle(isFirstLine ? *renderer.firstLineStyle() : *renderer.style())
	, m_textFillColor(textFillColor)
	, m_textStrokeColor(textStrokeColor)
	, m_textStrokeWidth(textStrokeWidth)
{
	renderer.getTextDecorationColorsAndStyles(m_decoration, m_underlineColor, m_overlineColor, m_linethroughColor, m_underlineStyle, m_overlineStyle,
		m_linethroughStyle);
	if (isFirstLine) {
        renderer.getTextDecorationColorsAndStyles(m_decoration, m_underlineColor, m_overlineColor, m_linethroughColor,
        	m_underlineStyle, m_overlineStyle, m_linethroughStyle, true);
    }
}

void TextDecorationPainter::paintTextDecoration(const TextRun& textRun, const FloatPoint& textOrigin, const FloatPoint& boxOrigin)
{
    const auto& fontMetrics = m_lineStyle.fontMetrics();
    float textDecorationThickness = m_lineStyle.textDecorationThickness().resolve(m_lineStyle.computedFontSize(), fontMetrics);
    FloatPoint localOrigin = boxOrigin;

    auto paintDecoration = [&] (TextDecoration decoration, TextDecorationStyle style, const Color& color, const FloatRect& rect) {
		m_context.setStrokeColor(color);

		auto strokeStyle = textDecorationStyleToStrokeStyle(style);

		if (style == TextDecorationStyleWavy)
			strokeWavyTextDecoration(m_context, rect, m_lineStyle.computedFontSize());
		else if (decoration == TextDecorationUnderline || decoration == TextDecorationOverline) {
#if ENABLE(CSS3_TEXT_DECORATION_SKIP_INK)
			if ((m_lineStyle.textDecorationSkip() == TextDecorationSkipInk || m_lineStyle.textDecorationSkip() == TextDecorationSkipAuto) && m_isHorizontal) {
				if (!m_context.paintingDisabled()) {
					FloatRect underlineBoundingBox = m_context.computeUnderlineBoundsForText(rect, m_isPrinting);
					DashArray intersections = m_font.dashesForIntersectionsWithRect(textRun, textOrigin, underlineBoundingBox);
					DashArray boundaries = translateIntersectionPointsToSkipInkBoundaries(intersections, underlineBoundingBox.height(), rect.width());
					ASSERT(!(boundaries.size() % 2));
					// We don't use underlineBoundingBox here because drawLinesForText() will run computeUnderlineBoundsForText() internally.
					m_context.drawLinesForText(rect.location(), rect.height(), boundaries, m_isPrinting, style == TextDecorationStyleDouble, strokeStyle);
				}
			} else {
#endif
				if(m_lineStyle.paintOrder() == PaintOrder::Stroke && style == TextDecorationStyleSolid && strokeStyle == SolidStroke) {
					m_context.drawLineForText(rect, m_isPrinting, style == TextDecorationStyleDouble, strokeStyle);

					Color oldFillColor = m_context.fillColor();
					FloatRect fillRect = rect;
					fillRect.inflate(-m_textStrokeWidth);
					m_context.setFillColor(m_textFillColor);
					m_context.fillRect(fillRect);
					m_context.setFillColor(oldFillColor);
				} else {
					// FIXME: Need to support text-decoration-skip: none.

					m_context.drawLineForText(rect, m_isPrinting, style == TextDecorationStyleDouble, strokeStyle);
				}
#if ENABLE(CSS3_TEXT_DECORATION_SKIP_INK)
			}
#endif

		} else {
			ASSERT(decoration == TextDecoration::LineThrough);
			if(m_lineStyle.paintOrder() == PaintOrder::Stroke && style == TextDecorationStyleSolid && strokeStyle == SolidStroke) {
				m_context.drawLineForText(rect, m_isPrinting, style == TextDecorationStyleDouble, strokeStyle);

				Color oldFillColor = m_context.fillColor();
				FloatRect fillRect = rect;
				fillRect.inflate(-m_textStrokeWidth);
				m_context.setFillColor(m_textFillColor);
				m_context.fillRect(fillRect);
				m_context.setFillColor(oldFillColor);
			} else
				m_context.drawLineForText(rect, m_isPrinting, style == TextDecorationStyleDouble, strokeStyle);
		}
    };

    bool linesAreOpaque = !m_isPrinting
        && (!(m_decoration & TextDecorationUnderline) || m_underlineColor.alpha() == 255)
        && (!(m_decoration & TextDecorationOverline) || m_overlineColor.alpha() == 255)
        && (!(m_decoration & TextDecorationLineThrough) || m_linethroughColor.alpha() == 255);

    float extraOffset = 0;
    bool clipping = !linesAreOpaque && m_shadow && m_shadow->next();
    if (clipping) {
        FloatRect clipRect(localOrigin, FloatSize(m_width, fontMetrics.ascent() + 2));
        for (const ShadowData* shadow = m_shadow; shadow; shadow = shadow->next()) {
            int shadowExtent = shadow->paintingExtent();
            FloatRect shadowRect(localOrigin, FloatSize(m_width, fontMetrics.ascent() + 2));
            shadowRect.inflate(shadowExtent);
            float shadowX = LayoutUnit(m_isHorizontal ? shadow->x().value() : shadow->y().value());
			float shadowY = LayoutUnit(m_isHorizontal ? shadow->y().value() : -shadow->x().value());
			shadowRect.move(shadowX, shadowY);
            clipRect.unite(shadowRect);
            extraOffset = std::max(extraOffset, std::max(0.f, shadowY) + shadowExtent);
        }
        m_context.save();
        m_context.clip(clipRect);
        extraOffset += fontMetrics.ascent() + 2;
        localOrigin.move(0, extraOffset);
    }

    const ShadowData* shadow = m_shadow;
    do {
        if (shadow) {
            if (!shadow->next()) {
                // The last set of lines paints normally inside the clip.
                localOrigin.move(0, -extraOffset);
                extraOffset = 0;
            }
            float shadowX = LayoutUnit(m_isHorizontal ? shadow->x().value() : shadow->y().value());
            float shadowY = LayoutUnit(m_isHorizontal ? shadow->y().value() : -shadow->x().value());

            m_context.setShadow(FloatSize(shadowX, shadowY - extraOffset), shadow->radius().value(), shadow->color());
            shadow = shadow->next();
        }
        
        // These decorations should match the visual overflows computed in visualOverflowForDecorations()
        if (m_decoration & TextDecorationUnderline) {
        	float textDecorationBaseFontSize = 16;
			auto defaultGap = m_lineStyle.computedFontSize() / textDecorationBaseFontSize;
			float offset = computeUnderlineOffset(m_lineStyle.textUnderlinePosition(), m_lineStyle.textUnderlineOffset(), m_lineStyle.fontMetrics(), m_inlineTextBox, defaultGap);
			float wavyOffset = m_underlineStyle == TextDecorationStyleWavy ? m_wavyOffset : 0;
			FloatRect rect(localOrigin, FloatSize(m_width, textDecorationThickness));
			rect.move(0, offset + wavyOffset);
            paintDecoration(TextDecorationUnderline, m_underlineStyle, m_underlineColor, rect);
        }
        if (m_decoration & TextDecorationOverline) {
            float wavyOffset = m_overlineStyle == TextDecorationStyleWavy ? m_wavyOffset : 0;
            FloatRect rect(localOrigin, FloatSize(m_width, textDecorationThickness));
            float autoTextDecorationThickness = TextDecorationThickness::createWithAuto().resolve(m_lineStyle.computedFontSize(), fontMetrics);
			rect.move(0, autoTextDecorationThickness - textDecorationThickness - wavyOffset);
            paintDecoration(TextDecorationOverline, m_overlineStyle, m_overlineColor, rect);
        }
        if (m_decoration & TextDecorationLineThrough) {
            FloatRect rect(localOrigin, FloatSize(m_width, textDecorationThickness));
            float autoTextDecorationThickness = TextDecorationThickness::createWithAuto().resolve(m_lineStyle.computedFontSize(), fontMetrics);
			auto center = 2 * fontMetrics.floatAscent() / 3 + autoTextDecorationThickness / 2;
			rect.move(0, center - textDecorationThickness / 2);
			paintDecoration(TextDecorationLineThrough, m_linethroughStyle, m_linethroughColor, rect);
        }
    } while (shadow);

    if (clipping)
        m_context.restore();
    else if (m_shadow)
        m_context.clearShadow();
}

} // namespace WebCore
