/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * This file incorporates work covered by the following license notice:
 *
 *   Licensed to the Apache Software Foundation (ASF) under one or more
 *   contributor license agreements. See the NOTICE file distributed
 *   with this work for additional information regarding copyright
 *   ownership. The ASF licenses this file to you under the Apache
 *   License, Version 2.0 (the "License"); you may not use this file
 *   except in compliance with the License. You may obtain a copy of
 *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
 */

#include <sdr/overlay/overlaymanagerbuffered.hxx>
#include <svx/sdrpaintwindow.hxx>
#include <vcl/outdev.hxx>
#include <basegfx/range/b2drange.hxx>
#include <vcl/window.hxx>
#include <tools/fract.hxx>
#include <vcl/cursor.hxx>


namespace sdr::overlay
{
        void OverlayManagerBuffered::ImpPrepareBufferDevice()
        {
            // compare size of mpBufferDevice with size of visible area
            if(mpBufferDevice->GetOutputSizePixel() != getOutputDevice().GetOutputSizePixel())
            {
                // set new buffer size, copy as much content as possible (use bool parameter for vcl).
                // Newly uncovered regions will be repainted.
                mpBufferDevice->SetOutputSizePixel(getOutputDevice().GetOutputSizePixel(), false);
            }

            // compare the MapModes for zoom/scroll changes
            if(mpBufferDevice->GetMapMode() != getOutputDevice().GetMapMode())
            {
                const bool bZoomed(
                    mpBufferDevice->GetMapMode().GetScaleX() != getOutputDevice().GetMapMode().GetScaleX()
                    || mpBufferDevice->GetMapMode().GetScaleY() != getOutputDevice().GetMapMode().GetScaleY());

                if(!bZoomed)
                {
                    const Point& rOriginOld = mpBufferDevice->GetMapMode().GetOrigin();
                    const Point& rOriginNew = getOutputDevice().GetMapMode().GetOrigin();
                    const bool bScrolled(rOriginOld != rOriginNew);


                    if(bScrolled)
                    {
                        // get pixel bounds (tdf#149322 do subtraction in logic units before converting result back to pixel)
                        const Point aLogicOriginDiff(rOriginNew - rOriginOld);
                        const Size aPixelOriginDiff(mpBufferDevice->LogicToPixel(Size(aLogicOriginDiff.X(), aLogicOriginDiff.Y())));
                        const Point aDestinationOffsetPixel(aPixelOriginDiff.Width(), aPixelOriginDiff.Height());
                        const Size aOutputSizePixel(mpBufferDevice->GetOutputSizePixel());

                        // remember and switch off MapMode
                        const bool bMapModeWasEnabled(mpBufferDevice->IsMapModeEnabled());
                        mpBufferDevice->EnableMapMode(false);

                        // scroll internally buffered stuff
                        mpBufferDevice->DrawOutDev(
                            aDestinationOffsetPixel, aOutputSizePixel, // destination
                            Point(), aOutputSizePixel); // source

                        // restore MapMode
                        mpBufferDevice->EnableMapMode(bMapModeWasEnabled);

                        // scroll remembered region, too.
                        if(!maBufferRememberedRangePixel.isEmpty())
                        {
                            const basegfx::B2IPoint aIPointDestinationOffsetPixel(aDestinationOffsetPixel.X(), aDestinationOffsetPixel.Y());
                            const basegfx::B2IPoint aNewMinimum(maBufferRememberedRangePixel.getMinimum() + aIPointDestinationOffsetPixel);
                            const basegfx::B2IPoint aNewMaximum(maBufferRememberedRangePixel.getMaximum() + aIPointDestinationOffsetPixel);
                            maBufferRememberedRangePixel = basegfx::B2IRange(aNewMinimum, aNewMaximum);
                        }
                    }
                }

                // copy new MapMode
                mpBufferDevice->SetMapMode(getOutputDevice().GetMapMode());
            }

            // #i29186#
            mpBufferDevice->SetDrawMode(getOutputDevice().GetDrawMode());
            mpBufferDevice->SetSettings(getOutputDevice().GetSettings());
            mpBufferDevice->SetAntialiasing(getOutputDevice().GetAntialiasing());
        }

        void OverlayManagerBuffered::ImpRestoreBackground() const
        {
            const tools::Rectangle aRegionRectanglePixel(
                maBufferRememberedRangePixel.getMinX(), maBufferRememberedRangePixel.getMinY(),
                maBufferRememberedRangePixel.getMaxX(), maBufferRememberedRangePixel.getMaxY());
            const vcl::Region aRegionPixel(aRegionRectanglePixel);

            ImpRestoreBackground(aRegionPixel);
        }

        void OverlayManagerBuffered::ImpRestoreBackground(const vcl::Region& rRegionPixel) const
        {
            // MapModes off
            const bool bMapModeWasEnabledDest(getOutputDevice().IsMapModeEnabled());
            const bool bMapModeWasEnabledSource(mpBufferDevice->IsMapModeEnabled());
            getOutputDevice().EnableMapMode(false);
            const_cast<OverlayManagerBuffered*>(this)->mpBufferDevice->EnableMapMode(false);

            // local region
            RectangleVector aRectangles;
            rRegionPixel.GetRegionRectangles(aRectangles);

            for(const auto& rRect : aRectangles)
            {
                // restore the area
                const Point aTopLeft(rRect.TopLeft());
                const Size aSize(rRect.GetSize());

                getOutputDevice().DrawOutDev(
                    aTopLeft, aSize, // destination
                    aTopLeft, aSize, // source
                    *mpBufferDevice);
            }

            // restore MapModes
            getOutputDevice().EnableMapMode(bMapModeWasEnabledDest);
            const_cast<OverlayManagerBuffered*>(this)->mpBufferDevice->EnableMapMode(bMapModeWasEnabledSource);
        }

        void OverlayManagerBuffered::ImpSaveBackground(const vcl::Region& rRegion, OutputDevice* pPreRenderDevice)
        {
            // prepare source
            OutputDevice& rSource = pPreRenderDevice ? *pPreRenderDevice : getOutputDevice();

            // Ensure buffer is valid
            ImpPrepareBufferDevice();

            // build region which needs to be copied
            vcl::Region aRegion(rSource.LogicToPixel(rRegion));

            // limit to PaintRegion if it's a window. This will be evtl. the expanded one,
            // but always the exact redraw area
            if(OUTDEV_WINDOW == rSource.GetOutDevType())
            {
                vcl::Window& rWindow = *rSource.GetOwnerWindow();
                vcl::Region aPaintRegionPixel = rWindow.LogicToPixel(rWindow.GetPaintRegion());
                aRegion.Intersect(aPaintRegionPixel);

                // #i72754# Make sure content is completely rendered, the window
                // will be used as source of a DrawOutDev soon
                rWindow.GetOutDev()->Flush();
            }

            // also limit to buffer size
            const tools::Rectangle aBufferDeviceRectanglePixel(Point(), mpBufferDevice->GetOutputSizePixel());
            aRegion.Intersect(aBufferDeviceRectanglePixel);

            // MapModes off
            const bool bMapModeWasEnabledDest(rSource.IsMapModeEnabled());
            const bool bMapModeWasEnabledSource(mpBufferDevice->IsMapModeEnabled());
            rSource.EnableMapMode(false);
            mpBufferDevice->EnableMapMode(false);

            // prepare to iterate over the rectangles from the region in pixels
            RectangleVector aRectangles;
            aRegion.GetRegionRectangles(aRectangles);

            for(const auto& rRect : aRectangles)
            {
                // for each rectangle, save the area
                const Point aTopLeft(rRect.TopLeft());
                const Size aSize(rRect.GetSize());

                mpBufferDevice->DrawOutDev(
                    aTopLeft, aSize, // destination
                    aTopLeft, aSize, // source
                    rSource);
            }

            // restore MapModes
            rSource.EnableMapMode(bMapModeWasEnabledDest);
            mpBufferDevice->EnableMapMode(bMapModeWasEnabledSource);
        }

        IMPL_LINK_NOARG(OverlayManagerBuffered, ImpBufferTimerHandler, Timer*, void)
        {
            //Resolves: fdo#46728 ensure this exists until end of scope
            rtl::Reference<OverlayManager> xKeepAlive(this);

            // stop timer
            maBufferIdle.Stop();

            if(maBufferRememberedRangePixel.isEmpty())
                // nothing to refresh, done
                return;

            if (maMapModeLastCompleteRedraw == MapMode())
                // there was no CompleteRedraw yet, done
                return;

            // in calc i stumbled over the situation that MapModes for OutDevs
            // were different for ::CompleteRedraw and this timer-based refresh,
            // probably due to Calc's massive MapMode changes - as soon as an
            // overlay invalidate happened the next reschedule in calc *will*
            // trigger this refresh, despite what MapMode may be set at the
            // target device currently.
            // These updates only make sense in combination with the saved
            // background, so this is an error (and happens only in Calc due
            // to these always changing MapModes).
            // Thus it is plausible to remember the MapMode of the last full
            // CompleteRedraw and force local usage to it.
            const MapMode aPrevMapMode(getOutputDevice().GetMapMode());
            const bool bPatchMapMode(aPrevMapMode != maMapModeLastCompleteRedraw);
            if (bPatchMapMode)
                getOutputDevice().SetMapMode(maMapModeLastCompleteRedraw);

            // logic size for impDrawMember call
            basegfx::B2DRange aBufferRememberedRangeLogic(
                maBufferRememberedRangePixel.getMinX(), maBufferRememberedRangePixel.getMinY(),
                maBufferRememberedRangePixel.getMaxX(), maBufferRememberedRangePixel.getMaxY());
            aBufferRememberedRangeLogic.transform(getOutputDevice().GetInverseViewTransformation());

            // prepare cursor handling
            const bool bTargetIsWindow(OUTDEV_WINDOW == mrOutputDevice.GetOutDevType());
            bool bCursorWasEnabled(false);

            // #i80730# switch off VCL cursor during overlay refresh
            if(bTargetIsWindow)
            {
                vcl::Window& rWindow = *mrOutputDevice.GetOwnerWindow();
                vcl::Cursor* pCursor = rWindow.GetCursor();

                if(pCursor && pCursor->IsVisible())
                {
                    pCursor->Hide();
                    bCursorWasEnabled = true;
                }
            }

            // refresh with prerendering
            {
                // #i73602# ensure valid and sized mpOutputBufferDevice
                const Size aDestinationSizePixel(mpBufferDevice->GetOutputSizePixel());
                const Size aOutputBufferSizePixel(mpOutputBufferDevice->GetOutputSizePixel());

                if(aDestinationSizePixel != aOutputBufferSizePixel)
                {
                    mpOutputBufferDevice->SetOutputSizePixel(aDestinationSizePixel);
                }

                mpOutputBufferDevice->SetMapMode(getOutputDevice().GetMapMode());
                mpOutputBufferDevice->EnableMapMode(false);
                mpOutputBufferDevice->SetDrawMode(mpBufferDevice->GetDrawMode());
                mpOutputBufferDevice->SetSettings(mpBufferDevice->GetSettings());
                mpOutputBufferDevice->SetAntialiasing(mpBufferDevice->GetAntialiasing());

                // calculate sizes
                tools::Rectangle aRegionRectanglePixel(
                    maBufferRememberedRangePixel.getMinX(), maBufferRememberedRangePixel.getMinY(),
                    maBufferRememberedRangePixel.getMaxX(), maBufferRememberedRangePixel.getMaxY());

                // truncate aRegionRectanglePixel to destination pixel size, more does
                // not need to be prepared since destination is a buffer for a window. So,
                // maximum size indirectly shall be limited to getOutputDevice().GetOutputSizePixel()
                if(aRegionRectanglePixel.Left() < 0)
                {
                    aRegionRectanglePixel.SetLeft( 0 );
                }

                if(aRegionRectanglePixel.Top() < 0)
                {
                    aRegionRectanglePixel.SetTop( 0 );
                }

                if(aRegionRectanglePixel.Right() > aDestinationSizePixel.getWidth())
                {
                    aRegionRectanglePixel.SetRight( aDestinationSizePixel.getWidth() );
                }

                if(aRegionRectanglePixel.Bottom() > aDestinationSizePixel.getHeight())
                {
                    aRegionRectanglePixel.SetBottom( aDestinationSizePixel.getHeight() );
                }

                // get sizes
                const Point aTopLeft(aRegionRectanglePixel.TopLeft());
                const Size aSize(aRegionRectanglePixel.GetSize());

                {
                    const bool bMapModeWasEnabledDest(mpBufferDevice->IsMapModeEnabled());
                    mpBufferDevice->EnableMapMode(false);

                    mpOutputBufferDevice->DrawOutDev(
                        aTopLeft, aSize, // destination
                        aTopLeft, aSize, // source
                        *mpBufferDevice);

                    // restore MapModes
                    mpBufferDevice->EnableMapMode(bMapModeWasEnabledDest);
                }

                // paint overlay content for remembered region, use
                // method from base class directly
                mpOutputBufferDevice->EnableMapMode();
                OverlayManager::ImpDrawMembers(aBufferRememberedRangeLogic, *mpOutputBufferDevice);
                mpOutputBufferDevice->EnableMapMode(false);

                // copy to output
                {
                    const bool bMapModeWasEnabledDest(getOutputDevice().IsMapModeEnabled());
                    getOutputDevice().EnableMapMode(false);

                    getOutputDevice().DrawOutDev(
                        aTopLeft, aSize, // destination
                        aTopLeft, aSize, // source
                        *mpOutputBufferDevice);

                    // debug
                    /*getOutputDevice().SetLineCOL_RED);
                    getOutputDevice().SetFillColor();
                    getOutputDevice().DrawRect(Rectangle(aTopLeft, aSize));*/

                    // restore MapModes
                    getOutputDevice().EnableMapMode(bMapModeWasEnabledDest);
                }
            }

            // VCL hack for transparent child windows
            // Problem is e.g. a radiobutton form control in life mode. The used window
            // is a transparence vcl childwindow. This flag only allows the parent window to
            // paint into the child windows area, but there is no mechanism which takes
            // care for a repaint of the child window. A transparent child window is NOT
            // a window which always keeps it's content consistent over the parent, but it's
            // more like just a paint flag for the parent.
            // To get the update, the windows in question are updated manually here.
            if(bTargetIsWindow)
            {
                vcl::Window& rWindow = *mrOutputDevice.GetOwnerWindow();

                const tools::Rectangle aRegionRectanglePixel(
                    maBufferRememberedRangePixel.getMinX(),
                    maBufferRememberedRangePixel.getMinY(),
                    maBufferRememberedRangePixel.getMaxX(),
                    maBufferRememberedRangePixel.getMaxY());
                PaintTransparentChildren(rWindow, aRegionRectanglePixel);
            }

            // #i80730# restore visibility of VCL cursor
            if(bCursorWasEnabled)
            {
                vcl::Window& rWindow = *mrOutputDevice.GetOwnerWindow();
                vcl::Cursor* pCursor = rWindow.GetCursor();

                if(pCursor)
                {
                    // check if cursor still exists. It may have been deleted from someone
                    pCursor->Show();
                }
            }

            // forget remembered Region
            maBufferRememberedRangePixel.reset();

            // restore evtl. temporarily patched MapMode (see explanation
            // above)
            if (bPatchMapMode)
                getOutputDevice().SetMapMode(aPrevMapMode);
        }

        OverlayManagerBuffered::OverlayManagerBuffered(
            OutputDevice& rOutputDevice)
        :   OverlayManager(rOutputDevice),
            mpBufferDevice(VclPtr<VirtualDevice>::Create()),
            mpOutputBufferDevice(VclPtr<VirtualDevice>::Create()),
            maBufferIdle( "sdr::overlay::OverlayManagerBuffered maBufferIdle" ),
            maMapModeLastCompleteRedraw()
        {
            // Init timer
            maBufferIdle.SetPriority( TaskPriority::POST_PAINT );
            maBufferIdle.SetInvokeHandler(LINK(this, OverlayManagerBuffered, ImpBufferTimerHandler));
        }

        rtl::Reference<OverlayManager> OverlayManagerBuffered::create(
            OutputDevice& rOutputDevice)
        {
            return rtl::Reference<OverlayManager>(new OverlayManagerBuffered(rOutputDevice));
        }

        OverlayManagerBuffered::~OverlayManagerBuffered()
        {
            // Clear timer
            maBufferIdle.Stop();

            if(!maBufferRememberedRangePixel.isEmpty())
            {
                // Restore all rectangles for remembered region from buffer
                ImpRestoreBackground();
            }
        }

        void OverlayManagerBuffered::completeRedraw(const vcl::Region& rRegion, OutputDevice* pPreRenderDevice) const
        {
            // remember MapMode of last full completeRedraw to
            // have it available in timer-based updates (see
            // ImpBufferTimerHandler)
            maMapModeLastCompleteRedraw = getOutputDevice().GetMapMode();

            if(!rRegion.IsEmpty())
            {
                // save new background
                const_cast<OverlayManagerBuffered*>(this)->ImpSaveBackground(rRegion, pPreRenderDevice);
            }

            // call parent
            OverlayManager::completeRedraw(rRegion, pPreRenderDevice);
        }

        void OverlayManagerBuffered::invalidateRange(const basegfx::B2DRange& rRange)
        {
            if(rRange.isEmpty())
                return;

            if (maMapModeLastCompleteRedraw == MapMode())
                // there was no CompleteRedraw yet, done
                return;

            // buffered output, do not invalidate but use the timer
            // to trigger a timer event for refresh
            maBufferIdle.Start();

            // tdf#166520 need to also use MapModeLastCompleteRedraw for
            // the invalidates -> Calc is just completely unstable using
            // MapModes at any Window. The one remembered at full
            // repaint has to be the DrawingLayer one - overlay works
            // in DrawingLayer coordinates/MapMode(s)
            const MapMode aPrevMapMode(getOutputDevice().GetMapMode());
            const bool bPatchMapMode(aPrevMapMode != maMapModeLastCompleteRedraw);
            if (bPatchMapMode)
                getOutputDevice().SetMapMode(maMapModeLastCompleteRedraw);

            // add the discrete range to the remembered region
            // #i75163# use double precision and floor/ceil rounding to get overlapped pixel region, even
            // when the given logic region has a width/height of 0.0. This does NOT work with LogicToPixel
            // since it just transforms the top left and bottom right points equally without taking
            // discrete pixel coverage into account. An empty B2DRange and thus empty logic Rectangle translated
            // to an also empty discrete pixel rectangle - what is wrong.
            basegfx::B2DRange aDiscreteRange(rRange);
            aDiscreteRange.transform(getOutputDevice().GetViewTransformation());

            if(getCurrentViewInformation2D().getUseAntiAliasing())
            {
                // assume AA needs one pixel more and invalidate one pixel more
                const double fDiscreteOne(getDiscreteOne());
                const basegfx::B2IPoint aTopLeft(
                    static_cast<sal_Int32>(floor(aDiscreteRange.getMinX() - fDiscreteOne)),
                    static_cast<sal_Int32>(floor(aDiscreteRange.getMinY() - fDiscreteOne)));
                const basegfx::B2IPoint aBottomRight(
                    static_cast<sal_Int32>(ceil(aDiscreteRange.getMaxX() + fDiscreteOne)),
                    static_cast<sal_Int32>(ceil(aDiscreteRange.getMaxY() + fDiscreteOne)));

                maBufferRememberedRangePixel.expand(aTopLeft);
                maBufferRememberedRangePixel.expand(aBottomRight);
            }
            else
            {
                const basegfx::B2IPoint aTopLeft(static_cast<sal_Int32>(floor(aDiscreteRange.getMinX())), static_cast<sal_Int32>(floor(aDiscreteRange.getMinY())));
                const basegfx::B2IPoint aBottomRight(static_cast<sal_Int32>(ceil(aDiscreteRange.getMaxX())), static_cast<sal_Int32>(ceil(aDiscreteRange.getMaxY())));

                maBufferRememberedRangePixel.expand(aTopLeft);
                maBufferRememberedRangePixel.expand(aBottomRight);
            }

            // restore evtl. temporarily patched MapMode (see explanation
            // above)
            if (bPatchMapMode)
                getOutputDevice().SetMapMode(aPrevMapMode);
        }
} // end of namespace

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
