/* -*- 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 <o3tl/safeint.hxx>
#include <osl/endian.h>
#include <filter/importcgm.hxx>
#include <tools/stream.hxx>
#include "bitmap.hxx"
#include "chart.hxx"
#include "elements.hxx"
#include "outact.hxx"
#include <memory>
#include <sal/log.hxx>
#include <comphelper/diagnose_ex.hxx>

using namespace ::com::sun::star;

constexpr double gnOutdx = 28000;                // Output size in 1/100TH mm
constexpr double gnOutdy = 21000;                // on which is mapped

CGM::CGM(uno::Reference< frame::XModel > const & rModel)
    : mnVDCXadd(0)
    , mnVDCYadd(0)
    , mnVDCXmul(0)
    , mnVDCYmul(0)
    , mnVDCdx(0)
    , mnVDCdy(0)
    , mnXFraction(0)
    , mnYFraction(0)
    , mbAngReverse(false)
    , mbStatus(true)
    , mbMetaFile(false)
    , mbIsFinished(false)
    , mbPicture(false)
    , mbPictureBody(false)
    , mbFigure(false)
    , mbFirstOutPut(false)
    , mbInDefaultReplacement(false)
    , mnAct4PostReset(0)
    , mnBitmapInserts(0)
    , mpOutAct(new CGMImpressOutAct(*this, rModel))
    , mpSource(nullptr)
    , mpEndValidSource(nullptr)
    , mnParaSize(0)
    , mnActCount(0)
    , mnEscape(0)
    , mnElementClass(0)
    , mnElementID(0)
    , mnElementSize(0)
{
    pElement.reset( new CGMElements );
    pCopyOfE.reset( new CGMElements );
}

CGM::~CGM()
{
    maDefRepList.clear();
    maDefRepSizeList.clear();
};

sal_uInt32 CGM::GetBackGroundColor() const
{
    return pElement ? pElement->aColorTable[ 0 ] : 0;
}

sal_uInt32 CGM::ImplGetUI16()
{
    sal_uInt8* pSource = mpSource + mnParaSize;
    if (mpEndValidSource - pSource < 2)
        throw css::uno::Exception(u"attempt to read past end of input"_ustr, nullptr);
    mnParaSize += 2;
    return ( pSource[ 0 ] << 8 ) +  pSource[ 1 ];
};

sal_uInt8 CGM::ImplGetByte( sal_uInt32 nSource, sal_uInt32 nPrecision )
{
    return static_cast<sal_uInt8>( nSource >> ( ( nPrecision - 1 ) << 3 ) );
};

sal_Int32 CGM::ImplGetI( sal_uInt32 nPrecision )
{
    sal_uInt8* pSource = mpSource + mnParaSize;
    if (pSource > mpEndValidSource || o3tl::make_unsigned(mpEndValidSource - pSource) < nPrecision)
        throw css::uno::Exception(u"attempt to read past end of input"_ustr, nullptr);
    mnParaSize += nPrecision;
    switch( nPrecision )
    {
        case 1 :
        {
            return  static_cast<char>(*pSource);
        }

        case 2 :
        {
            return static_cast<sal_Int16>( ( pSource[ 0 ] << 8 ) | pSource[ 1 ] );
        }

        case 3 :
        {
            return ( ( pSource[ 0 ] << 24 ) | ( pSource[ 1 ] << 16 ) | pSource[ 2 ] << 8 ) >> 8;
        }
        case 4:
        {
            return static_cast<sal_Int32>( ( pSource[ 0 ] << 24 ) | ( pSource[ 1 ] << 16 ) | ( pSource[ 2 ] << 8 ) | ( pSource[ 3 ] ) );
        }
        default:
            mbStatus = false;
            return 0;
    }
}

sal_uInt32 CGM::ImplGetUI( sal_uInt32 nPrecision )
{
    sal_uInt8* pSource = mpSource + mnParaSize;
    if (pSource > mpEndValidSource || o3tl::make_unsigned(mpEndValidSource - pSource) < nPrecision)
        throw css::uno::Exception(u"attempt to read past end of input"_ustr, nullptr);
    mnParaSize += nPrecision;
    switch( nPrecision )
    {
        case 1 :
            return  static_cast<sal_Int8>(*pSource);
        case 2 :
        {
            return static_cast<sal_uInt16>( ( pSource[ 0 ] << 8 ) | pSource[ 1 ] );
        }
        case 3 :
        {
            return ( pSource[ 0 ] << 16 ) | ( pSource[ 1 ] << 8 ) | pSource[ 2 ];
        }
        case 4:
        {
            return static_cast<sal_uInt32>( ( pSource[ 0 ] << 24 ) | ( pSource[ 1 ] << 16 ) | ( pSource[ 2 ] << 8 ) | ( pSource[ 3 ] ) );
        }
        default:
            mbStatus = false;
            return 0;
    }
}

void CGM::ImplGetSwitch4( const sal_uInt8* pSource, sal_uInt8* pDest )
{
    for ( int i = 0; i < 4; i++ )
    {
        pDest[ i ] = pSource[ i ^ 3 ];          // Little Endian <-> Big Endian switch
    }
}

void CGM::ImplGetSwitch8( const sal_uInt8* pSource, sal_uInt8* pDest )
{
    for ( int i = 0; i < 8; i++ )
    {
        pDest[ i ] = pSource[ i ^ 7 ];          // Little Endian <-> Big Endian switch
    }
}

double CGM::ImplGetFloat( RealPrecision eRealPrecision, sal_uInt32 nRealSize )
{
    void*   pPtr;
    sal_uInt8   aBuf[8];
    double  nRetValue;
    double  fDoubleBuf;
    float   fFloatBuf;

#ifdef OSL_BIGENDIAN
    const bool bCompatible = true;
#else
    const bool bCompatible = false;
#endif

    if (o3tl::make_unsigned(mpEndValidSource - (mpSource + mnParaSize)) < nRealSize)
        throw css::uno::Exception(u"attempt to read past end of input"_ustr, nullptr);

    if ( bCompatible )
    {
        pPtr = mpSource + mnParaSize;
    }
    else
    {
        if ( nRealSize == 4 )
            ImplGetSwitch4( mpSource + mnParaSize, &aBuf[0] );
        else
            ImplGetSwitch8( mpSource + mnParaSize, &aBuf[0] );
        pPtr = &aBuf;
    }
    if ( eRealPrecision == RP_FLOAT )
    {
        if ( nRealSize == 4 )
        {
            memcpy( static_cast<void*>(&fFloatBuf), pPtr, 4 );
            nRetValue = static_cast<double>(fFloatBuf);
        }
        else
        {
            memcpy( static_cast<void*>(&fDoubleBuf), pPtr, 8 );
            nRetValue = fDoubleBuf;
        }
    }
    else // ->RP_FIXED
    {
        tools::Long    nVal;
        const int nSwitch = bCompatible ? 0 : 1 ;
        if ( nRealSize == 4 )
        {
            sal_uInt16* pShort = static_cast<sal_uInt16*>(pPtr);
            nVal = pShort[ nSwitch ];
            nVal <<= 16;
            nVal |= pShort[ nSwitch ^ 1 ];
            nRetValue = static_cast<double>(nVal);
            nRetValue /= 65536;
        }
        else
        {
            sal_Int32* pLong = static_cast<sal_Int32*>(pPtr);
            nRetValue = fabs(static_cast<double>(pLong[nSwitch]));
            nRetValue *= 65536;
            nVal = static_cast<sal_uInt32>( pLong[ nSwitch ^ 1 ] );
            nVal >>= 16;
            nRetValue += static_cast<double>(nVal);
            if ( pLong[ nSwitch ] < 0 )
            {
                nRetValue = -nRetValue;
            }
            nRetValue /= 65536;
        }
    }
    mnParaSize += nRealSize;
    return nRetValue;
}

sal_uInt32 CGM::ImplGetPointSize()
{
    if ( pElement->eVDCType == VDC_INTEGER )
        return pElement->nVDCIntegerPrecision << 1;
    else
        return pElement->nVDCRealSize << 1;
}

inline double CGM::ImplGetIX()
{
    return ( ( ImplGetI( pElement->nVDCIntegerPrecision ) + mnVDCXadd ) * mnVDCXmul );
}

inline double CGM::ImplGetFX()
{
    return ( ( ImplGetFloat( pElement->eVDCRealPrecision, pElement->nVDCRealSize ) + mnVDCXadd ) * mnVDCXmul );
}

inline double CGM::ImplGetIY()
{
    return ( ( ImplGetI( pElement->nVDCIntegerPrecision ) + mnVDCYadd ) * mnVDCYmul );
}

inline double CGM::ImplGetFY()
{
    return ( ( ImplGetFloat( pElement->eVDCRealPrecision, pElement->nVDCRealSize ) + mnVDCYadd ) * mnVDCYmul );
}

void CGM::ImplGetPoint( FloatPoint& rFloatPoint, bool bMap )
{
    if ( pElement->eVDCType == VDC_INTEGER )
    {
        rFloatPoint.X = ImplGetIX();
        rFloatPoint.Y = ImplGetIY();
    }
    else // ->floating points
    {
        rFloatPoint.X = ImplGetFX();
        rFloatPoint.Y = ImplGetFY();
    }
    if ( bMap )
        ImplMapPoint( rFloatPoint );
}

void CGM::ImplGetRectangle( FloatRect& rFloatRect, bool bMap )
{
    if ( pElement->eVDCType == VDC_INTEGER )
    {
        rFloatRect.Left = ImplGetIX();
        rFloatRect.Bottom = ImplGetIY();
        rFloatRect.Right = ImplGetIX();
        rFloatRect.Top = ImplGetIY();
    }
    else // ->floating points
    {
        rFloatRect.Left = ImplGetFX();
        rFloatRect.Bottom = ImplGetFY();
        rFloatRect.Right = ImplGetFX();
        rFloatRect.Top = ImplGetFY();
    }
    if ( bMap )
    {
        ImplMapX( rFloatRect.Left );
        ImplMapX( rFloatRect.Right );
        ImplMapY( rFloatRect.Top );
        ImplMapY( rFloatRect.Bottom );
        rFloatRect.Justify();
    }
}

void CGM::ImplGetRectangleNS( FloatRect& rFloatRect )
{
    if ( pElement->eVDCType == VDC_INTEGER )
    {
        rFloatRect.Left = ImplGetI( pElement->nVDCIntegerPrecision );
        rFloatRect.Bottom = ImplGetI( pElement->nVDCIntegerPrecision );
        rFloatRect.Right = ImplGetI( pElement->nVDCIntegerPrecision );
        rFloatRect.Top = ImplGetI( pElement->nVDCIntegerPrecision );
    }
    else // ->floating points
    {
        rFloatRect.Left = ImplGetFloat( pElement->eVDCRealPrecision, pElement->nVDCRealSize );
        rFloatRect.Bottom = ImplGetFloat( pElement->eVDCRealPrecision, pElement->nVDCRealSize );
        rFloatRect.Right = ImplGetFloat( pElement->eVDCRealPrecision, pElement->nVDCRealSize );
        rFloatRect.Top = ImplGetFloat( pElement->eVDCRealPrecision, pElement->nVDCRealSize );
    }
}

sal_uInt32 CGM::ImplGetBitmapColor( bool bDirect )
{
    // the background color is always a direct color

    sal_uInt32  nTmp;
    if ( ( pElement->eColorSelectionMode == CSM_DIRECT ) || bDirect )
    {
        sal_uInt32      nColor = ImplGetByte( ImplGetUI( pElement->nColorPrecision ), 1 );
        sal_uInt32      nDiff = pElement->nColorValueExtent[ 3 ] - pElement->nColorValueExtent[ 0 ] + 1;

        if ( !nDiff )
            nDiff++;
        nColor = ( ( nColor - pElement->nColorValueExtent[ 0 ] ) << 8 ) / nDiff;
        nTmp = nColor << 16 & 0xff0000;

        nColor = ImplGetByte( ImplGetUI( pElement->nColorPrecision ), 1 );
        nDiff = pElement->nColorValueExtent[ 4 ] - pElement->nColorValueExtent[ 1 ] + 1;
        if ( !nDiff )
            nDiff++;
        nColor = ( ( nColor - pElement->nColorValueExtent[ 1 ] ) << 8 ) / nDiff;
        nTmp |= nColor << 8 & 0xff00;

        nColor = ImplGetByte( ImplGetUI( pElement->nColorPrecision ), 1 );
        nDiff = pElement->nColorValueExtent[ 5 ] - pElement->nColorValueExtent[ 2 ] + 1;
        if ( !nDiff )
            nDiff++;
        nColor = ( ( nColor - pElement->nColorValueExtent[ 2 ] ) << 8 ) / nDiff;
        nTmp |= static_cast<sal_uInt8>(nColor);
    }
    else
    {
        sal_uInt32 nIndex = ImplGetUI( pElement->nColorIndexPrecision );
        nTmp = pElement->aColorTable[ static_cast<sal_uInt8>(nIndex) ] ;
    }
    return nTmp;
}

// call this function each time after the mapmode settings has been changed
void CGM::ImplSetMapMode()
{
    int nAngReverse = 1;
    mnVDCdx = pElement->aVDCExtent.Right - pElement->aVDCExtent.Left;

    mnVDCXadd = -pElement->aVDCExtent.Left;
    mnVDCXmul = 1;
    if ( mnVDCdx < 0 )
    {
        nAngReverse ^= 1;
        mnVDCdx = -mnVDCdx;
        mnVDCXmul = -1;
    }

    mnVDCdy = pElement->aVDCExtent.Bottom - pElement->aVDCExtent.Top;
    mnVDCYadd = -pElement->aVDCExtent.Top;
    mnVDCYmul = 1;
    if ( mnVDCdy < 0 )
    {
        nAngReverse ^= 1;
        mnVDCdy = -mnVDCdy;
        mnVDCYmul = -1;
    }
    if ( nAngReverse )
        mbAngReverse = true;
    else
        mbAngReverse = false;

    if (mnVDCdy == 0.0 || mnVDCdx == 0.0 || gnOutdy == 0.0)
    {
        mbStatus = false;
        return;
    }

    double fQuo1 = mnVDCdx / mnVDCdy;
    double fQuo2 = gnOutdx / gnOutdy;
    if ( fQuo2 < fQuo1 )
    {
        mnXFraction = gnOutdx / mnVDCdx;
        mnYFraction = gnOutdy * ( fQuo2 / fQuo1 ) / mnVDCdy;
    }
    else
    {
        mnXFraction = gnOutdx * ( fQuo1 / fQuo2 ) / mnVDCdx;
        mnYFraction = gnOutdy / mnVDCdy;
    }
}

void CGM::ImplMapDouble( double& nNumb )
{
    if ( pElement->eDeviceViewPortMap != DVPM_FORCED )
        return;

    // point is 1mm * ScalingFactor
    switch ( pElement->eDeviceViewPortMode )
    {
        case DVPM_FRACTION :
        {
            nNumb *= ( mnXFraction + mnYFraction ) / 2;
        }
        break;

        case DVPM_METRIC :
        {
            // nNumb *= ( 100 * pElement->nDeviceViewPortScale );
            nNumb *= ( mnXFraction + mnYFraction ) / 2;
            if ( pElement->nDeviceViewPortScale < 0 )
                nNumb = -nNumb;
        }
        break;

        case DVPM_DEVICE :
        {

        }
        break;

        default:

            break;
    }
}

void CGM::ImplMapX( double& nNumb )
{
    if ( pElement->eDeviceViewPortMap != DVPM_FORCED )
        return;

    // point is 1mm * ScalingFactor
    switch ( pElement->eDeviceViewPortMode )
    {
        case DVPM_FRACTION :
        {
            nNumb *= mnXFraction;
        }
        break;

        case DVPM_METRIC :
        {
            // nNumb *= ( 100 * pElement->nDeviceViewPortScale );
            nNumb *= mnXFraction;
            if ( pElement->nDeviceViewPortScale < 0 )
                nNumb = -nNumb;
        }
        break;

        case DVPM_DEVICE :
        {

        }
        break;

        default:

            break;
    }
}

void CGM::ImplMapY( double& nNumb )
{
    if ( pElement->eDeviceViewPortMap != DVPM_FORCED )
        return;

    // point is 1mm * ScalingFactor
    switch ( pElement->eDeviceViewPortMode )
    {
        case DVPM_FRACTION :
        {
            nNumb *= mnYFraction;
        }
        break;

        case DVPM_METRIC :
        {
            // nNumb *= ( 100 * pElement->nDeviceViewPortScale );
            nNumb *= mnYFraction;
            if ( pElement->nDeviceViewPortScale < 0 )
                nNumb = -nNumb;
        }
        break;

        case DVPM_DEVICE :
        {

        }
        break;

        default:

            break;
    }
}

// convert a point to the current VC mapmode (1/100TH mm)
void CGM::ImplMapPoint( FloatPoint& rFloatPoint )
{
    if ( pElement->eDeviceViewPortMap != DVPM_FORCED )
        return;

    // point is 1mm * ScalingFactor
    switch ( pElement->eDeviceViewPortMode )
    {
        case DVPM_FRACTION :
        {
            rFloatPoint.X *= mnXFraction;
            rFloatPoint.Y *= mnYFraction;
        }
        break;

        case DVPM_METRIC :
        {
            rFloatPoint.X *= mnXFraction;
            rFloatPoint.Y *= mnYFraction;
            if ( pElement->nDeviceViewPortScale < 0 )
            {
                rFloatPoint.X = -rFloatPoint.X;
                rFloatPoint.Y = -rFloatPoint.Y;
            }
        }
        break;

        case DVPM_DEVICE :
        {

        }
        break;

        default:

            break;
    }
}

void CGM::ImplDoClass()
{
    switch ( mnElementClass )
    {
        case 0 : ImplDoClass0(); break;
        case 1 : ImplDoClass1(); break;
        case 2 : ImplDoClass2(); break;
        case 3 : ImplDoClass3(); break;
        case 4 :
        {
            ImplDoClass4();
            mnAct4PostReset = 0;
        }
        break;
        case 5 : ImplDoClass5(); break;
        case 6 : ImplDoClass6(); break;
        case 7 : ImplDoClass7(); break;
        case 8 : ImplDoClass8(); break;
        case 9 : ImplDoClass9(); break;
        case 15 :ImplDoClass15(); break;
        default: break;
    }
    mnActCount++;
};

void CGM::ImplDefaultReplacement()
{
    if (maDefRepList.empty())
        return;

    if (mbInDefaultReplacement)
    {
        SAL_WARN("filter.icgm", "recursion in ImplDefaultReplacement");
        return;
    }

    mbInDefaultReplacement = true;

    sal_uInt32  nOldEscape = mnEscape;
    sal_uInt32  nOldElementClass = mnElementClass;
    sal_uInt32  nOldElementID = mnElementID;
    sal_uInt32  nOldElementSize = mnElementSize;
    sal_uInt8*  pOldBuf = mpSource;
    sal_uInt8*  pOldEndValidSource = mpEndValidSource;

    for ( size_t i = 0, n = maDefRepList.size(); i < n; ++i )
    {
        sal_uInt8*  pBuf = maDefRepList[ i ].get();
        sal_uInt32  nElementSize = maDefRepSizeList[ i ];
        mpEndValidSource = pBuf + nElementSize;
        sal_uInt32  nCount = 0;
        while ( mbStatus && ( nCount < nElementSize ) )
        {
            mpSource = pBuf + nCount;
            mnParaSize = 0;
            mnEscape = ImplGetUI16();
            mnElementClass = mnEscape >> 12;
            mnElementID = ( mnEscape & 0x0fe0 ) >> 5;
            mnElementSize = mnEscape & 0x1f;
            if ( mnElementSize == 31 )
            {
                mnElementSize = ImplGetUI16();
            }
            nCount += mnParaSize;
            mnParaSize = 0;
            mpSource = pBuf + nCount;
            if ( mnElementSize & 1 )
                nCount++;
            nCount += mnElementSize;
            if ( ( mnElementClass != 1 ) || ( mnElementID != 0xc ) )    // recursion is not possible here!!
                ImplDoClass();
        }
    }
    mnEscape = nOldEscape;
    mnElementClass = nOldElementClass;
    mnElementID = nOldElementID;
    mnParaSize = mnElementSize = nOldElementSize;
    mpSource = pOldBuf;
    mpEndValidSource = pOldEndValidSource;

    mbInDefaultReplacement = false;
}

bool CGM::Write( SvStream& rIStm )
{
    if ( !mpBuf )
        mpBuf.reset( new sal_uInt8[ 0xffff ] );

    mnParaSize = 0;
    mpSource = mpBuf.get();
    if (rIStm.ReadBytes(mpSource, 2) != 2)
        return false;
    mpEndValidSource = mpSource + 2;
    mnEscape = ImplGetUI16();
    mnElementClass = mnEscape >> 12;
    mnElementID = ( mnEscape & 0x0fe0 ) >> 5;
    mnElementSize = mnEscape & 0x1f;

    if ( mnElementSize == 31 )
    {
        if (rIStm.ReadBytes(mpSource + mnParaSize, 2) != 2)
            return false;
        mpEndValidSource = mpSource + mnParaSize + 2;
        mnElementSize = ImplGetUI16();
    }
    mnParaSize = 0;
    if (mnElementSize)
    {
        if (rIStm.ReadBytes(mpSource, mnElementSize) != mnElementSize)
            return false;
        mpEndValidSource = mpSource + mnElementSize;
    }

    if ( mnElementSize & 1 )
        rIStm.SeekRel( 1 );
    ImplDoClass();

    return mbStatus;
};

// GraphicImport - the exported function
FILTER_DLLPUBLIC sal_uInt32
ImportCGM(SvStream& rIn, uno::Reference< frame::XModel > const & rXModel, css::uno::Reference<css::task::XStatusIndicator> const & aXStatInd)
{

    sal_uInt32  nStatus = 0;            // retvalue == 0 -> ERROR
                                        //          == 0xffrrggbb -> background color in the lower 24 bits

    if( rXModel.is() )
    {
        try
        {
            CGM aCGM(rXModel);
            if (aCGM.IsValid())
            {
                rIn.SetEndian(SvStreamEndian::BIG);
                sal_uInt64 const nInSize = rIn.remainingSize();
                rIn.Seek(0);

                sal_uInt64  nNext = 0;
                sal_uInt32  nAdd = nInSize / 20;
                bool bProgressBar = aXStatInd.is();
                if ( bProgressBar )
                    aXStatInd->start( u"CGM Import"_ustr , nInSize );

                while (aCGM.IsValid() && (rIn.Tell() < nInSize) && !aCGM.IsFinished())
                {
                    if ( bProgressBar )
                    {
                        sal_uInt64 nCurrentPos = rIn.Tell();
                        if ( nCurrentPos >= nNext )
                        {
                            aXStatInd->setValue( nCurrentPos );
                            nNext = nCurrentPos + nAdd;
                        }
                    }

                    if (!aCGM.Write(rIn))
                        break;
                }
                if ( aCGM.IsValid() )
                {
                    nStatus = aCGM.GetBackGroundColor() | 0xff000000;
                }
                if ( bProgressBar )
                    aXStatInd->end();
            }
        }
        catch (const css::uno::Exception&)
        {
            TOOLS_WARN_EXCEPTION("filter.icgm", "");
            nStatus = 0;
        }
    }
    return nStatus;
}

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