/*
 * Copyright (C) 2012 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. AND ITS CONTRIBUTORS ``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 ITS 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 "RenderSnapshottedPlugIn.h"

#include "Chrome.h"
#include "ChromeClient.h"
#include "Cursor.h"
#include "FEGaussianBlur.h"
#include "Filter.h"
#include "FrameLoaderClient.h"
#include "FrameView.h"
#include "Gradient.h"
#include "HTMLPlugInImageElement.h"
#include "ImageBuffer.h"
#include "MouseEvent.h"
#include "Page.h"
#include "PaintInfo.h"
#include "Path.h"
#include "SourceGraphic.h"

namespace WebCore {

static const int autoStartPlugInSizeThresholdWidth = 1;
static const int autoStartPlugInSizeThresholdHeight = 1;
static const int startLabelPadding = 10; // Label should be 10px from edge of box.
static const int startLabelInset = 20; // But the label is inset from its box also. FIXME: This will be removed when we go to a ShadowDOM approach.
static const double showLabelAfterMouseOverDelay = 1;
static const double showLabelAutomaticallyDelay = 3;
static const int snapshotLabelBlurRadius = 5;

#if ENABLE(FILTERS)
class RenderSnapshottedPlugInBlurFilter : public Filter {
    WTF_MAKE_FAST_ALLOCATED;
public:
    static PassRefPtr<RenderSnapshottedPlugInBlurFilter> create(int radius)
    {
        return adoptRef(new RenderSnapshottedPlugInBlurFilter(radius));
    }

    void setSourceImageRect(const FloatRect& r)
    {
        m_sourceImageRect = r;
        m_filterRegion = r;
        m_sourceGraphic->setMaxEffectRect(r);
        m_blur->setMaxEffectRect(r);
    }
    virtual FloatRect sourceImageRect() const { return m_sourceImageRect; }
    virtual FloatRect filterRegion() const { return m_filterRegion; }

    void apply();
    ImageBuffer* output() const { return m_blur->asImageBuffer(); }

private:
    RenderSnapshottedPlugInBlurFilter(int radius);

    FloatRect m_sourceImageRect;
    FloatRect m_filterRegion;
    RefPtr<SourceGraphic> m_sourceGraphic;
    RefPtr<FEGaussianBlur> m_blur;
};

RenderSnapshottedPlugInBlurFilter::RenderSnapshottedPlugInBlurFilter(int radius)
{
    setFilterResolution(FloatSize(1, 1));
    m_sourceGraphic = SourceGraphic::create(this);
    m_blur = FEGaussianBlur::create(this, radius, radius);
    m_blur->inputEffects().append(m_sourceGraphic);
}

void RenderSnapshottedPlugInBlurFilter::apply()
{
    m_sourceGraphic->clearResult();
    m_blur->clearResult();
    m_blur->apply();
}
#endif

RenderSnapshottedPlugIn::RenderSnapshottedPlugIn(HTMLPlugInImageElement* element)
    : RenderEmbeddedObject(element)
    , m_snapshotResource(RenderImageResource::create())
    , m_shouldShowLabel(false)
    , m_shouldShowLabelAutomatically(false)
    , m_showedLabelOnce(false)
    , m_showReason(UserMousedOver)
    , m_showLabelDelayTimer(this, &RenderSnapshottedPlugIn::showLabelDelayTimerFired)
    , m_snapshotResourceForLabel(RenderImageResource::create())
{
    m_snapshotResource->initialize(this);
    m_snapshotResourceForLabel->initialize(this);
}

RenderSnapshottedPlugIn::~RenderSnapshottedPlugIn()
{
    ASSERT(m_snapshotResource);
    m_snapshotResource->shutdown();
    ASSERT(m_snapshotResourceForLabel);
    m_snapshotResourceForLabel->shutdown();
}

HTMLPlugInImageElement* RenderSnapshottedPlugIn::plugInImageElement() const
{
    return static_cast<HTMLPlugInImageElement*>(node());
}

void RenderSnapshottedPlugIn::updateSnapshot(PassRefPtr<Image> image)
{
    // Zero-size plugins will have no image.
    if (!image)
        return;

    // We may have stored a version of this snapshot to use when showing the
    // label. Invalidate it now and it will be regenerated later.
    if (m_snapshotResourceForLabel->hasImage())
        m_snapshotResourceForLabel->setCachedImage(0);

    m_snapshotResource->setCachedImage(new CachedImage(image.get()));
    repaint();
    if (m_shouldShowLabelAutomatically)
        resetDelayTimer(ShouldShowAutomatically);
}

void RenderSnapshottedPlugIn::paint(PaintInfo& paintInfo, const LayoutPoint& paintOffset)
{
    if (plugInImageElement()->displayState() < HTMLPlugInElement::PlayingWithPendingMouseClick) {
        RenderReplaced::paint(paintInfo, paintOffset);
        return;
    }

    RenderEmbeddedObject::paint(paintInfo, paintOffset);
}

void RenderSnapshottedPlugIn::paintReplaced(PaintInfo& paintInfo, const LayoutPoint& paintOffset)
{
    if (plugInImageElement()->displayState() < HTMLPlugInElement::PlayingWithPendingMouseClick) {
        if (m_shouldShowLabel)
            paintReplacedSnapshotWithLabel(paintInfo, paintOffset);
        else
            paintReplacedSnapshot(paintInfo, paintOffset);
        return;
    }

    RenderEmbeddedObject::paintReplaced(paintInfo, paintOffset);
}

void RenderSnapshottedPlugIn::paintSnapshot(Image* image, PaintInfo& paintInfo, const LayoutPoint& paintOffset)
{
    LayoutUnit cWidth = contentWidth();
    LayoutUnit cHeight = contentHeight();
    if (!cWidth || !cHeight)
        return;

    GraphicsContext* context = paintInfo.context;
#if PLATFORM(MAC)
    if (style()->highlight() != nullAtom && !context->paintingDisabled())
        paintCustomHighlight(toPoint(paintOffset - location()), style()->highlight(), true);
#endif

    LayoutSize contentSize(cWidth, cHeight);
    LayoutPoint contentLocation = paintOffset;
    contentLocation.move(borderLeft() + paddingLeft(), borderTop() + paddingTop());

    LayoutRect rect(contentLocation, contentSize);
    IntRect alignedRect = pixelSnappedIntRect(rect);
    if (alignedRect.width() <= 0 || alignedRect.height() <= 0)
        return;

    bool useLowQualityScaling = shouldPaintAtLowQuality(context, image, image, alignedRect.size());
    context->drawImage(image, style()->colorSpace(), alignedRect, CompositeSourceOver, shouldRespectImageOrientation(), useLowQualityScaling);
}

void RenderSnapshottedPlugIn::paintReplacedSnapshot(PaintInfo& paintInfo, const LayoutPoint& paintOffset)
{
    RefPtr<Image> image = m_snapshotResource->image();
    if (!image || image->isNull())
        return;

    paintSnapshot(image.get(), paintInfo, paintOffset);
}

Image* RenderSnapshottedPlugIn::startLabelImage(LabelSize size) const
{
    static Image* labelImages[2] = { 0, 0 };
    static bool initializedImages[2] = { false, false };

    int arrayIndex = static_cast<int>(size);
    if (labelImages[arrayIndex])
        return labelImages[arrayIndex];
    if (initializedImages[arrayIndex])
        return 0;

    if (document()->page()) {
        labelImages[arrayIndex] = document()->page()->chrome()->client()->plugInStartLabelImage(size).leakRef();
        initializedImages[arrayIndex] = true;
    }
    return labelImages[arrayIndex];
}

#if ENABLE(FILTERS)
static PassRefPtr<Image> snapshottedPluginImageForLabelDisplay(PassRefPtr<Image> snapshot, const LayoutRect& blurRegion)
{
    OwnPtr<ImageBuffer> snapshotBuffer = ImageBuffer::create(snapshot->size());
    snapshotBuffer->context()->drawImage(snapshot.get(), ColorSpaceDeviceRGB, IntPoint(0, 0));

    OwnPtr<ImageBuffer> blurBuffer = ImageBuffer::create(roundedIntSize(blurRegion.size()));
    blurBuffer->context()->drawImage(snapshot.get(), ColorSpaceDeviceRGB, IntPoint(-blurRegion.x(), -blurRegion.y()));

    RefPtr<RenderSnapshottedPlugInBlurFilter> blurFilter = RenderSnapshottedPlugInBlurFilter::create(snapshotLabelBlurRadius);
    blurFilter->setSourceImage(blurBuffer.release());
    blurFilter->setSourceImageRect(FloatRect(FloatPoint(), blurRegion.size()));
    blurFilter->apply();

    snapshotBuffer->context()->drawImageBuffer(blurFilter->output(), ColorSpaceDeviceRGB, roundedIntPoint(blurRegion.location()));
    return snapshotBuffer->copyImage();
}
#endif

void RenderSnapshottedPlugIn::paintReplacedSnapshotWithLabel(PaintInfo& paintInfo, const LayoutPoint& paintOffset)
{
    if (contentBoxRect().isEmpty())
        return;

    if (!plugInImageElement()->hovered() && m_showReason == UserMousedOver)
        return;

    m_showedLabelOnce = true;
    LayoutRect rect = contentBoxRect();
    LayoutRect labelRect = tryToFitStartLabel(LabelSizeLarge, rect);
    LabelSize size = NoLabel;
    if (!labelRect.isEmpty())
        size = LabelSizeLarge;
    else {
        labelRect = tryToFitStartLabel(LabelSizeSmall, rect);
        if (!labelRect.isEmpty())
            size = LabelSizeSmall;
        else
            return;
    }

    Image* labelImage = startLabelImage(size);
    if (!labelImage)
        return;

    RefPtr<Image> snapshotImage = m_snapshotResource->image();
    if (!snapshotImage || snapshotImage->isNull())
        return;

#if ENABLE(FILTERS)
    RefPtr<Image> blurredSnapshotImage = m_snapshotResourceForLabel->image();
    if (!blurredSnapshotImage || blurredSnapshotImage->isNull()) {
        blurredSnapshotImage = snapshottedPluginImageForLabelDisplay(snapshotImage, labelRect);
        m_snapshotResourceForLabel->setCachedImage(new CachedImage(blurredSnapshotImage.get()));
    }
    snapshotImage = blurredSnapshotImage;
#endif

    paintSnapshot(snapshotImage.get(), paintInfo, paintOffset);

    // Remember that the labelRect includes the label inset, so we need to adjust for it.
    paintInfo.context->drawImage(labelImage, ColorSpaceDeviceRGB,
                                 IntRect(roundedIntPoint(paintOffset + labelRect.location() - IntSize(startLabelInset, startLabelInset)),
                                         roundedIntSize(labelRect.size() + IntSize(2 * startLabelInset, 2 * startLabelInset))),
                                 labelImage->rect());
}

void RenderSnapshottedPlugIn::repaintLabel()
{
    // FIXME: This is unfortunate. We should just repaint the label.
    repaint();
}

void RenderSnapshottedPlugIn::showLabelDelayTimerFired(Timer<RenderSnapshottedPlugIn>*)
{
    m_shouldShowLabel = true;
    repaintLabel();
}

void RenderSnapshottedPlugIn::setShouldShowLabelAutomatically(bool show)
{
    m_shouldShowLabelAutomatically = show;
}

CursorDirective RenderSnapshottedPlugIn::getCursor(const LayoutPoint& point, Cursor& overrideCursor) const
{
    if (plugInImageElement()->displayState() < HTMLPlugInElement::PlayingWithPendingMouseClick) {
        overrideCursor = handCursor();
        return SetCursor;
    }
    return RenderEmbeddedObject::getCursor(point, overrideCursor);
}

void RenderSnapshottedPlugIn::handleEvent(Event* event)
{
    if (!event->isMouseEvent())
        return;

    MouseEvent* mouseEvent = static_cast<MouseEvent*>(event);

    if (event->type() == eventNames().clickEvent) {
        if (mouseEvent->button() != LeftButton)
            return;

        plugInImageElement()->setDisplayState(HTMLPlugInElement::PlayingWithPendingMouseClick);
        plugInImageElement()->userDidClickSnapshot(mouseEvent);

        if (widget()) {
            if (Frame* frame = document()->frame())
                frame->loader()->client()->recreatePlugin(widget());
            repaint();
        }
        event->setDefaultHandled();
    } else if (event->type() == eventNames().mousedownEvent) {
        if (mouseEvent->button() != LeftButton)
            return;

        if (m_showLabelDelayTimer.isActive())
            m_showLabelDelayTimer.stop();

        event->setDefaultHandled();
    } else if (event->type() == eventNames().mouseoverEvent) {
        if (!m_showedLabelOnce || m_showReason != ShouldShowAutomatically)
            resetDelayTimer(UserMousedOver);
        event->setDefaultHandled();
    } else if (event->type() == eventNames().mouseoutEvent) {
        if (m_showLabelDelayTimer.isActive())
            m_showLabelDelayTimer.stop();
        if (m_shouldShowLabel) {
            if (m_showReason == UserMousedOver) {
                m_shouldShowLabel = false;
                repaintLabel();
            }
        } else if (m_shouldShowLabelAutomatically)
            resetDelayTimer(ShouldShowAutomatically);
        event->setDefaultHandled();
    }
}

LayoutRect RenderSnapshottedPlugIn::tryToFitStartLabel(LabelSize size, const LayoutRect& contentBox) const
{
    Image* labelImage = startLabelImage(size);
    if (!labelImage)
        return LayoutRect();

    // Assume that the labelImage has been provided to match our device scale.
    float scaleFactor = 1;
    if (document()->page())
        scaleFactor = document()->page()->deviceScaleFactor();
    IntSize labelImageSize = labelImage->size();
    labelImageSize.scale(1 / (scaleFactor ? scaleFactor : 1));

    LayoutSize labelSize = labelImageSize - LayoutSize(2 * startLabelInset, 2 * startLabelInset);
    LayoutRect candidateRect(contentBox.maxXMinYCorner() + LayoutSize(-startLabelPadding, startLabelPadding) + LayoutSize(-labelSize.width(), 0), labelSize);
    // The minimum allowed content box size is the label image placed in the center of the box, surrounded by startLabelPadding.
    if (candidateRect.x() < startLabelPadding || candidateRect.maxY() > contentBox.height() - startLabelPadding)
        return LayoutRect();
    return candidateRect;
}

void RenderSnapshottedPlugIn::resetDelayTimer(ShowReason reason)
{
    m_showReason = reason;
    m_showLabelDelayTimer.startOneShot(reason == UserMousedOver ? showLabelAfterMouseOverDelay : showLabelAutomaticallyDelay);
}

} // namespace WebCore
