/* -*- 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 <sal/config.h>

#include <basegfx/matrix/b2dhommatrixtools.hxx>
#include <basegfx/polygon/b2dpolygonclipper.hxx>
#include <comphelper/scopeguard.hxx>
#include <utility>

#include "surface.hxx"

namespace canvas
{
    Surface::Surface( PageManagerSharedPtr         rPageManager,
                      std::shared_ptr<IColorBuffer> xColorBuffer,
                      const ::basegfx::B2IPoint&   rPos,
                      const ::basegfx::B2ISize&    rSize ) :
        mpColorBuffer(std::move(xColorBuffer)),
        mpPageManager(std::move(rPageManager)),
        maSourceOffset(rPos),
        maSize(rSize),
        mbIsDirty(true)
    {
    }

    Surface::~Surface()
    {
        if(mpFragment)
            mpPageManager->free(mpFragment);
    }

    void Surface::setColorBufferDirty()
    {
        mbIsDirty=true;
    }

    basegfx::B2DRectangle Surface::getUVCoords() const
    {
        ::basegfx::B2ISize aPageSize(mpPageManager->getPageSize());
        ::basegfx::B2IPoint aDestOffset;
        if( mpFragment )
            aDestOffset = mpFragment->getPos();

        const double pw( aPageSize.getWidth() );
        const double ph( aPageSize.getHeight() );
        const double ox( aDestOffset.getX() );
        const double oy( aDestOffset.getY() );
        const double sx( maSize.getWidth() );
        const double sy( maSize.getHeight() );

        return ::basegfx::B2DRectangle( ox/pw,
                                        oy/ph,
                                        (ox+sx)/pw,
                                        (oy+sy)/ph );
    }

    basegfx::B2DRectangle Surface::getUVCoords( const ::basegfx::B2IPoint& rPos,
                                                const ::basegfx::B2ISize&  rSize ) const
    {
        ::basegfx::B2ISize aPageSize(mpPageManager->getPageSize());

        const double pw( aPageSize.getWidth() );
        const double ph( aPageSize.getHeight() );
        const double ox( rPos.getX() );
        const double oy( rPos.getY() );
        const double sx( rSize.getWidth() );
        const double sy( rSize.getHeight() );

        return ::basegfx::B2DRectangle( ox/pw,
                                        oy/ph,
                                        (ox+sx)/pw,
                                        (oy+sy)/ph );
    }

    bool Surface::draw( double                          fAlpha,
                        const ::basegfx::B2DPoint&      rPos,
                        const ::basegfx::B2DHomMatrix&  rTransform )
    {
        std::shared_ptr<IRenderModule> pRenderModule(mpPageManager->getRenderModule());

        RenderModuleGuard aGuard( pRenderModule );

        prepareRendering();

        // convert size to normalized device coordinates
        const ::basegfx::B2DRectangle aUV( getUVCoords() );

        const double u1(aUV.getMinX());
        const double v1(aUV.getMinY());
        const double u2(aUV.getMaxX());
        const double v2(aUV.getMaxY());

        // concat transforms
        // 1) offset of surface subarea
        // 2) surface transform
        // 3) translation to output position [rPos]
        // 4) scale to normalized device coordinates
        // 5) flip y-axis
        // 6) translate to account for viewport transform
        basegfx::B2DHomMatrix aTransform(basegfx::utils::createTranslateB2DHomMatrix(
            maSourceOffset.getX(), maSourceOffset.getY()));
        aTransform = aTransform * rTransform;
        aTransform.translate(rPos);

        /*
            ######################################
            ######################################
            ######################################

                           Y
                           ^+1
                           |
                   2       |       3
                     x------------x
                     |     |      |
                      |    |      |
               ------|-----O------|------>X
               -1    |     |      |     +1
                     |     |      |
                     x------------x
                    1      |       0
                            |
                           |-1

            ######################################
            ######################################
            ######################################
        */

        const ::basegfx::B2DPoint p0(aTransform * ::basegfx::B2DPoint(maSize.getWidth(),maSize.getHeight()));
        const ::basegfx::B2DPoint p1(aTransform * ::basegfx::B2DPoint(0.0,maSize.getHeight()));
        const ::basegfx::B2DPoint p2(aTransform * ::basegfx::B2DPoint(0.0,0.0));
        const ::basegfx::B2DPoint p3(aTransform * ::basegfx::B2DPoint(maSize.getWidth(),0.0));

        canvas::Vertex vertex;
        vertex.r = 1.0f;
        vertex.g = 1.0f;
        vertex.b = 1.0f;
        vertex.a = static_cast<float>(fAlpha);
        vertex.z = 0.0f;

        {
            pRenderModule->beginPrimitive( canvas::IRenderModule::PrimitiveType::Quad );

            // issue an endPrimitive() when leaving the scope
            const ::comphelper::ScopeGuard aScopeGuard(
                [&pRenderModule]() mutable { pRenderModule->endPrimitive(); } );

            vertex.u=static_cast<float>(u2); vertex.v=static_cast<float>(v2);
            vertex.x=static_cast<float>(p0.getX()); vertex.y=static_cast<float>(p0.getY());
            pRenderModule->pushVertex(vertex);

            vertex.u=static_cast<float>(u1); vertex.v=static_cast<float>(v2);
            vertex.x=static_cast<float>(p1.getX()); vertex.y=static_cast<float>(p1.getY());
            pRenderModule->pushVertex(vertex);

            vertex.u=static_cast<float>(u1); vertex.v=static_cast<float>(v1);
            vertex.x=static_cast<float>(p2.getX()); vertex.y=static_cast<float>(p2.getY());
            pRenderModule->pushVertex(vertex);

            vertex.u=static_cast<float>(u2); vertex.v=static_cast<float>(v1);
            vertex.x=static_cast<float>(p3.getX()); vertex.y=static_cast<float>(p3.getY());
            pRenderModule->pushVertex(vertex);
        }

        return !(pRenderModule->isError());
    }

    bool Surface::drawRectangularArea(
                        double                         fAlpha,
                        const ::basegfx::B2DPoint&     rPos,
                        const ::basegfx::B2DRectangle& rArea,
                        const ::basegfx::B2DHomMatrix& rTransform )
    {
        if( rArea.isEmpty() )
            return true; // immediate exit for empty area

        std::shared_ptr<IRenderModule> pRenderModule(mpPageManager->getRenderModule());

        RenderModuleGuard aGuard( pRenderModule );

        prepareRendering();

        // these positions are relative to the texture
        ::basegfx::B2IPoint aPos1(
            ::basegfx::fround(rArea.getMinimum().getX()),
            ::basegfx::fround(rArea.getMinimum().getY()));
        ::basegfx::B2IPoint aPos2(
            ::basegfx::fround(rArea.getMaximum().getX()),
            ::basegfx::fround(rArea.getMaximum().getY()) );

        // clip the positions to the area this surface covers
        aPos1.setX(std::max(aPos1.getX(), maSourceOffset.getX()));
        aPos1.setY(std::max(aPos1.getY(), maSourceOffset.getY()));
        aPos2.setX(std::min(aPos2.getX(), maSourceOffset.getX() + maSize.getWidth()));
        aPos2.setY(std::min(aPos2.getY(), maSourceOffset.getY() + maSize.getHeight()));

        // if the resulting area is empty, return immediately
        ::basegfx::B2IVector aSize(aPos2 - aPos1);
        if(aSize.getX() <= 0 || aSize.getY() <= 0)
            return true;

        ::basegfx::B2IPoint aDestOffset;
        if( mpFragment )
            aDestOffset = mpFragment->getPos();

        // convert size to normalized device coordinates
        const ::basegfx::B2DRectangle aUV(
            getUVCoords(aPos1 - maSourceOffset + aDestOffset,
                        basegfx::B2ISize(aSize.getX(), aSize.getY())) );
        const double u1(aUV.getMinX());
        const double v1(aUV.getMinY());
        const double u2(aUV.getMaxX());
        const double v2(aUV.getMaxY());

        // concatenate transforms
        // 1) offset of surface subarea
        // 2) surface transform
        // 3) translation to output position [rPos]
        basegfx::B2DHomMatrix aTransform(basegfx::utils::createTranslateB2DHomMatrix(aPos1.getX(), aPos1.getY()));
        aTransform = aTransform * rTransform;
        aTransform.translate(rPos);


        /*
            ######################################
            ######################################
            ######################################

                           Y
                           ^+1
                           |
                   2       |       3
                     x------------x
                     |     |      |
                      |    |      |
               ------|-----O------|------>X
               -1    |     |      |     +1
                     |     |      |
                     x------------x
                    1      |       0
                            |
                           |-1

            ######################################
            ######################################
            ######################################
        */

        const ::basegfx::B2DPoint p0(aTransform * ::basegfx::B2DPoint(aSize.getX(),aSize.getY()));
        const ::basegfx::B2DPoint p1(aTransform * ::basegfx::B2DPoint(0.0,         aSize.getY()));
        const ::basegfx::B2DPoint p2(aTransform * ::basegfx::B2DPoint(0.0,         0.0));
        const ::basegfx::B2DPoint p3(aTransform * ::basegfx::B2DPoint(aSize.getX(),0.0));

        canvas::Vertex vertex;
        vertex.r = 1.0f;
        vertex.g = 1.0f;
        vertex.b = 1.0f;
        vertex.a = static_cast<float>(fAlpha);
        vertex.z = 0.0f;

        {
            pRenderModule->beginPrimitive( canvas::IRenderModule::PrimitiveType::Quad );

            // issue an endPrimitive() when leaving the scope
            const ::comphelper::ScopeGuard aScopeGuard(
                [&pRenderModule]() mutable { pRenderModule->endPrimitive(); } );

            vertex.u=static_cast<float>(u2); vertex.v=static_cast<float>(v2);
            vertex.x=static_cast<float>(p0.getX()); vertex.y=static_cast<float>(p0.getY());
            pRenderModule->pushVertex(vertex);

            vertex.u=static_cast<float>(u1); vertex.v=static_cast<float>(v2);
            vertex.x=static_cast<float>(p1.getX()); vertex.y=static_cast<float>(p1.getY());
            pRenderModule->pushVertex(vertex);

            vertex.u=static_cast<float>(u1); vertex.v=static_cast<float>(v1);
            vertex.x=static_cast<float>(p2.getX()); vertex.y=static_cast<float>(p2.getY());
            pRenderModule->pushVertex(vertex);

            vertex.u=static_cast<float>(u2); vertex.v=static_cast<float>(v1);
            vertex.x=static_cast<float>(p3.getX()); vertex.y=static_cast<float>(p3.getY());
            pRenderModule->pushVertex(vertex);
        }

        return !(pRenderModule->isError());
    }

    bool Surface::drawWithClip( double                          fAlpha,
                                const ::basegfx::B2DPoint&      rPos,
                                const ::basegfx::B2DPolygon&    rClipPoly,
                                const ::basegfx::B2DHomMatrix&  rTransform )
    {
        std::shared_ptr<IRenderModule> pRenderModule(mpPageManager->getRenderModule());

        RenderModuleGuard aGuard( pRenderModule );

        prepareRendering();

        // untransformed surface rectangle, relative to the whole
        // image (note: this surface might actually only be a tile of
        // the whole image, with non-zero maSourceOffset)
        const double x1(maSourceOffset.getX());
        const double y1(maSourceOffset.getY());
        const double w(maSize.getWidth());
        const double h(maSize.getHeight());
        const double x2(x1+w);
        const double y2(y1+h);
        const ::basegfx::B2DRectangle aSurfaceClipRect(x1,y1,x2,y2);

        // concatenate transforms
        // we use 'fround' here to avoid rounding errors. the vertices will
        // be transformed by the overall transform and uv coordinates will
        // be calculated from the result, and this is why we need to use
        // integer coordinates here...
        basegfx::B2DHomMatrix aTransform = rTransform;
        aTransform.translate(rPos);

        /*
            ######################################
            ######################################
            ######################################

                           Y
                           ^+1
                           |
                   2       |       3
                     x------------x
                     |     |      |
                      |    |      |
               ------|-----O------|------>X
               -1    |     |      |     +1
                     |     |      |
                     x------------x
                    1      |       0
                            |
                           |-1

            ######################################
            ######################################
            ######################################
        */

        // uv coordinates that map the surface rectangle
        // to the destination rectangle.
        const ::basegfx::B2DRectangle aUV( getUVCoords() );

        basegfx::B2DPolygon rTriangleList(basegfx::utils::clipTriangleListOnRange(rClipPoly,
                                                                                  aSurfaceClipRect));

        // Push vertices to backend renderer
        if(const sal_uInt32 nVertexCount = rTriangleList.count())
        {
            canvas::Vertex vertex;
            vertex.r = 1.0f;
            vertex.g = 1.0f;
            vertex.b = 1.0f;
            vertex.a = static_cast<float>(fAlpha);
            vertex.z = 0.0f;

            pRenderModule->beginPrimitive( canvas::IRenderModule::PrimitiveType::Triangle );

            // issue an endPrimitive() when leaving the scope
            const ::comphelper::ScopeGuard aScopeGuard(
                [&pRenderModule]() mutable { pRenderModule->endPrimitive(); } );

            for(sal_uInt32 nIndex=0; nIndex<nVertexCount; ++nIndex)
            {
                const basegfx::B2DPoint &aPoint = rTriangleList.getB2DPoint(nIndex);
                basegfx::B2DPoint aTransformedPoint(aTransform * aPoint);
                const double tu(((aPoint.getX()-aSurfaceClipRect.getMinX())*aUV.getWidth()/w)+aUV.getMinX());
                const double tv(((aPoint.getY()-aSurfaceClipRect.getMinY())*aUV.getHeight()/h)+aUV.getMinY());
                vertex.u=static_cast<float>(tu);
                vertex.v=static_cast<float>(tv);
                vertex.x=static_cast<float>(aTransformedPoint.getX());
                vertex.y=static_cast<float>(aTransformedPoint.getY());
                pRenderModule->pushVertex(vertex);
            }
        }

        return !(pRenderModule->isError());
    }

    void Surface::prepareRendering()
    {
        mpPageManager->validatePages();

        // clients requested to draw from this surface, therefore one
        // of the above implemented concrete rendering operations
        // was triggered. we therefore need to ask the pagemanager
        // to allocate some space for the fragment we're dedicated to.
        if(!mpFragment)
        {
            mpFragment = mpPageManager->allocateSpace(maSize);
            if( mpFragment )
            {
                mpFragment->setColorBuffer(mpColorBuffer);
                mpFragment->setSourceOffset(maSourceOffset);
            }
        }

        if( mpFragment )
        {
            // now we need to 'select' the fragment, which will in turn
            // pull information from the image on demand.
            // in case this fragment is still not located on any of the
            // available pages ['naked'], we force the page manager to
            // do it now, no way to defer this any longer...
            if(!(mpFragment->select(mbIsDirty)))
                mpPageManager->nakedFragment(mpFragment);

        }
        mbIsDirty=false;
    }
}

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