/* -*- 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 <filter/XpmReader.hxx>

#include <vcl/graph.hxx>
#include <tools/stream.hxx>

#include <vcl/filter/ImportOutput.hxx>
#include <vcl/BitmapWriteAccess.hxx>

#include "rgbtable.hxx"

#include <cstring>
#include <array>
#include <map>

#define XPMTEMPBUFSIZE      0x00008000
#define XPMSTRINGBUF        0x00008000

#define XPMIDENTIFIER       0x00000001          // mnIdentifier includes one of the six phases
#define XPMDEFINITION       0x00000002          // the XPM format consists of
#define XPMVALUES           0x00000003
#define XPMCOLORS           0x00000004
#define XPMPIXELS           0x00000005
#define XPMEXTENSIONS       0x00000006
#define XPMENDEXT           0x00000007

#define XPMREMARK           0x00000001          // defines used by mnStatus
#define XPMDOUBLE           0x00000002
#define XPMSTRING           0x00000004
#define XPMFINISHED         0x00000008

namespace {

enum ReadState
{
    XPMREAD_OK,
    XPMREAD_ERROR
};

}

class BitmapWriteAccess;
class Graphic;

namespace
{

class XPMReader
{
private:
    SvStream& mrStream;
    Bitmap maBitmap;
    BitmapScopedWriteAccess mpWriterAccess;
    Bitmap maMaskBitmap;
    BitmapScopedWriteAccess mpMaskWriterAccess;
    sal_uInt64 mnLastPos;

    tools::Long mnWidth = 0;
    tools::Long mnHeight = 0;
    sal_uLong mnColors = 0;
    sal_uInt32 mnCpp = 0; // characters per pix
    bool mbTransparent = false;
    bool mbStatus  = true;
    sal_uLong mnStatus = 0;
    sal_uLong mnIdentifier = XPMIDENTIFIER;
    sal_uInt8 mcThisByte = 0;
    sal_uInt8 mcLastByte = 0;
    sal_uLong mnTempAvail = 0;
    sal_uInt8* mpTempBuf = nullptr;
    sal_uInt8* mpTempPtr = nullptr;
    // each key is ( mnCpp )Byte(s)-> ASCII entry assigned to the colour
    // each colordata is
    // 1 Byte   -> 0xFF if colour is transparent
    // 3 Bytes  -> RGB value of the colour
    typedef std::array<sal_uInt8, 4> ColorData;
    typedef std::map<OString, ColorData> ColorMap;
    ColorMap maColMap;
    sal_uLong mnStringSize = 0;
    sal_uInt8* mpStringBuf = nullptr;
    sal_uLong mnParaSize = 0;
    sal_uInt8* mpPara = nullptr;

    bool                    ImplGetString();
    bool                    ImplGetColor();
    bool                    ImplGetScanLine( sal_uLong );
    bool                    ImplGetColSub(ColorData &rDest);
    bool                    ImplGetColKey( sal_uInt8 );
    void                    ImplGetRGBHex(ColorData &rDest, sal_uLong);
    bool                    ImplGetPara( sal_uLong numb );
    static bool             ImplCompare(sal_uInt8 const *, sal_uInt8 const *, sal_uLong);
    sal_uLong               ImplGetULONG( sal_uLong nPara );

public:
    explicit XPMReader(SvStream& rStream);

    ReadState ReadXPM(Bitmap& rBitmap);
};

}

XPMReader::XPMReader(SvStream& rStream)
    : mrStream(rStream)
    , mnLastPos(rStream.Tell())
{
}

ReadState XPMReader::ReadXPM(Bitmap& rBitmap)
{
    if (!mrStream.good())
        return XPMREAD_ERROR;

    ReadState eReadState = XPMREAD_ERROR;

    mrStream.Seek( mnLastPos );
    mbStatus = true;

    if ( mbStatus )
    {
        mpStringBuf = new sal_uInt8 [ XPMSTRINGBUF ];
        mpTempBuf = new sal_uInt8 [ XPMTEMPBUFSIZE ];

        mbStatus = ImplGetString();
        if ( mbStatus )
        {
            mnIdentifier = XPMVALUES;           // fetch Bitmap information
            mnWidth = ImplGetULONG( 0 );
            mnHeight = ImplGetULONG( 1 );
            mnColors = ImplGetULONG( 2 );
            mnCpp = ImplGetULONG( 3 );
        }
        if ( mnColors > ( SAL_MAX_UINT32 / ( 4 + mnCpp ) ) )
            mbStatus = false;
        if ( ( mnWidth * mnCpp ) >= XPMSTRINGBUF )
            mbStatus = false;
        //xpms are a minimum of one character (one byte) per pixel, so if the file isn't
        //even that long, it's not all there
        if (mrStream.remainingSize() + mnTempAvail < static_cast<sal_uInt64>(mnWidth) * mnHeight)
            mbStatus = false;
        if ( mbStatus && mnWidth && mnHeight && mnColors && mnCpp )
        {
            mnIdentifier = XPMCOLORS;

            for (sal_uLong i = 0; i < mnColors; ++i)
            {
                if (!ImplGetColor())
                {
                    mbStatus = false;
                    break;
                }
            }

            if ( mbStatus )
            {
                // create a 24bit graphic when more as 256 colours present
                auto ePixelFormat = vcl::PixelFormat::INVALID;
                if ( mnColors > 256 )
                    ePixelFormat = vcl::PixelFormat::N24_BPP;
                else
                    ePixelFormat = vcl::PixelFormat::N8_BPP;

                maBitmap = Bitmap(Size(mnWidth, mnHeight), ePixelFormat);
                mpWriterAccess = maBitmap;

                // mbTransparent is TRUE if at least one colour is transparent
                if ( mbTransparent )
                {
                    maMaskBitmap = Bitmap(Size(mnWidth, mnHeight), vcl::PixelFormat::N8_BPP, &Bitmap::GetGreyPalette(256));
                    mpMaskWriterAccess = maMaskBitmap;
                    if (!mpMaskWriterAccess)
                        mbStatus = false;
                }
                if (mpWriterAccess && mbStatus)
                {
                    if (mnColors <= 256)  // palette is only needed by using less than 257
                    {                     // colors
                        sal_uInt8 i = 0;
                        for (auto& elem : maColMap)
                        {
                            mpWriterAccess->SetPaletteColor(i, Color(elem.second[1], elem.second[2], elem.second[3]));
                            //reuse map entry, overwrite color with palette index
                            elem.second[1] = i;
                            i++;
                        }
                    }

                    // now we get the bitmap data
                    mnIdentifier = XPMPIXELS;
                    for (tools::Long i = 0; i < mnHeight; ++i)
                    {
                        if ( !ImplGetScanLine( i ) )
                        {
                            mbStatus = false;
                            break;
                        }
                    }
                    mnIdentifier = XPMEXTENSIONS;
                }
            }
        }

        delete[] mpStringBuf;
        delete[] mpTempBuf;

    }
    if( mbStatus )
    {
        mpWriterAccess.reset();
        if (mpMaskWriterAccess)
        {
            mpMaskWriterAccess.reset();
            rBitmap = Bitmap(maBitmap, maMaskBitmap);
        }
        else
        {
            rBitmap = maBitmap;
        }
        eReadState = XPMREAD_OK;
    }
    else
    {
        mpMaskWriterAccess.reset();
        mpWriterAccess.reset();
    }
    return eReadState;
}

// ImplGetColor returns various colour values,
// returns TRUE if various colours could be assigned
bool XPMReader::ImplGetColor()
{
    sal_uInt8*  pString = mpStringBuf;
    if (!ImplGetString())
        return false;

    if (mnStringSize < mnCpp)
        return false;

    OString aKey(reinterpret_cast<char*>(pString), mnCpp);
    ColorData aValue{0};
    bool bStatus = ImplGetColSub(aValue);
    if (bStatus)
    {
        maColMap[aKey] = aValue;
    }
    return bStatus;
}

// ImpGetScanLine reads the string mpBufSize and writes the pixel in the
// Bitmap. Parameter nY is the horizontal position.
bool XPMReader::ImplGetScanLine( sal_uLong nY )
{
    bool    bStatus = ImplGetString();
    sal_uInt8*  pString = mpStringBuf;
    BitmapColor     aWhite;
    BitmapColor     aBlack;

    if ( bStatus )
    {
        if (mpMaskWriterAccess)
        {
            aWhite = mpMaskWriterAccess->GetBestMatchingColor( COL_WHITE );
            aBlack = mpMaskWriterAccess->GetBestMatchingColor( COL_BLACK );
        }
        if (mnStringSize != (sal_uLong(mnWidth) * mnCpp))
            bStatus = false;
        else
        {
            Scanline pScanline = mpWriterAccess->GetScanline(nY);
            Scanline pMaskScanline = mpMaskWriterAccess ? mpMaskWriterAccess->GetScanline(nY) : nullptr;
            for (tools::Long i = 0; i < mnWidth; ++i)
            {
                OString aKey(reinterpret_cast<char*>(pString), mnCpp);
                auto it = maColMap.find(aKey);
                if (it != maColMap.end())
                {
                    if (mnColors > 256)
                        mpWriterAccess->SetPixelOnData(pScanline, i, Color(it->second[1], it->second[2], it->second[3]));
                    else
                        mpWriterAccess->SetPixelOnData(pScanline, i, BitmapColor(it->second[1]));
                    if (pMaskScanline)
                        mpMaskWriterAccess->SetPixelOnData(pMaskScanline, i, it->second[0] ? aWhite : aBlack);
                }
                pString += mnCpp;
            }
        }
    }
    return bStatus;
}

// tries to determine a colour value from mpStringBuf
// if a colour was found the RGB value is written a pDest[1]..pDest[2]
// pDest[0] contains 0xFF if the colour is transparent otherwise 0

bool XPMReader::ImplGetColSub(ColorData& rDest)
{
    unsigned char cTransparent[] = "None";

    bool bColStatus = false;

    if ( ImplGetColKey( 'c' ) || ImplGetColKey( 'm' ) || ImplGetColKey( 'g' ) )
    {
        // hexentry for RGB or HSV color ?
        if (*mpPara == '#')
        {
            rDest[0] = 0;
            bColStatus = true;
            switch ( mnParaSize )
            {
                case 25 :
                    ImplGetRGBHex(rDest, 6);
                    break;
                case 13 :
                    ImplGetRGBHex(rDest, 2);
                    break;
                case  7 :
                    ImplGetRGBHex(rDest, 0);
                    break;
                default:
                    bColStatus = false;
                    break;
            }
        }
        // maybe pixel is transparent
        else if ( ImplCompare( &cTransparent[0], mpPara, 4 ))
        {
            rDest[0] = 0xff;
            bColStatus = true;
            mbTransparent = true;
        }
        // last we will try to get the colorname
        else if ( mnParaSize > 2 )  // name must enlarge the minimum size
        {
            sal_uLong i = 0;
            while ( true )
            {
                if ( pRGBTable[ i ].name == nullptr )
                    break;
                if ( std::strlen(pRGBTable[i].name) > mnParaSize &&
                        pRGBTable[ i ].name[ mnParaSize ] == 0 )
                {
                    if ( ImplCompare ( reinterpret_cast<unsigned char const *>(pRGBTable[ i ].name),
                            mpPara, mnParaSize ) )
                    {
                        bColStatus = true;
                        rDest[0] = 0;
                        rDest[1] = pRGBTable[i].red;
                        rDest[2] = pRGBTable[i].green;
                        rDest[3] = pRGBTable[i].blue;
                        break;
                    }
                }
                i++;
            }
        }
    }
    return bColStatus;
}

// ImplGetColKey searches string mpStringBuf for a parameter 'nKey'
// and returns a boolean. (if TRUE mpPara and mnParaSize will be set)

bool XPMReader::ImplGetColKey( sal_uInt8 nKey )
{
    sal_uInt8 nTemp, nPrev = ' ';

    if (mnStringSize < mnCpp + 1)
        return false;

    mpPara = mpStringBuf + mnCpp + 1;
    mnParaSize = 0;

    while ( *mpPara != 0 )
    {
        if ( *mpPara == nKey )
        {
            nTemp = *( mpPara + 1 );
            if ( nTemp == ' ' || nTemp == 0x09 )
            {
                if ( nPrev == ' ' || nPrev == 0x09 )
                    break;
            }
        }
        nPrev = *mpPara;
        mpPara++;
    }
    if ( *mpPara )
    {
        mpPara++;
        while ( (*mpPara == ' ') || (*mpPara == 0x09) )
        {
            mpPara++;
        }
        if ( *mpPara != 0 )
        {
            while ( *(mpPara+mnParaSize) != ' ' && *(mpPara+mnParaSize) != 0x09 &&
                        *(mpPara+mnParaSize) != 0 )
            {
                mnParaSize++;
            }
        }
    }
    return mnParaSize != 0;
}

// ImplGetRGBHex translates the ASCII-Hexadecimalvalue belonging to mpPara
// in a RGB value and writes this to rDest
// below formats should be contained in mpPara:
// if nAdd = 0 : '#12ab12'                    -> RGB = 0x12, 0xab, 0x12
//           2 : '#1234abcd1234'                  "      "     "     "
//           6 : '#12345678abcdefab12345678'      "      "     "     "

void XPMReader::ImplGetRGBHex(ColorData& rDest, sal_uLong nAdd)
{
    sal_uInt8*  pPtr = mpPara+1;

    for (sal_uLong i = 1; i < 4; ++i)
    {
        sal_uInt8 nHex = (*pPtr++) - '0';
        if ( nHex > 9 )
            nHex = ((nHex - 'A' + '0') & 7) + 10;

        sal_uInt8 nTemp = (*pPtr++) - '0';
        if ( nTemp > 9 )
            nTemp = ((nTemp - 'A' + '0') & 7) + 10;
        nHex = ( nHex << 4 ) + nTemp;

        pPtr += nAdd;
        rDest[i] = nHex;
    }
}

// ImplGetUlong returns the value of a up to 6-digit long ASCII-decimal number.

sal_uLong XPMReader::ImplGetULONG( sal_uLong nPara )
{
    if ( ImplGetPara ( nPara ) )
    {
        sal_uLong nRetValue = 0;
        sal_uInt8* pPtr = mpPara;

        if ( ( mnParaSize > 6 ) || ( mnParaSize == 0 ) ) return 0;
        for ( sal_uLong i = 0; i < mnParaSize; i++ )
        {
            sal_uInt8 j = (*pPtr++) - 48;
            if ( j > 9 ) return 0;              // ascii is invalid
            nRetValue*=10;
            nRetValue+=j;
        }
        return nRetValue;
    }
    else return 0;
}

bool XPMReader::ImplCompare(sal_uInt8 const * pSource, sal_uInt8 const * pDest, sal_uLong nSize)
{
    for (sal_uLong i = 0; i < nSize; ++i)
    {
        if ( ( pSource[i]&~0x20 ) != ( pDest[i]&~0x20 ) )
        {
            return false;
        }
    }
    return true;
}

// ImplGetPara tries to retrieve nNumb (0...x) parameters from mpStringBuf.
// Parameters are separated by spaces or tabs.
// If a parameter was found then the return value is TRUE and mpPara + mnParaSize
// are set.

bool XPMReader::ImplGetPara ( sal_uLong nNumb )
{
    sal_uInt8   nByte;
    sal_uLong   nSize = 0;
    sal_uInt8*  pPtr = mpStringBuf;
    sal_uLong   nCount = 0;

    if ( ( *pPtr != ' ' ) && ( *pPtr != 0x09 ) )
    {
        mpPara = pPtr;
        mnParaSize = 0;
        nCount = 0;
    }
    else
    {
        mpPara = nullptr;
        nCount = 0xffffffff;
    }

    while ( nSize < mnStringSize )
    {
        nByte = *pPtr;

        if ( mpPara )
        {
            if ( ( nByte == ' ' ) || ( nByte == 0x09 ) )
            {
                if ( nCount == nNumb )
                    break;
                else
                    mpPara = nullptr;
            }
            else
                mnParaSize++;
        }
        else
        {
            if ( ( nByte != ' ' ) && ( nByte != 0x09 ) )
            {
                mpPara = pPtr;
                mnParaSize = 1;
                nCount++;
            }
        }
        nSize++;
        pPtr++;
    }
    return ( ( nCount == nNumb ) && mpPara );
}

// The next string is read and stored in mpStringBuf (terminated with 0);
// mnStringSize contains the size of the string read.
// Comments like '//' and '/*...*/' are skipped.

bool XPMReader::ImplGetString()
{
    sal_uInt8 const sID[] = "/* XPM */";
    sal_uInt8*      pString = mpStringBuf;

    mnStringSize = 0;
    mpStringBuf[0] = 0;

    while( mbStatus && ( mnStatus != XPMFINISHED ) )
    {
        if ( mnTempAvail == 0 )
        {
            mnTempAvail = mrStream.ReadBytes( mpTempBuf, XPMTEMPBUFSIZE );
            if ( mnTempAvail == 0 )
                break;

            mpTempPtr = mpTempBuf;

            if ( mnIdentifier == XPMIDENTIFIER )
            {
                if ( mnTempAvail <= 50 )
                {
                    mbStatus = false;   // file is too short to be a correct XPM format
                    break;
                }
                for ( int i = 0; i < 9; i++ )   // searching for "/* XPM */"
                    if ( *mpTempPtr++ != sID[i] )
                    {
                        mbStatus = false;
                        break;
                    }
                mnTempAvail-=9;
                mnIdentifier++;
            }
        }
        mcLastByte = mcThisByte;
        mcThisByte = *mpTempPtr++;
        mnTempAvail--;

        if ( mnStatus & XPMDOUBLE )
        {
            if ( mcThisByte == 0x0a )
                mnStatus &=~XPMDOUBLE;
            continue;
        }
        if ( mnStatus & XPMREMARK )
        {
            if ( ( mcThisByte == '/' )  && ( mcLastByte == '*' ) )
                mnStatus &=~XPMREMARK;
            continue;
        }
        if ( mnStatus & XPMSTRING )             // characters in string
        {
            if ( mcThisByte == '"' )
            {
                mnStatus &=~XPMSTRING;          // end of parameter by eol
                break;
            }
            if ( mnStringSize >= ( XPMSTRINGBUF - 1 ) )
            {
                mbStatus = false;
                break;
            }
            *pString++ = mcThisByte;
            pString[0] = 0;
            mnStringSize++;
            continue;
        }
        else
        {                                           // characters beside string
            switch ( mcThisByte )
            {
                case '*' :
                    if ( mcLastByte == '/' ) mnStatus |= XPMREMARK;
                    break;
                case '/' :
                    if ( mcLastByte == '/' ) mnStatus |= XPMDOUBLE;
                    break;
                case '"' : mnStatus |= XPMSTRING;
                    break;
                case '{' :
                    if ( mnIdentifier == XPMDEFINITION )
                        mnIdentifier++;
                    break;
                case '}' :
                    if ( mnIdentifier == XPMENDEXT )
                        mnStatus = XPMFINISHED;
                    break;
            }
        }
    }
    return mbStatus;
}


VCL_DLLPUBLIC bool ImportXPM(SvStream& rStream, ImportOutput& rImportOutput)
{
    XPMReader aXPMReader(rStream);

    Bitmap aBitmap;
    ReadState eReadState = aXPMReader.ReadXPM(aBitmap);

    if (eReadState == XPMREAD_ERROR)
        return false;
    rImportOutput.moBitmap = aBitmap;
    return true;
}

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