/* -*- 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/.
 */

#include <interpre.hxx>
#include <jumpmatrix.hxx>
#include <formulacell.hxx>
#include <scmatrix.hxx>
#include <rtl/strbuf.hxx>
#include <rtl/character.hxx>
#include <formula/errorcodes.hxx>
#include <sfx2/bindings.hxx>
#include <sfx2/linkmgr.hxx>
#include <tools/urlobj.hxx>

#include <officecfg/Office/Common.hxx>
#include <libxml/xpath.h>
#include <datastreamgettime.hxx>
#include <dpobject.hxx>
#include <document.hxx>
#include <tokenarray.hxx>
#include <webservicelink.hxx>

#include <sc.hrc>

#include <cstring>
#include <memory>
#include <string_view>
#include <libxml/parser.h>

using namespace com::sun::star;

// TODO: Add new methods for ScInterpreter here.

void ScInterpreter::ScFilterXML()
{
    sal_uInt8 nParamCount = GetByte();
    if (!MustHaveParamCount( nParamCount, 2 ) )
        return;

    SCSIZE nMatCols = 1, nMatRows = 1, nNode = 0;
    // In array/matrix context node elements' results are to be
    // subsequently stored. Check this before obtaining any argument from
    // the stack so the stack type can be used.
    if (pJumpMatrix || IsInArrayContext())
    {
        if (pJumpMatrix)
        {
            // Single result, GetString() will retrieve the corresponding
            // argument and JumpMatrix() will store it at the proper
            // position. Note that nMatCols and nMatRows are still 1.
            SCSIZE nCurCol = 0, nCurRow = 0;
            pJumpMatrix->GetPos( nCurCol, nCurRow);
            nNode = nCurRow;
        }
        else if (bMatrixFormula)
        {
            // If there is no formula cell then continue with a single
            // result.
            if (pMyFormulaCell)
            {
                SCCOL nCols;
                SCROW nRows;
                pMyFormulaCell->GetMatColsRows( nCols, nRows);
                nMatCols = nCols;
                nMatRows = nRows;
            }
        }
        else if (GetStackType() == formula::svMatrix)
        {
            const ScMatrix* pPathMatrix = pStack[sp-1]->GetMatrix();
            if (!pPathMatrix)
            {
                PushIllegalParameter();
                return;
            }
            pPathMatrix->GetDimensions( nMatCols, nMatRows);

            /* TODO: it is unclear what should happen if there are
             * different path arguments in matrix elements. We may have to
             * evaluate each, and for repeated identical paths use
             * subsequent nodes. As is, the path at 0,0 is used as obtained
             * by GetString(). */

        }
    }
    if (!nMatCols || !nMatRows)
    {
        PushNoValue();
        return;
    }

    OUString aXPathExpression = GetString().getString();
    OUString aString = GetString().getString();
    if(aString.isEmpty() || aXPathExpression.isEmpty())
    {
        PushError( FormulaError::NoValue );
        return;
    }

    OString aOXPathExpression = OUStringToOString( aXPathExpression, RTL_TEXTENCODING_UTF8 );
    const char* pXPathExpr = aOXPathExpression.getStr();
    OString aOString = OUStringToOString( aString, RTL_TEXTENCODING_UTF8 );
    const char* pXML = aOString.getStr();

    std::shared_ptr<xmlParserCtxt> pContext(
            xmlNewParserCtxt(), xmlFreeParserCtxt );

    std::shared_ptr<xmlDoc> pDoc( xmlParseMemory( pXML, aOString.getLength() ),
            xmlFreeDoc );

    if(!pDoc)
    {
        PushError( FormulaError::NoValue );
        return;
    }

    std::shared_ptr<xmlXPathContext> pXPathCtx( xmlXPathNewContext(pDoc.get()),
            xmlXPathFreeContext );

    std::shared_ptr<xmlXPathObject> pXPathObj( xmlXPathEvalExpression(BAD_CAST(pXPathExpr), pXPathCtx.get()),
            xmlXPathFreeObject );

    if(!pXPathObj)
    {
        PushError( FormulaError::NoValue );
        return;
    }

    switch(pXPathObj->type)
    {
        case XPATH_UNDEFINED:
            PushNoValue();
            break;
        case XPATH_NODESET:
            {
                xmlNodeSetPtr pNodeSet = pXPathObj->nodesetval;
                if(!pNodeSet)
                {
                    PushError( FormulaError::NoValue );
                    return;
                }

                const size_t nSize = pNodeSet->nodeNr;
                if (nNode >= nSize)
                {
                    // For pJumpMatrix
                    PushError( FormulaError::NotAvailable);
                    return;
                }

                /* TODO: for nMatCols>1 IF stack type is svMatrix, i.e.
                 * pPathMatrix!=nullptr, we may want a result matrix with
                 * nMatCols columns as well, but clarify first how to treat
                 * differing path elements. */

                ScMatrixRef xResMat;
                if (nMatRows > 1)
                {
                    xResMat = GetNewMat( 1, nMatRows, true);
                    if (!xResMat)
                    {
                        PushError( FormulaError::CodeOverflow);
                        return;
                    }
                }

                for ( ; nNode < nMatRows; ++nNode)
                {
                    if( nSize > nNode )
                    {
                        OUString aResult;
                        if(pNodeSet->nodeTab[nNode]->type == XML_NAMESPACE_DECL)
                        {
                            xmlNsPtr ns = reinterpret_cast<xmlNsPtr>(pNodeSet->nodeTab[nNode]);
                            xmlNodePtr cur = reinterpret_cast<xmlNodePtr>(ns->next);
                            std::shared_ptr<xmlChar> pChar2(xmlNodeGetContent(cur), xmlFree);
                            aResult = OStringToOUString(std::string_view(reinterpret_cast<char*>(pChar2.get())), RTL_TEXTENCODING_UTF8);
                        }
                        else
                        {
                            xmlNodePtr cur = pNodeSet->nodeTab[nNode];
                            std::shared_ptr<xmlChar> pChar2(xmlNodeGetContent(cur), xmlFree);
                            aResult = OStringToOUString(std::string_view(reinterpret_cast<char*>(pChar2.get())), RTL_TEXTENCODING_UTF8);
                        }
                        if (xResMat)
                            xResMat->PutString( mrStrPool.intern( aResult), 0, nNode);
                        else
                            PushString(aResult);
                    }
                    else
                    {
                        if (xResMat)
                            xResMat->PutError( FormulaError::NotAvailable, 0, nNode);
                        else
                            PushError( FormulaError::NotAvailable );
                    }
                }
                if (xResMat)
                    PushMatrix( xResMat);
            }
            break;
        case XPATH_BOOLEAN:
            {
                bool bVal = pXPathObj->boolval != 0;
                PushDouble(double(bVal));
            }
            break;
        case XPATH_NUMBER:
            {
                double fVal = pXPathObj->floatval;
                PushDouble(fVal);
            }
            break;
        case XPATH_STRING:
            PushString(OUString::createFromAscii(reinterpret_cast<char*>(pXPathObj->stringval)));
            break;
#if LIBXML_VERSION < 21000 || defined(LIBXML_XPTR_LOCS_ENABLED)
        case XPATH_POINT:
            PushNoValue();
            break;
        case XPATH_RANGE:
            PushNoValue();
            break;
        case XPATH_LOCATIONSET:
            PushNoValue();
            break;
#endif
        case XPATH_USERS:
            PushNoValue();
            break;
        case XPATH_XSLT_TREE:
            PushNoValue();
            break;

    }
}

static ScWebServiceLink* lcl_GetWebServiceLink(const sfx2::LinkManager* pLinkMgr, std::u16string_view rURL)
{
    size_t nCount = pLinkMgr->GetLinks().size();
    for (size_t i=0; i<nCount; ++i)
    {
        ::sfx2::SvBaseLink* pBase = pLinkMgr->GetLinks()[i].get();
        if (ScWebServiceLink* pLink = dynamic_cast<ScWebServiceLink*>(pBase))
        {
            if (pLink->GetURL() == rURL)
                return pLink;
        }
    }

    return nullptr;
}

static bool lcl_FunctionAccessLoadWebServiceLink( OUString& rResult, ScDocument* pDoc, const OUString& rURI )
{
    // For FunctionAccess service always force a changed data update.
    ScWebServiceLink aLink( pDoc, rURI);
    if (aLink.DataChanged( OUString(), css::uno::Any()) != sfx2::SvBaseLink::UpdateResult::SUCCESS)
        return false;

    if (!aLink.HasResult())
        return false;

    rResult = aLink.GetResult();

    return true;
}

void ScInterpreter::ScWebservice()
{
    sal_uInt8 nParamCount = GetByte();
    if (!MustHaveParamCount( nParamCount, 1 ) )
        return;

    OUString aURI = GetString().getString();

    if (aURI.isEmpty())
    {
        PushError( FormulaError::NoValue );
        return;
    }

    INetURLObject aObj(aURI, INetProtocol::File);
    INetProtocol eProtocol = aObj.GetProtocol();
    if (eProtocol != INetProtocol::Http && eProtocol != INetProtocol::Https)
    {
        PushError(FormulaError::NoValue);
        return;
    }

    if (!mpLinkManager)
    {
        if (!mrDoc.IsFunctionAccess() || mrDoc.HasLinkFormulaNeedingCheck())
        {
            PushError( FormulaError::NoValue);
        }
        else
        {
            OUString aResult;
            if (lcl_FunctionAccessLoadWebServiceLink( aResult, &mrDoc, aURI))
                PushString( aResult);
            else
                PushError( FormulaError::NoValue);
        }
        return;
    }

    // Need to reinterpret after loading (build links)
    pArr->AddRecalcMode( ScRecalcMode::ONLOAD_LENIENT );

    //  while the link is not evaluated, idle must be disabled (to avoid circular references)
    bool bOldEnabled = mrDoc.IsIdleEnabled();
    mrDoc.EnableIdle(false);

    // Get/ Create link object
    ScWebServiceLink* pLink = lcl_GetWebServiceLink(mpLinkManager, aURI);

    bool bWasError = (pMyFormulaCell && pMyFormulaCell->GetRawError() != FormulaError::NONE);

    if (!pLink)
    {
        pLink = new ScWebServiceLink(&mrDoc, aURI);
        mpLinkManager->InsertFileLink(*pLink, sfx2::SvBaseLinkObjectType::ClientFile, aURI);
        if ( mpLinkManager->GetLinks().size() == 1 )                    // the first one?
        {
            SfxBindings* pBindings = mrDoc.GetViewBindings();
            if (pBindings)
                pBindings->Invalidate( SID_LINKS );             // Link-Manager enabled
        }

        //if the document was just loaded, but the ScDdeLink entry was missing, then
        //don't update this link until the links are updated in response to the users
        //decision
        if (!mrDoc.HasLinkFormulaNeedingCheck())
        {
            pLink->Update();
        }

        if (pMyFormulaCell)
        {
            // StartListening after the Update to avoid circular references
            pMyFormulaCell->StartListening(*pLink);
        }
    }
    else
    {
        if (pMyFormulaCell)
            pMyFormulaCell->StartListening(*pLink);
    }

    //  If a new Error from Reschedule appears when the link is executed then reset the errorflag
    if (pMyFormulaCell && pMyFormulaCell->GetRawError() != FormulaError::NONE && !bWasError)
        pMyFormulaCell->SetErrCode(FormulaError::NONE);

    //  check the value
    if (pLink->HasResult())
        PushString(pLink->GetResult());
    else if (mrDoc.HasLinkFormulaNeedingCheck())
    {
        // If this formula cell is recalculated just after load and the
        // expression is exactly WEBSERVICE("literal_URI") (i.e. no other
        // calculation involved, not even a cell reference) and a cached
        // result is set as hybrid string then use that as result value to
        // prevent a #VALUE! result due to the "Automatic update of
        // external links has been disabled."
        // This will work only once, as the new formula cell result won't
        // be a hybrid anymore.
        /* TODO: the FormulaError::LinkFormulaNeedingCheck could be used as
         * a signal for the formula cell to keep the hybrid string as
         * result of the overall formula *iff* no higher prioritized
         * ScRecalcMode than ONLOAD_LENIENT is present in the entire
         * document (i.e. the formula result could not be influenced by an
         * ONLOAD_MUST or ALWAYS recalc, necessary as we don't track
         * interim results of subexpressions that could be compared), which
         * also means to track setting ScRecalcMode somehow... note this is
         * just a vague idea so far and might or might not work. */
        if (pMyFormulaCell && pMyFormulaCell->HasHybridStringResult())
        {
            if (pMyFormulaCell->GetCode()->GetCodeLen() == 2)
            {
                formula::FormulaToken const * const * pRPN = pMyFormulaCell->GetCode()->GetCode();
                if (pRPN[0]->GetType() == formula::svString && pRPN[1]->GetOpCode() == ocWebservice)
                    PushString( pMyFormulaCell->GetResultString());
                else
                    PushError(FormulaError::LinkFormulaNeedingCheck);
            }
            else
                PushError(FormulaError::LinkFormulaNeedingCheck);
        }
        else
            PushError(FormulaError::LinkFormulaNeedingCheck);
    }
    else
        PushError(FormulaError::NoValue);

    mrDoc.EnableIdle(bOldEnabled);
    mpLinkManager->CloseCachedComps();
}

/**
 Returns a string in which all non-alphanumeric characters except stroke and
 underscore (-_) have been replaced with a percent (%) sign
 followed by hex digits.
 It is encoded the same way that the posted data from a WWW form is encoded,
 that is the same way as in application/x-www-form-urlencoded media type and
 as per RFC 3986.

 @see fdo#76870
*/
void ScInterpreter::ScEncodeURL()
{
    sal_uInt8 nParamCount = GetByte();
    if ( !MustHaveParamCount( nParamCount, 1 ) )
        return;

    OUString aStr = GetString().getString();
    if ( aStr.isEmpty() )
    {
        PushError( FormulaError::NoValue );
        return;
    }

    OString aUtf8Str( aStr.toUtf8());
    const sal_Int32 nLen = aUtf8Str.getLength();
    OStringBuffer aUrlBuf( nLen );
    for ( int i = 0; i < nLen; i++ )
    {
        char c = aUtf8Str[ i ];
        if ( rtl::isAsciiAlphanumeric( static_cast<unsigned char>( c ) ) || c == '-' || c == '_' )
            aUrlBuf.append( c );
        else
        {
            aUrlBuf.append( '%' );
            auto convertedChar = OString::number( static_cast<unsigned char>( c ), 16 ).toAsciiUpperCase();
            // RFC 3986 indicates:
            // "A percent-encoded octet is encoded as a character triplet,
            // consisting of the percent character "%" followed by the two hexadecimal digits
            // representing that octet's numeric value"
            if (convertedChar.length == 1)
                aUrlBuf.append("0");
            aUrlBuf.append(convertedChar);
        }
    }
    PushString( OUString::fromUtf8( aUrlBuf ) );
}

void ScInterpreter::ScDebugVar()
{
    // This is to be used by developers only!  Never document this for end
    // users.  This is a convenient way to extract arbitrary internal state to
    // a cell for easier debugging.

    if (!officecfg::Office::Common::Misc::ExperimentalMode::get())
    {
        PushError(FormulaError::NoName);
        return;
    }

    if (!MustHaveParamCount(GetByte(), 1))
        return;

    rtl_uString* p = GetString().getDataIgnoreCase();
    if (!p)
    {
        PushIllegalParameter();
        return;
    }

    OUString aStrUpper(p);

    if (aStrUpper == "PIVOTCOUNT")
    {
        // Set the number of pivot tables in the document.

        double fVal = 0.0;
        if (mrDoc.HasPivotTable())
        {
            const ScDPCollection* pDPs = mrDoc.GetDPCollection();
            fVal = pDPs->GetCount();
        }
        PushDouble(fVal);
    }
    else if (aStrUpper == "DATASTREAM_IMPORT")
        PushDouble( sc::datastream_get_time( sc::DebugTime::Import ) );
    else if (aStrUpper == "DATASTREAM_RECALC")
        PushDouble( sc::datastream_get_time( sc::DebugTime::Recalc ) );
    else if (aStrUpper == "DATASTREAM_RENDER")
        PushDouble( sc::datastream_get_time( sc::DebugTime::Render ) );
    else
        PushIllegalParameter();
}

void ScInterpreter::ScErf()
{
    sal_uInt8 nParamCount = GetByte();
    if (MustHaveParamCount( nParamCount, 1 ) )
        PushDouble( std::erf( GetDouble() ) );
}

void ScInterpreter::ScErfc()
{
    sal_uInt8 nParamCount = GetByte();
    if (MustHaveParamCount( nParamCount, 1 ) )
        PushDouble( std::erfc( GetDouble() ) );
}

void ScInterpreter::ScColor()
{
    sal_uInt8 nParamCount = GetByte();
    if(!MustHaveParamCount(nParamCount, 3, 4))
        return;

    double nAlpha = 0;
    if(nParamCount == 4)
        nAlpha = rtl::math::approxFloor(GetDouble());

    if(nAlpha < 0 || nAlpha > 255)
    {
        PushIllegalArgument();
        return;
    }

    double nBlue = rtl::math::approxFloor(GetDouble());

    if(nBlue < 0 || nBlue > 255)
    {
        PushIllegalArgument();
        return;
    }

    double nGreen = rtl::math::approxFloor(GetDouble());

    if(nGreen < 0 || nGreen > 255)
    {
        PushIllegalArgument();
        return;
    }

    double nRed = rtl::math::approxFloor(GetDouble());

    if(nRed < 0 || nRed > 255)
    {
        PushIllegalArgument();
        return;
    }

    double nVal = 256*256*256*nAlpha + 256*256*nRed + 256*nGreen + nBlue;
    PushDouble(nVal);
}

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