/*
 * Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies)
 *
 * 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 "TiledDrawingAreaProxy.h"

#if ENABLE(TILED_BACKING_STORE)
#include "DrawingAreaMessageKinds.h"
#include "DrawingAreaProxyMessageKinds.h"
#include "MessageID.h"
#include "UpdateChunk.h"
#include "WebCoreArgumentCoders.h"
#include "WebPageProxy.h"
#include "WebProcessProxy.h"

using namespace WebCore;

namespace WebKit {

static const int defaultTileWidth = 1024;
static const int defaultTileHeight = 1024;

PassOwnPtr<TiledDrawingAreaProxy> TiledDrawingAreaProxy::create(PlatformWebView* webView, WebPageProxy* webPageProxy)
{
    return adoptPtr(new TiledDrawingAreaProxy(webView, webPageProxy));
}

TiledDrawingAreaProxy::TiledDrawingAreaProxy(PlatformWebView* webView, WebPageProxy* webPageProxy)
    : DrawingAreaProxy(DrawingAreaTypeTiled, webPageProxy)
    , m_isWaitingForDidSetFrameNotification(false)
    , m_isVisible(true)
    , m_webView(webView)
    , m_tileBufferUpdateTimer(RunLoop::main(), this, &TiledDrawingAreaProxy::tileBufferUpdateTimerFired)
    , m_tileCreationTimer(RunLoop::main(), this, &TiledDrawingAreaProxy::tileCreationTimerFired)
    , m_tileSize(defaultTileWidth, defaultTileHeight)
    , m_tileCreationDelay(0.01)
    , m_keepAreaMultiplier(2.5, 4.5)
    , m_coverAreaMultiplier(2, 3)
    , m_contentsScale(1)
{
}

TiledDrawingAreaProxy::~TiledDrawingAreaProxy()
{
}

void TiledDrawingAreaProxy::sizeDidChange()
{
    WebPageProxy* page = this->page();
    if (!page || !page->isValid())
        return;

    if (m_size.isEmpty())
        return;

    m_viewSize = m_size;
    m_lastSetViewSize = m_size;

    if (m_isWaitingForDidSetFrameNotification)
        return;
    m_isWaitingForDidSetFrameNotification = true;

    page->process()->responsivenessTimer()->start();
    page->process()->deprecatedSend(DrawingAreaLegacyMessage::SetSize, page->pageID(), CoreIPC::In(m_size));
}

void TiledDrawingAreaProxy::setPageIsVisible(bool isVisible)
{
    WebPageProxy* page = this->page();

    if (isVisible == m_isVisible)
        return;

    m_isVisible = isVisible;
    if (!page || !page->isValid())
        return;

    if (!m_isVisible) {
        // Tell the web process that it doesn't need to paint anything for now.
        page->process()->deprecatedSend(DrawingAreaLegacyMessage::SuspendPainting, page->pageID(), CoreIPC::In());
        return;
    }

    // The page is now visible.
    page->process()->deprecatedSend(DrawingAreaLegacyMessage::ResumePainting, page->pageID(), CoreIPC::In());

    // FIXME: We should request a full repaint here if needed.
}

void TiledDrawingAreaProxy::didSetSize(const IntSize& viewSize)
{
    ASSERT(m_isWaitingForDidSetFrameNotification);
    m_isWaitingForDidSetFrameNotification = false;

    if (viewSize != m_lastSetViewSize)
        setSize(m_lastSetViewSize, IntSize());

    WebPageProxy* page = this->page();
    page->process()->responsivenessTimer()->stop();
}

void TiledDrawingAreaProxy::didReceiveMessage(CoreIPC::Connection*, CoreIPC::MessageID messageID, CoreIPC::ArgumentDecoder* arguments)
{
    switch (messageID.get<DrawingAreaProxyLegacyMessage::Kind>()) {
    case DrawingAreaProxyLegacyMessage::TileUpdated: {
        int tileID;
        UpdateChunk updateChunk;
        float scale;
        unsigned pendingUpdateCount;
        if (!arguments->decode(CoreIPC::Out(tileID, updateChunk, scale, pendingUpdateCount)))
            return;

        TiledDrawingAreaTile* tile = m_tilesByID.get(tileID);
        ASSERT(!tile || tile->ID() == tileID);
        if (tile)
            tile->updateFromChunk(&updateChunk, scale);
        tileBufferUpdateComplete();
        break;
    }
    case DrawingAreaProxyLegacyMessage::DidSetSize: {
        IntSize size;
        if (!arguments->decode(CoreIPC::Out(size)))
            return;

        didSetSize(size);
        break;
    }
    case DrawingAreaProxyLegacyMessage::Invalidate: {
        IntRect rect;
        if (!arguments->decode(CoreIPC::Out(rect)))
            return;

        invalidate(rect);
        break;
    }
    case DrawingAreaProxyLegacyMessage::AllTileUpdatesProcessed: {
        tileBufferUpdateComplete();
        break;
    }
    case DrawingAreaProxyLegacyMessage::SnapshotTaken: {
        UpdateChunk chunk;
        if (!arguments->decode(CoreIPC::Out(chunk)))
            return;
        snapshotTaken(chunk);
        break;
    }
    default:
        ASSERT_NOT_REACHED();
    }
}

void TiledDrawingAreaProxy::requestTileUpdate(int tileID, const IntRect& dirtyRect)
{
    page()->process()->connection()->deprecatedSend(DrawingAreaLegacyMessage::RequestTileUpdate, page()->pageID(), CoreIPC::In(tileID, dirtyRect, contentsScale()));
}

void TiledDrawingAreaProxy::waitUntilUpdatesComplete()
{
    while (hasPendingUpdates()) {
        int tileID;
        UpdateChunk updateChunk;
        float scale;
        unsigned pendingUpdateCount;
        static const double tileUpdateTimeout = 10.0;
        OwnPtr<CoreIPC::ArgumentDecoder> arguments = page()->process()->connection()->deprecatedWaitFor(DrawingAreaProxyLegacyMessage::TileUpdated, page()->pageID(), tileUpdateTimeout);
        if (!arguments)
            break;
        if (!arguments->decode(CoreIPC::Out(tileID, updateChunk, scale, pendingUpdateCount)))
            break;
        TiledDrawingAreaTile* tile = m_tilesByID.get(tileID);
        ASSERT(!tile || tile->ID() == tileID);
        if (tile)
            tile->updateFromChunk(&updateChunk, scale);
    }
    tileBufferUpdateComplete();
}

PassRefPtr<TiledDrawingAreaTile> TiledDrawingAreaProxy::createTile(const TiledDrawingAreaTile::Coordinate& coordinate)
{
    RefPtr<TiledDrawingAreaTile> tile = TiledDrawingAreaTile::create(this, coordinate);
    setTile(coordinate, tile);
    return tile;
}

void TiledDrawingAreaProxy::setTileSize(const IntSize& size)
{
    if (m_tileSize == size)
        return;
    m_tileSize = size;
    removeAllTiles();
    startTileCreationTimer();
}

void TiledDrawingAreaProxy::setTileCreationDelay(double delay)
{
    m_tileCreationDelay = delay;
}

void TiledDrawingAreaProxy::setKeepAndCoverAreaMultipliers(const FloatSize& keepMultiplier, const FloatSize& coverMultiplier)
{
    m_keepAreaMultiplier = keepMultiplier;
    m_coverAreaMultiplier = coverMultiplier;
    startTileCreationTimer();
}

void TiledDrawingAreaProxy::takeSnapshot(const IntSize& size, const IntRect& contentsRect)
{
    WebPageProxy* page = this->page();
    page->process()->deprecatedSend(DrawingAreaLegacyMessage::TakeSnapshot, page->pageID(), CoreIPC::Out(size, contentsRect));
}

void TiledDrawingAreaProxy::invalidate(const IntRect& contentsDirtyRect)
{
    IntRect dirtyRect(mapFromContents(contentsDirtyRect));

    TiledDrawingAreaTile::Coordinate topLeft = tileCoordinateForPoint(dirtyRect.location());
    TiledDrawingAreaTile::Coordinate bottomRight = tileCoordinateForPoint(IntPoint(dirtyRect.maxX(), dirtyRect.maxY()));

    IntRect coverRect = calculateCoverRect(m_previousVisibleRect);

    Vector<TiledDrawingAreaTile::Coordinate> tilesToRemove;

    for (unsigned yCoordinate = topLeft.y(); yCoordinate <= bottomRight.y(); ++yCoordinate) {
        for (unsigned xCoordinate = topLeft.x(); xCoordinate <= bottomRight.x(); ++xCoordinate) {
            RefPtr<TiledDrawingAreaTile> currentTile = tileAt(TiledDrawingAreaTile::Coordinate(xCoordinate, yCoordinate));
            if (!currentTile)
                continue;
            if (!currentTile->rect().intersects(dirtyRect))
                continue;
            // If a tile outside out current cover rect gets invalidated, just drop it instead of updating.
            if (!currentTile->rect().intersects(coverRect)) {
                tilesToRemove.append(currentTile->coordinate());
                continue;
            }
            currentTile->invalidate(dirtyRect);
        }
    }

    unsigned removeCount = tilesToRemove.size();
    for (unsigned n = 0; n < removeCount; ++n)
        removeTile(tilesToRemove[n]);

    startTileBufferUpdateTimer();
}

void TiledDrawingAreaProxy::updateTileBuffers()
{
    Vector<RefPtr<TiledDrawingAreaTile> > newDirtyTiles;
    TileMap::iterator end = m_tiles.end();
    for (TileMap::iterator it = m_tiles.begin(); it != end; ++it) {
        RefPtr<TiledDrawingAreaTile>& current = it->second;
        if (!current->isDirty())
            continue;
        newDirtyTiles.append(it->second);
    }

    if (newDirtyTiles.isEmpty())
        return;

    unsigned size = newDirtyTiles.size();
    for (unsigned n = 0; n < size; ++n)
        newDirtyTiles[n]->updateBackBuffer();
}

void TiledDrawingAreaProxy::tileBufferUpdateComplete()
{
    // Bail out if all tile back buffers have not been updated.
    Vector<TiledDrawingAreaTile*> tilesToFlip;
    TileMap::iterator end = m_tiles.end();
    for (TileMap::iterator it = m_tiles.begin(); it != end; ++it) {
        RefPtr<TiledDrawingAreaTile>& current = it->second;
        if (current->isReadyToPaint() && (current->isDirty() || current->hasBackBufferUpdatePending()))
            return;
        if (current->hasReadyBackBuffer())
            tilesToFlip.append(current.get());
    }
    // Everything done, move back buffers to front.
    Vector<IntRect> paintedArea;
    unsigned size = tilesToFlip.size();
    for (unsigned n = 0; n < size; ++n) {
        TiledDrawingAreaTile* tile = tilesToFlip[n];
        tile->swapBackBufferToFront();
        // FIXME: should not request system repaint for the full tile.
        paintedArea.append(mapToContents(tile->rect()));
    }
    if (size)
        updateWebView(paintedArea);

    m_tileCreationTimer.startOneShot(0);
}

bool TiledDrawingAreaProxy::paint(const IntRect& rect, PlatformDrawingContext context)
{
    if (m_isWaitingForDidSetFrameNotification) {
        WebPageProxy* page = this->page();
        if (!page->isValid())
            return false;

        if (page->process()->isLaunching())
            return false;
    }

    adjustVisibleRect();

    GraphicsContext gc(context);
    gc.save();

    // Assumes the backing store is painted with the scale transform applied.
    // Since tile content is already scaled, first revert the scaling from the painter.
    gc.scale(FloatSize(1 / m_contentsScale, 1 / m_contentsScale));

    IntRect dirtyRect = mapFromContents(rect);

    TiledDrawingAreaTile::Coordinate topLeft = tileCoordinateForPoint(dirtyRect.location());
    TiledDrawingAreaTile::Coordinate bottomRight = tileCoordinateForPoint(IntPoint(dirtyRect.maxX(), dirtyRect.maxY()));

    for (unsigned yCoordinate = topLeft.y(); yCoordinate <= bottomRight.y(); ++yCoordinate) {
        for (unsigned xCoordinate = topLeft.x(); xCoordinate <= bottomRight.x(); ++xCoordinate) {
            TiledDrawingAreaTile::Coordinate currentCoordinate(xCoordinate, yCoordinate);
            RefPtr<TiledDrawingAreaTile> currentTile = tileAt(currentCoordinate);
            if (currentTile && currentTile->isReadyToPaint())
                currentTile->paint(&gc, dirtyRect);
        }
    }

    gc.restore();
    return true;
}

void TiledDrawingAreaProxy::adjustVisibleRect()
{
    IntRect visibleRect = mapFromContents(webViewVisibleRect());
    if (m_previousVisibleRect == visibleRect)
        return;
    m_previousVisibleRect = visibleRect;

    startTileCreationTimer();
}

void TiledDrawingAreaProxy::setContentsScale(float scale)
{
    if (m_contentsScale == scale)
        return;
    m_contentsScale = scale;
    removeAllTiles();
    createTiles();
}

void TiledDrawingAreaProxy::removeAllTiles()
{
    Vector<RefPtr<TiledDrawingAreaTile> > tilesToRemove;
    copyValuesToVector(m_tiles, tilesToRemove);
    unsigned removeCount = tilesToRemove.size();
    for (unsigned n = 0; n < removeCount; ++n)
        removeTile(tilesToRemove[n]->coordinate());
}

double TiledDrawingAreaProxy::tileDistance(const IntRect& viewport, const TiledDrawingAreaTile::Coordinate& tileCoordinate)
{
    if (viewport.intersects(tileRectForCoordinate(tileCoordinate)))
        return 0;

    IntPoint viewCenter = viewport.location() + IntSize(viewport.width() / 2, viewport.height() / 2);
    TiledDrawingAreaTile::Coordinate centerCoordinate = tileCoordinateForPoint(viewCenter);

    // Manhattan distance, biased so that vertical distances are shorter.
    const double horizontalBias = 1.3;
    return abs(centerCoordinate.y() - tileCoordinate.y()) + horizontalBias * abs(centerCoordinate.x() - tileCoordinate.x());
}

IntRect TiledDrawingAreaProxy::calculateKeepRect(const IntRect& visibleRect) const
{
    IntRect result = visibleRect;
    // Inflates to both sides, so divide inflate delta by 2
    result.inflateX(visibleRect.width() * (m_keepAreaMultiplier.width() - 1) / 2);
    result.inflateY(visibleRect.height() * (m_keepAreaMultiplier.height() - 1) / 2);
    result.intersect(contentsRect());
    return result;
}

IntRect TiledDrawingAreaProxy::calculateCoverRect(const IntRect& visibleRect) const
{
    IntRect result = visibleRect;
    // Inflates to both sides, so divide inflate delta by 2
    result.inflateX(visibleRect.width() * (m_coverAreaMultiplier.width() - 1) / 2);
    result.inflateY(visibleRect.height() * (m_coverAreaMultiplier.height() - 1) / 2);
    result.intersect(contentsRect());
    return result;
}

void TiledDrawingAreaProxy::createTiles()
{
    IntRect visibleRect = mapFromContents(webViewVisibleRect());
    m_previousVisibleRect = visibleRect;

    if (visibleRect.isEmpty())
        return;

    // Resize tiles on edges in case the contents size has changed.
    bool didResizeTiles = resizeEdgeTiles();

    // Remove tiles outside out current maximum keep rect.
    dropTilesOutsideRect(calculateKeepRect(visibleRect));

    // Cover the cover rect with tiles.
    IntRect coverRect = calculateCoverRect(visibleRect);

    // Search for the tile position closest to the viewport center that does not yet contain a tile.
    // Which position is considered the closest depends on the tileDistance function.
    double shortestDistance = std::numeric_limits<double>::infinity();
    Vector<TiledDrawingAreaTile::Coordinate> tilesToCreate;
    unsigned requiredTileCount = 0;
    bool hasVisibleCheckers = false;
    TiledDrawingAreaTile::Coordinate topLeft = tileCoordinateForPoint(visibleRect.location());
    TiledDrawingAreaTile::Coordinate bottomRight = tileCoordinateForPoint(IntPoint(visibleRect.maxX(), visibleRect.maxY()));
    for (unsigned yCoordinate = topLeft.y(); yCoordinate <= bottomRight.y(); ++yCoordinate) {
        for (unsigned xCoordinate = topLeft.x(); xCoordinate <= bottomRight.x(); ++xCoordinate) {
            TiledDrawingAreaTile::Coordinate currentCoordinate(xCoordinate, yCoordinate);
            // Distance is 0 for all currently visible tiles.
            double distance = tileDistance(visibleRect, currentCoordinate);

            RefPtr<TiledDrawingAreaTile> tile = tileAt(currentCoordinate);
            if (!distance && (!tile || !tile->isReadyToPaint()))
                hasVisibleCheckers = true;
            if (tile)
                continue;

            ++requiredTileCount;

            if (distance > shortestDistance)
                continue;
            if (distance < shortestDistance) {
                tilesToCreate.clear();
                shortestDistance = distance;
            }
            tilesToCreate.append(currentCoordinate);
        }
    }

    if (hasVisibleCheckers && shortestDistance > 0)
        return;

    // Now construct the tile(s).
    unsigned tilesToCreateCount = tilesToCreate.size();
    for (unsigned n = 0; n < tilesToCreateCount; ++n)
        createTile(tilesToCreate[n]);

    requiredTileCount -= tilesToCreateCount;

    // Paint the content of the newly created tiles.
    if (tilesToCreateCount || didResizeTiles)
        updateTileBuffers();

    // Keep creating tiles until the whole coverRect is covered.
    if (requiredTileCount)
        m_tileCreationTimer.startOneShot(m_tileCreationDelay);
}

bool TiledDrawingAreaProxy::resizeEdgeTiles()
{
    IntRect contentsRect = this->contentsRect();
    bool wasResized = false;

    Vector<TiledDrawingAreaTile::Coordinate> tilesToRemove;
    TileMap::iterator end = m_tiles.end();
    for (TileMap::iterator it = m_tiles.begin(); it != end; ++it) {
        TiledDrawingAreaTile::Coordinate tileCoordinate = it->second->coordinate();
        IntRect tileRect = it->second->rect();
        IntRect expectedTileRect = tileRectForCoordinate(tileCoordinate);
        if (!contentsRect.contains(tileRect))
            tilesToRemove.append(tileCoordinate);
        else if (expectedTileRect != tileRect) {
            it->second->resize(expectedTileRect.size());
            wasResized = true;
        }
    }
    unsigned removeCount = tilesToRemove.size();
    for (unsigned n = 0; n < removeCount; ++n)
        removeTile(tilesToRemove[n]);
    return wasResized;
}

void TiledDrawingAreaProxy::dropTilesOutsideRect(const IntRect& keepRect)
{
    FloatRect keepRectF = keepRect;

    Vector<TiledDrawingAreaTile::Coordinate> toRemove;
    TileMap::iterator end = m_tiles.end();
    for (TileMap::iterator it = m_tiles.begin(); it != end; ++it) {
        TiledDrawingAreaTile::Coordinate coordinate = it->second->coordinate();
        FloatRect tileRect = it->second->rect();
        if (!tileRect.intersects(keepRectF))
            toRemove.append(coordinate);
    }
    unsigned removeCount = toRemove.size();
    for (unsigned n = 0; n < removeCount; ++n)
        removeTile(toRemove[n]);
}

PassRefPtr<TiledDrawingAreaTile> TiledDrawingAreaProxy::tileAt(const TiledDrawingAreaTile::Coordinate& coordinate) const
{
    return m_tiles.get(coordinate);
}

void TiledDrawingAreaProxy::setTile(const TiledDrawingAreaTile::Coordinate& coordinate, RefPtr<TiledDrawingAreaTile> tile)
{
    m_tiles.set(coordinate, tile);
    m_tilesByID.set(tile->ID(), tile.get());
}

void TiledDrawingAreaProxy::removeTile(const TiledDrawingAreaTile::Coordinate& coordinate)
{
    RefPtr<TiledDrawingAreaTile> tile = m_tiles.take(coordinate);

    m_tilesByID.remove(tile->ID());

    if (!tile->hasBackBufferUpdatePending())
        return;
    WebPageProxy* page = this->page();
    page->process()->deprecatedSend(DrawingAreaLegacyMessage::CancelTileUpdate, page->pageID(), CoreIPC::In(tile->ID()));
}

IntRect TiledDrawingAreaProxy::mapToContents(const IntRect& rect) const
{
    return enclosingIntRect(FloatRect(rect.x() / m_contentsScale,
                                      rect.y() / m_contentsScale,
                                      rect.width() / m_contentsScale,
                                      rect.height() / m_contentsScale));
}

IntRect TiledDrawingAreaProxy::mapFromContents(const IntRect& rect) const
{
    return enclosingIntRect(FloatRect(rect.x() * m_contentsScale,
                                      rect.y() * m_contentsScale,
                                      rect.width() * m_contentsScale,
                                      rect.height() * m_contentsScale));
}

IntRect TiledDrawingAreaProxy::contentsRect() const
{
    return mapFromContents(IntRect(IntPoint(0, 0), m_viewSize));
}

IntRect TiledDrawingAreaProxy::tileRectForCoordinate(const TiledDrawingAreaTile::Coordinate& coordinate) const
{
    IntRect rect(coordinate.x() * m_tileSize.width(),
                 coordinate.y() * m_tileSize.height(),
                 m_tileSize.width(),
                 m_tileSize.height());

    rect.intersect(contentsRect());
    return rect;
}

TiledDrawingAreaTile::Coordinate TiledDrawingAreaProxy::tileCoordinateForPoint(const IntPoint& point) const
{
    int x = point.x() / m_tileSize.width();
    int y = point.y() / m_tileSize.height();
    return TiledDrawingAreaTile::Coordinate(std::max(x, 0), std::max(y, 0));
}


void TiledDrawingAreaProxy::startTileBufferUpdateTimer()
{
    if (m_tileBufferUpdateTimer.isActive())
        return;
    m_tileBufferUpdateTimer.startOneShot(0);
}

void TiledDrawingAreaProxy::tileBufferUpdateTimerFired()
{
    updateTileBuffers();
}

void TiledDrawingAreaProxy::startTileCreationTimer()
{
    if (m_tileCreationTimer.isActive())
        return;
    m_tileCreationTimer.startOneShot(0);
}

void TiledDrawingAreaProxy::tileCreationTimerFired()
{
    createTiles();
}

bool TiledDrawingAreaProxy::hasPendingUpdates() const
{
    TileMap::const_iterator end = m_tiles.end();
    for (TileMap::const_iterator it = m_tiles.begin(); it != end; ++it) {
        const RefPtr<TiledDrawingAreaTile>& current = it->second;
        if (current->hasBackBufferUpdatePending())
            return true;
    }
    return false;
}

} // namespace WebKit

#endif