/*
 * Copyright (C) 2013 Google 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:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * 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.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT
 * OWNER 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 "core/frame/PinchViewport.h"

#include "core/frame/FrameHost.h"
#include "core/frame/FrameView.h"
#include "core/frame/LocalFrame.h"
#include "core/frame/Settings.h"
#include "core/page/Chrome.h"
#include "core/page/ChromeClient.h"
#include "core/page/Page.h"
#include "core/page/scrolling/ScrollingCoordinator.h"
#include "core/rendering/RenderView.h"
#include "core/rendering/compositing/RenderLayerCompositor.h"
#include "platform/TraceEvent.h"
#include "platform/geometry/FloatSize.h"
#include "platform/graphics/GraphicsLayer.h"
#include "platform/graphics/GraphicsLayerFactory.h"
#include "platform/scroll/Scrollbar.h"
#include "public/platform/Platform.h"
#include "public/platform/WebCompositorSupport.h"
#include "public/platform/WebLayer.h"
#include "public/platform/WebLayerTreeView.h"
#include "public/platform/WebScrollbar.h"
#include "public/platform/WebScrollbarLayer.h"

using blink::WebLayer;
using blink::WebLayerTreeView;
using blink::WebScrollbar;
using blink::WebScrollbarLayer;
using blink::FrameHost;
using blink::GraphicsLayer;
using blink::GraphicsLayerFactory;

namespace blink {

PinchViewport::PinchViewport(FrameHost& owner)
    : m_frameHost(owner)
    , m_scale(1)
{
    reset();
}

PinchViewport::~PinchViewport() { }

void PinchViewport::setSize(const IntSize& size)
{
    if (m_size == size)
        return;

    TRACE_EVENT2("blink", "PinchViewport::setSize", "width", size.width(), "height", size.height());
    m_size = size;

    // Make sure we clamp the offset to within the new bounds.
    setLocation(m_offset);

    if (m_innerViewportContainerLayer) {
        m_innerViewportContainerLayer->setSize(m_size);

        // Need to re-compute sizes for the overlay scrollbars.
        setupScrollbar(WebScrollbar::Horizontal);
        setupScrollbar(WebScrollbar::Vertical);
    }
}

void PinchViewport::reset()
{
    setLocation(FloatPoint());
    setScale(1);
}

void PinchViewport::mainFrameDidChangeSize()
{
    TRACE_EVENT0("blink", "PinchViewport::mainFrameDidChangeSize");

    // In unit tests we may not have initialized the layer tree.
    if (m_innerViewportScrollLayer)
        m_innerViewportScrollLayer->setSize(contentsSize());

    // Make sure the viewport's offset is clamped within the newly sized main frame.
    setLocation(m_offset);
}

FloatRect PinchViewport::visibleRect() const
{
    FloatSize scaledSize(m_size);
    scaledSize.scale(1 / m_scale);
    return FloatRect(m_offset, scaledSize);
}

FloatRect PinchViewport::visibleRectInDocument() const
{
    if (!mainFrame() || !mainFrame()->view())
        return FloatRect();

    FloatRect viewRect = mainFrame()->view()->visibleContentRect();
    FloatRect pinchRect = visibleRect();
    pinchRect.moveBy(viewRect.location());
    return pinchRect;
}

void PinchViewport::scrollIntoView(const FloatRect& rect)
{
    if (!mainFrame() || !mainFrame()->view())
        return;

    FrameView* view = mainFrame()->view();

    float centeringOffsetX = (visibleRect().width() - rect.width()) / 2;
    float centeringOffsetY = (visibleRect().height() - rect.height()) / 2;

    FloatPoint targetOffset(
        rect.x() - centeringOffsetX - visibleRect().x(),
        rect.y() - centeringOffsetY - visibleRect().y());

    view->setScrollPosition(flooredIntPoint(targetOffset));

    FloatPoint remainder = FloatPoint(targetOffset - view->scrollPosition());
    move(remainder);
}

void PinchViewport::setLocation(const FloatPoint& newLocation)
{
    FloatPoint clampedOffset(clampOffsetToBoundaries(newLocation));

    if (clampedOffset == m_offset)
        return;

    m_offset = clampedOffset;

    ScrollingCoordinator* coordinator = m_frameHost.page().scrollingCoordinator();
    ASSERT(coordinator);
    coordinator->scrollableAreaScrollLayerDidChange(this);

    mainFrame()->loader().saveScrollState();
}

void PinchViewport::move(const FloatPoint& delta)
{
    setLocation(m_offset + delta);
}

void PinchViewport::setScale(float scale)
{
    if (scale == m_scale)
        return;

    m_scale = scale;

    if (mainFrame())
        mainFrame()->loader().saveScrollState();

    // Old-style pinch sets scale here but we shouldn't call into the
    // clamping code below.
    if (!m_innerViewportScrollLayer)
        return;

    // Ensure we clamp so we remain within the bounds.
    setLocation(visibleRect().location());

    // TODO: We should probably be calling scaleDidChange type functions here.
    // see Page::setPageScaleFactor.
}

// Modifies the top of the graphics layer tree to add layers needed to support
// the inner/outer viewport fixed-position model for pinch zoom. When finished,
// the tree will look like this (with * denoting added layers):
//
// *rootTransformLayer
//  +- *innerViewportContainerLayer (fixed pos container)
//      +- *pageScaleLayer
//  |       +- *innerViewportScrollLayer
//  |           +-- overflowControlsHostLayer (root layer)
//  |               +-- outerViewportContainerLayer (fixed pos container) [frame container layer in RenderLayerCompositor]
//  |               |   +-- outerViewportScrollLayer [frame scroll layer in RenderLayerCompositor]
//  |               |       +-- content layers ...
//  |               +-- horizontal ScrollbarLayer (non-overlay)
//  |               +-- verticalScrollbarLayer (non-overlay)
//  |               +-- scroll corner (non-overlay)
//  +- *horizontalScrollbarLayer (overlay)
//  +- *verticalScrollbarLayer (overlay)
//
void PinchViewport::attachToLayerTree(GraphicsLayer* currentLayerTreeRoot, GraphicsLayerFactory* graphicsLayerFactory)
{
    TRACE_EVENT1("blink", "PinchViewport::attachToLayerTree", "currentLayerTreeRoot", (bool)currentLayerTreeRoot);
    if (!currentLayerTreeRoot) {
        m_innerViewportScrollLayer->removeAllChildren();
        return;
    }

    if (currentLayerTreeRoot->parent() && currentLayerTreeRoot->parent() == m_innerViewportScrollLayer)
        return;

    if (!m_innerViewportScrollLayer) {
        ASSERT(!m_overlayScrollbarHorizontal
            && !m_overlayScrollbarVertical
            && !m_pageScaleLayer
            && !m_innerViewportContainerLayer);

        // FIXME: The root transform layer should only be created on demand.
        m_rootTransformLayer = GraphicsLayer::create(graphicsLayerFactory, this);
        m_innerViewportContainerLayer = GraphicsLayer::create(graphicsLayerFactory, this);
        m_pageScaleLayer = GraphicsLayer::create(graphicsLayerFactory, this);
        m_innerViewportScrollLayer = GraphicsLayer::create(graphicsLayerFactory, this);
        m_overlayScrollbarHorizontal = GraphicsLayer::create(graphicsLayerFactory, this);
        m_overlayScrollbarVertical = GraphicsLayer::create(graphicsLayerFactory, this);

        blink::ScrollingCoordinator* coordinator = m_frameHost.page().scrollingCoordinator();
        ASSERT(coordinator);
        coordinator->setLayerIsContainerForFixedPositionLayers(m_innerViewportScrollLayer.get(), true);

        // Set masks to bounds so the compositor doesn't clobber a manually
        // set inner viewport container layer size.
        m_innerViewportContainerLayer->setMasksToBounds(m_frameHost.settings().mainFrameClipsContent());
        m_innerViewportContainerLayer->setSize(m_size);

        m_innerViewportScrollLayer->platformLayer()->setScrollClipLayer(
            m_innerViewportContainerLayer->platformLayer());
        m_innerViewportScrollLayer->platformLayer()->setUserScrollable(true, true);

        m_rootTransformLayer->addChild(m_innerViewportContainerLayer.get());
        m_innerViewportContainerLayer->addChild(m_pageScaleLayer.get());
        m_pageScaleLayer->addChild(m_innerViewportScrollLayer.get());
        m_innerViewportContainerLayer->addChild(m_overlayScrollbarHorizontal.get());
        m_innerViewportContainerLayer->addChild(m_overlayScrollbarVertical.get());

        // Ensure this class is set as the scroll layer's ScrollableArea.
        coordinator->scrollableAreaScrollLayerDidChange(this);

        // Setup the inner viewport overlay scrollbars.
        setupScrollbar(WebScrollbar::Horizontal);
        setupScrollbar(WebScrollbar::Vertical);
    }

    m_innerViewportScrollLayer->removeAllChildren();
    m_innerViewportScrollLayer->addChild(currentLayerTreeRoot);

    // We only need to disable the existing (outer viewport) scrollbars
    // if the existing ones are already overlay.
    // FIXME: If we knew in advance before the overflowControlsHostLayer goes
    // away, we would re-enable the drawing of these scrollbars.
    // FIXME: This doesn't seem to work (at least on Android). Commenting out for now until
    // I figure out how to access RenderLayerCompositor from here.
    // if (GraphicsLayer* scrollbar = m_frameHost->compositor()->layerForHorizontalScrollbar())
    //    scrollbar->setDrawsContent(!page->mainFrame()->view()->hasOverlayScrollbars());
    // if (GraphicsLayer* scrollbar = m_frameHost->compositor()->layerForVerticalScrollbar())
    //    scrollbar->setDrawsContent(!page->mainFrame()->view()->hasOverlayScrollbars());
}

void PinchViewport::setupScrollbar(WebScrollbar::Orientation orientation)
{
    bool isHorizontal = orientation == WebScrollbar::Horizontal;
    GraphicsLayer* scrollbarGraphicsLayer = isHorizontal ?
        m_overlayScrollbarHorizontal.get() : m_overlayScrollbarVertical.get();
    OwnPtr<WebScrollbarLayer>& webScrollbarLayer = isHorizontal ?
        m_webOverlayScrollbarHorizontal : m_webOverlayScrollbarVertical;

    const int overlayScrollbarThickness = m_frameHost.settings().pinchOverlayScrollbarThickness();

    if (!webScrollbarLayer) {
        ScrollingCoordinator* coordinator = m_frameHost.page().scrollingCoordinator();
        ASSERT(coordinator);
        ScrollbarOrientation webcoreOrientation = isHorizontal ? HorizontalScrollbar : VerticalScrollbar;
        webScrollbarLayer = coordinator->createSolidColorScrollbarLayer(webcoreOrientation, overlayScrollbarThickness, 0, false);

        webScrollbarLayer->setClipLayer(m_innerViewportContainerLayer->platformLayer());
        scrollbarGraphicsLayer->setContentsToPlatformLayer(webScrollbarLayer->layer());
        scrollbarGraphicsLayer->setDrawsContent(false);
    }

    int xPosition = isHorizontal ? 0 : m_innerViewportContainerLayer->size().width() - overlayScrollbarThickness;
    int yPosition = isHorizontal ? m_innerViewportContainerLayer->size().height() - overlayScrollbarThickness : 0;
    int width = isHorizontal ? m_innerViewportContainerLayer->size().width() - overlayScrollbarThickness : overlayScrollbarThickness;
    int height = isHorizontal ? overlayScrollbarThickness : m_innerViewportContainerLayer->size().height() - overlayScrollbarThickness;

    // Use the GraphicsLayer to position the scrollbars.
    scrollbarGraphicsLayer->setPosition(IntPoint(xPosition, yPosition));
    scrollbarGraphicsLayer->setSize(IntSize(width, height));
    scrollbarGraphicsLayer->setContentsRect(IntRect(0, 0, width, height));
}

void PinchViewport::registerLayersWithTreeView(WebLayerTreeView* layerTreeView) const
{
    TRACE_EVENT0("blink", "PinchViewport::registerLayersWithTreeView");
    ASSERT(layerTreeView);
    ASSERT(m_frameHost.page().mainFrame());
    ASSERT(m_frameHost.page().mainFrame()->isLocalFrame());
    ASSERT(m_frameHost.page().deprecatedLocalMainFrame()->contentRenderer());

    RenderLayerCompositor* compositor = m_frameHost.page().deprecatedLocalMainFrame()->contentRenderer()->compositor();
    // Get the outer viewport scroll layer.
    WebLayer* scrollLayer = compositor->scrollLayer()->platformLayer();

    m_webOverlayScrollbarHorizontal->setScrollLayer(scrollLayer);
    m_webOverlayScrollbarVertical->setScrollLayer(scrollLayer);

    ASSERT(compositor);
    layerTreeView->registerViewportLayers(
        m_pageScaleLayer->platformLayer(),
        m_innerViewportScrollLayer->platformLayer(),
        scrollLayer);
}

void PinchViewport::clearLayersForTreeView(WebLayerTreeView* layerTreeView) const
{
    ASSERT(layerTreeView);

    layerTreeView->clearViewportLayers();
}

int PinchViewport::scrollSize(ScrollbarOrientation orientation) const
{
    IntSize scrollDimensions = maximumScrollPosition() - minimumScrollPosition();
    return (orientation == HorizontalScrollbar) ? scrollDimensions.width() : scrollDimensions.height();
}

IntPoint PinchViewport::minimumScrollPosition() const
{
    return IntPoint();
}

IntPoint PinchViewport::maximumScrollPosition() const
{
    return flooredIntPoint(FloatSize(contentsSize()) - visibleRect().size());
}

IntRect PinchViewport::scrollableAreaBoundingBox() const
{
    // This method should return the bounding box in the parent view's coordinate
    // space; however, PinchViewport technically isn't a child of any Frames.
    // Nonetheless, the PinchViewport always occupies the entire main frame so just
    // return that.
    LocalFrame* frame = mainFrame();

    if (!frame || !frame->view())
        return IntRect();

    return frame->view()->frameRect();
}

IntSize PinchViewport::contentsSize() const
{
    LocalFrame* frame = mainFrame();

    if (!frame || !frame->view())
        return IntSize();

    ASSERT(frame->view()->visibleContentScaleFactor() == 1);
    return frame->view()->visibleContentRect(IncludeScrollbars).size();
}

void PinchViewport::invalidateScrollbarRect(Scrollbar*, const IntRect&)
{
    // Do nothing. Pinch scrollbars live on the compositor thread and will
    // be updated when the viewport is synced to the CC.
}

void PinchViewport::setScrollOffset(const IntPoint& offset)
{
    setLocation(offset);
}

GraphicsLayer* PinchViewport::layerForContainer() const
{
    return m_innerViewportContainerLayer.get();
}

GraphicsLayer* PinchViewport::layerForScrolling() const
{
    return m_innerViewportScrollLayer.get();
}

GraphicsLayer* PinchViewport::layerForHorizontalScrollbar() const
{
    return m_overlayScrollbarHorizontal.get();
}

GraphicsLayer* PinchViewport::layerForVerticalScrollbar() const
{
    return m_overlayScrollbarVertical.get();
}

void PinchViewport::notifyAnimationStarted(const GraphicsLayer*, double monotonicTime)
{
}

void PinchViewport::paintContents(const GraphicsLayer*, GraphicsContext&, GraphicsLayerPaintingPhase, const IntRect& inClip)
{
}

LocalFrame* PinchViewport::mainFrame() const
{
    return m_frameHost.page().mainFrame() && m_frameHost.page().mainFrame()->isLocalFrame() ? m_frameHost.page().deprecatedLocalMainFrame() : 0;
}

FloatPoint PinchViewport::clampOffsetToBoundaries(const FloatPoint& offset)
{
    FloatPoint clampedOffset(offset);
    clampedOffset = clampedOffset.shrunkTo(FloatPoint(maximumScrollPosition()));
    clampedOffset = clampedOffset.expandedTo(FloatPoint(minimumScrollPosition()));
    return clampedOffset;
}

String PinchViewport::debugName(const GraphicsLayer* graphicsLayer)
{
    String name;
    if (graphicsLayer == m_innerViewportContainerLayer.get()) {
        name = "Inner Viewport Container Layer";
    } else if (graphicsLayer == m_pageScaleLayer.get()) {
        name =  "Page Scale Layer";
    } else if (graphicsLayer == m_innerViewportScrollLayer.get()) {
        name =  "Inner Viewport Scroll Layer";
    } else if (graphicsLayer == m_overlayScrollbarHorizontal.get()) {
        name =  "Overlay Scrollbar Horizontal Layer";
    } else if (graphicsLayer == m_overlayScrollbarVertical.get()) {
        name =  "Overlay Scrollbar Vertical Layer";
    } else {
        ASSERT_NOT_REACHED();
    }

    return name;
}

} // namespace blink
