/* -*- 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 <com/sun/star/uno/Reference.h>
#include <com/sun/star/linguistic2/XLinguServiceEventBroadcaster.hpp>
#include <com/sun/star/linguistic2/SpellFailure.hpp>
#include <com/sun/star/uno/XComponentContext.hpp>

#include <unotools/localedatawrapper.hxx>
#include <comphelper/processfactory.hxx>
#include <comphelper/sequence.hxx>
#include <tools/debug.hxx>
#include <svl/lngmisc.hxx>
#include <osl/mutex.hxx>
#include <sal/log.hxx>

#include <vector>

#include "spelldsp.hxx"
#include <linguistic/spelldta.hxx>
#include "lngsvcmgr.hxx"

using namespace osl;
using namespace com::sun::star;
using namespace com::sun::star::beans;
using namespace com::sun::star::lang;
using namespace com::sun::star::uno;
using namespace com::sun::star::linguistic2;
using namespace linguistic;

namespace {

// ProposalList: list of proposals for misspelled words
// The order of strings in the array should be left unchanged because the
// spellchecker should have put the more likely suggestions at the top.
// New entries will be added to the end but duplicates are to be avoided.
// Removing entries is done by assigning the empty string.
// The sequence is constructed from all non empty strings in the original
// while maintaining the order.
class ProposalList
{
    std::vector< OUString > aVec;
    sal_Unicode m_cSingleEndQuote = u'\''; // ASCII apostrophe

public:
    ProposalList(const std::u16string_view& rEndQuote);
    ProposalList(const ProposalList&) = delete;
    ProposalList& operator=(const ProposalList&) = delete;

    size_t  Count() const;
    void    Prepend( const OUString &rText );
    void    Append( const OUString &rNew, bool bPrepend = false );
    void    Append( const std::vector< OUString > &rNew );
    void    Append( const Sequence< OUString > &rNew );
    std::vector< OUString > GetVector() const;
};

}

ProposalList::ProposalList(const std::u16string_view& rEndQuote)
{
    if (!rEndQuote.empty())
        m_cSingleEndQuote = rEndQuote[0];
}

void ProposalList::Prepend( const OUString &rText )
{
    Append( rText, /*bPrepend=*/true );
}

void ProposalList::Append( const OUString &rOrig, bool bPrepend )
{
    // convert ASCII apostrophe to the typographic one
    const OUString aText(rOrig.replace('\'', m_cSingleEndQuote));
    if (std::find(aVec.begin(), aVec.end(), aText) == aVec.end())
    {
        if ( bPrepend )
            aVec.insert( aVec.begin(), aText );
        else
            aVec.push_back( aText );
    }
}

void ProposalList::Append( const std::vector< OUString > &rNew )
{
    size_t nLen = rNew.size();
    for ( size_t i = 0;  i < nLen;  ++i)
    {
        const OUString &rText = rNew[i];
        Append( rText );
    }
}

void ProposalList::Append( const Sequence< OUString > &rNew )
{
    for (const OUString& rText : rNew)
        Append( rText );
}

size_t ProposalList::Count() const
{
    // returns the number of non-empty strings in the vector

    size_t nRes = 0;
    size_t nLen = aVec.size();
    for (size_t i = 0;  i < nLen;  ++i)
    {
        if (!aVec[i].isEmpty())
            ++nRes;
    }
    return nRes;
}

std::vector< OUString > ProposalList::GetVector() const
{
    sal_Int32 nCount = Count();
    sal_Int32 nIdx = 0;
    std::vector< OUString > aRes( nCount );
    sal_Int32 nLen = aVec.size();
    for (sal_Int32 i = 0;  i < nLen;  ++i)
    {
        const OUString &rText = aVec[i];
        DBG_ASSERT( nIdx < nCount, "index out of range" );
        if (nIdx < nCount && !rText.isEmpty())
            aRes[ nIdx++ ] = rText;
    }
    return aRes;
}

static bool SvcListHasLanguage(
        const LangSvcEntries_Spell &rEntry,
        LanguageType nLanguage )
{
    Locale aTmpLocale = LanguageTag::convertToLocale( nLanguage );

    return std::any_of(rEntry.aSvcRefs.begin(), rEntry.aSvcRefs.end(),
        [&aTmpLocale](const Reference<XSpellChecker>& rRef) {
            return rRef.is() && rRef->hasLocale( aTmpLocale ); });
}

SpellCheckerDispatcher::SpellCheckerDispatcher( LngSvcMgr &rLngSvcMgr ) :
    m_rMgr    (rLngSvcMgr)
{
}


SpellCheckerDispatcher::~SpellCheckerDispatcher()
{
}


Sequence< Locale > SAL_CALL SpellCheckerDispatcher::getLocales()
{
    MutexGuard  aGuard( GetLinguMutex() );

    std::vector<Locale> aLocales;
    aLocales.reserve(m_aSvcMap.size());

    std::transform(m_aSvcMap.begin(), m_aSvcMap.end(), std::back_inserter(aLocales),
        [](SpellSvcByLangMap_t::const_reference elem) { return LanguageTag::convertToLocale(elem.first); });

    return comphelper::containerToSequence(aLocales);
}


sal_Bool SAL_CALL SpellCheckerDispatcher::hasLocale( const Locale& rLocale )
{
    MutexGuard  aGuard( GetLinguMutex() );
    SpellSvcByLangMap_t::const_iterator aIt( m_aSvcMap.find( LinguLocaleToLanguage( rLocale ) ) );
    return aIt != m_aSvcMap.end();
}


sal_Bool SAL_CALL
    SpellCheckerDispatcher::isValid( const OUString& rWord, const Locale& rLocale,
            const css::uno::Sequence< ::css::beans::PropertyValue >& rProperties )
{
    MutexGuard  aGuard( GetLinguMutex() );
    // for historical reasons, the word can be only with ASCII apostrophe in the dictionaries,
    // so as a fallback, convert typographical apostrophes to avoid annoying users, if they
    // have old (user) dictionaries only with the obsolete ASCII apostrophe.
    bool bConvert = false;
    bool bRet = isValid_Impl( rWord, LinguLocaleToLanguage( rLocale ), rProperties, bConvert );
    if (!bRet && bConvert)
    {
        // fallback: convert the apostrophes
        bRet = isValid_Impl( rWord, LinguLocaleToLanguage( rLocale ), rProperties, bConvert );
    }
    if (!bRet)
    {
        // accept compounds combined from custom and default dictionary words
        // separated by a hyphen
        sal_Int32 nHyphenPos = rWord.indexOf("-");
        if (nHyphenPos > -1)
        {
            return isValid(rWord.copy(0, nHyphenPos), rLocale, rProperties) &&
                isValid(rWord.copy(nHyphenPos + 1, rWord.getLength() - nHyphenPos - 1),
                                rLocale, rProperties);
        }
    }
    return bRet;
}

Reference< XSpellAlternatives > SAL_CALL
    SpellCheckerDispatcher::spell( const OUString& rWord, const Locale& rLocale,
            const css::uno::Sequence< ::css::beans::PropertyValue >& rProperties )
{
    MutexGuard  aGuard( GetLinguMutex() );
    return spell_Impl( rWord, LinguLocaleToLanguage( rLocale ), rProperties );
}


// returns the overall result of cross-checking with all user-dictionaries
// including the IgnoreAll list
static Reference< XDictionaryEntry > lcl_GetRulingDictionaryEntry(
    const OUString &rWord,
    LanguageType nLanguage )
{
    Reference< XDictionaryEntry > xRes;

    // the order of winning from top to bottom is:
    // 1) IgnoreAll list will always win
    // 2) Negative dictionaries will win over positive dictionaries
    Reference< XDictionary > xIgnoreAll( GetIgnoreAllList() );
    if (xIgnoreAll.is())
        xRes = xIgnoreAll->getEntry( rWord );
    if (!xRes.is())
    {
        Reference< XSearchableDictionaryList > xDList( GetDictionaryList() );
        Reference< XDictionaryEntry > xNegEntry( SearchDicList( xDList,
                rWord, nLanguage, false, true ) );
        if (xNegEntry.is())
            xRes = std::move(xNegEntry);
        else
        {
            Reference< XDictionaryEntry > xPosEntry( SearchDicList( xDList,
                    rWord, nLanguage, true, true ) );
            if (xPosEntry.is())
                xRes = std::move(xPosEntry);
        }
    }

    return xRes;
}


bool SpellCheckerDispatcher::isValid_Impl(
            const OUString& rWord,
            LanguageType nLanguage,
            const PropertyValues& rProperties,
            bool& rConvertApostrophe)
{
    MutexGuard  aGuard( GetLinguMutex() );

    bool bRes = true;

    if (LinguIsUnspecified( nLanguage) || rWord.isEmpty())
        return bRes;

    // search for entry with that language
    SpellSvcByLangMap_t::iterator    aIt( m_aSvcMap.find( nLanguage ) );
    LangSvcEntries_Spell    *pEntry = aIt != m_aSvcMap.end() ? aIt->second.get() : nullptr;

    if (pEntry)
    {
        OUString aChkWord( rWord );
        Locale aLocale( LanguageTag::convertToLocale( nLanguage ) );

        // replace typographical apostrophe by ASCII apostrophe only as a fallback
        // for old user dictionaries before the time of the default typographical apostrophe
        // (Note: otherwise also no problem with non-Unicode Hunspell dictionaries, because
        // the character conversion converts also the typographical apostrophe to the ASCII one)
        OUString aSingleQuote( GetLocaleDataWrapper( nLanguage ).getQuotationMarkEnd() );
        DBG_ASSERT( 1 == aSingleQuote.getLength(), "unexpected length of quotation mark" );
        if (!aSingleQuote.isEmpty() && aChkWord.indexOf(aSingleQuote[0]) > -1)
        {
            // tdf#150582 first check with the original typographical apostrophe,
            // and convert it only on the second try
            if (rConvertApostrophe)
                aChkWord = aChkWord.replace( aSingleQuote[0], '\'' );
            else
                rConvertApostrophe = true;
        }

        RemoveHyphens( aChkWord );
        if (IsIgnoreControlChars( rProperties, GetPropSet() ))
            RemoveControlChars( aChkWord );

        sal_Int32 nLen = pEntry->aSvcRefs.getLength();
        DBG_ASSERT( nLen == pEntry->aSvcImplNames.getLength(),
                "lng : sequence length mismatch");
        DBG_ASSERT( pEntry->nLastTriedSvcIndex < nLen,
                "lng : index out of range");

        sal_Int32 i = 0;
        bool bTmpRes = true;
        bool bTmpResValid = false;

        // try already instantiated services first
        {
            const Reference< XSpellChecker >  *pRef  =
                    pEntry->aSvcRefs.getConstArray();
            while (i <= pEntry->nLastTriedSvcIndex
                   && (!bTmpResValid || !bTmpRes))
            {
                bTmpResValid = true;
                if (pRef[i].is()  &&  pRef[i]->hasLocale( aLocale ))
                {
                    bTmpRes = GetCache().CheckWord( aChkWord, nLanguage );
                    if (!bTmpRes)
                    {
                        bTmpRes = pRef[i]->isValid( aChkWord, aLocale, rProperties );

                        // Add correct words to the cache.
                        // But not those that are correct only because of
                        // the temporary supplied settings.
                        if (bTmpRes  &&  !rProperties.hasElements())
                            GetCache().AddWord( aChkWord, nLanguage );
                    }
                }
                else
                    bTmpResValid = false;

                if (bTmpResValid)
                    bRes = bTmpRes;

                ++i;
            }
        }

        // if still no result instantiate new services and try those
        if ((!bTmpResValid || !bTmpRes)
            &&  pEntry->nLastTriedSvcIndex < nLen - 1)
        {
            const OUString *pImplNames = pEntry->aSvcImplNames.getConstArray();
            Reference< XSpellChecker >  *pRef  = pEntry->aSvcRefs .getArray();

            const Reference< XComponentContext >& xContext(
                comphelper::getProcessComponentContext() );

            // build service initialization argument
            Sequence< Any > aArgs(2);
            aArgs.getArray()[0] <<= GetPropSet();

            while (i < nLen && (!bTmpResValid || !bTmpRes))
            {
                // create specific service via it's implementation name
                Reference< XSpellChecker > xSpell;
                try
                {
                    xSpell.set( xContext->getServiceManager()->createInstanceWithArgumentsAndContext(
                                    pImplNames[i], aArgs, xContext ),
                                UNO_QUERY );
                }
                catch (uno::Exception &)
                {
                    SAL_WARN( "linguistic", "createInstanceWithArguments failed" );
                }
                pRef [i] = xSpell;

                Reference< XLinguServiceEventBroadcaster >
                        xBroadcaster( xSpell, UNO_QUERY );
                if (xBroadcaster.is())
                    m_rMgr.AddLngSvcEvtBroadcaster( xBroadcaster );

                bTmpResValid = true;
                if (xSpell.is()  &&  xSpell->hasLocale( aLocale ))
                {
                    bTmpRes = GetCache().CheckWord( aChkWord, nLanguage );
                    if (!bTmpRes)
                    {
                        bTmpRes = xSpell->isValid( aChkWord, aLocale, rProperties );
                         // Add correct words to the cache.
                        // But not those that are correct only because of
                        // the temporary supplied settings.
                        if (bTmpRes  &&  !rProperties.hasElements())
                            GetCache().AddWord( aChkWord, nLanguage );
                    }
                }
                else
                    bTmpResValid = false;
                if (bTmpResValid)
                    bRes = bTmpRes;

                pEntry->nLastTriedSvcIndex = static_cast<sal_Int16>(i);
                ++i;
            }

            // if language is not supported by any of the services
            // remove it from the list.
            if (i == nLen)
            {
                if (!SvcListHasLanguage( *pEntry, nLanguage ))
                    m_aSvcMap.erase( nLanguage );
            }
        }

        // cross-check against results from dictionaries which have precedence!
        if (GetDicList().is()  &&  IsUseDicList( rProperties, GetPropSet() ))
        {
            Reference< XDictionaryEntry > xTmp( lcl_GetRulingDictionaryEntry( aChkWord, nLanguage ) );
            if (xTmp.is()) {
                bRes = !xTmp->isNegative();
            } else {
                setCharClass(LanguageTag(nLanguage));
                CapType ct = capitalType(aChkWord, m_oCharClass ? &*m_oCharClass : nullptr);
                if (ct == CapType::INITCAP || ct == CapType::ALLCAP) {
                    Reference< XDictionaryEntry > xTmp2( lcl_GetRulingDictionaryEntry( makeLowerCase(aChkWord, m_oCharClass), nLanguage ) );
                    if (xTmp2.is()) {
                        bRes = !xTmp2->isNegative();
                    }
                }
            }
        }
    }

    return bRes;
}


Reference< XSpellAlternatives > SpellCheckerDispatcher::spell_Impl(
            const OUString& rWord,
            LanguageType nLanguage,
            const PropertyValues& rProperties )
{
    MutexGuard  aGuard( GetLinguMutex() );

    if (LinguIsUnspecified( nLanguage) || rWord.isEmpty())
        return nullptr;

    // search for entry with that language
    SpellSvcByLangMap_t::iterator    aIt( m_aSvcMap.find( nLanguage ) );
    LangSvcEntries_Spell    *pEntry = aIt != m_aSvcMap.end() ? aIt->second.get() : nullptr;

    if (!pEntry)
        return nullptr;

    Reference< XSpellAlternatives > xRes;

    OUString aChkWord( rWord );
    Locale aLocale( LanguageTag::convertToLocale( nLanguage ) );

    // replace typographical apostrophe by ascii apostrophe
    OUString aSingleQuote( GetLocaleDataWrapper( nLanguage ).getQuotationMarkEnd() );
    DBG_ASSERT( 1 == aSingleQuote.getLength(), "unexpected length of quotation mark" );
    if (!aSingleQuote.isEmpty())
        aChkWord = aChkWord.replace( aSingleQuote[0], '\'' );

    RemoveHyphens( aChkWord );
    if (IsIgnoreControlChars( rProperties, GetPropSet() ))
        RemoveControlChars( aChkWord );

    sal_Int32 nLen = pEntry->aSvcRefs.getLength();
    DBG_ASSERT( nLen == pEntry->aSvcImplNames.getLength(),
            "lng : sequence length mismatch");
    DBG_ASSERT( pEntry->nLastTriedSvcIndex < nLen,
            "lng : index out of range");

    sal_Int32 i = 0;
    Reference< XSpellAlternatives > xTmpRes;
    bool bTmpResValid = false;

    // try already instantiated services first
    {
        const Reference< XSpellChecker >  *pRef  = pEntry->aSvcRefs.getConstArray();
        sal_Int32 nNumSuggestions = -1;
        while (i <= pEntry->nLastTriedSvcIndex
               &&  (!bTmpResValid || xTmpRes.is()) )
        {
            bTmpResValid = true;
            if (pRef[i].is()  &&  pRef[i]->hasLocale( aLocale ))
            {
                bool bOK = GetCache().CheckWord( aChkWord, nLanguage );
                if (bOK)
                    xTmpRes = nullptr;
                else
                {
                    xTmpRes = pRef[i]->spell( aChkWord, aLocale, rProperties );

                    // Add correct words to the cache.
                    // But not those that are correct only because of
                    // the temporary supplied settings.
                    if (!xTmpRes.is()  &&  !rProperties.hasElements())
                        GetCache().AddWord( aChkWord, nLanguage );
                }
            }
            else
                bTmpResValid = false;

            // return first found result if the word is not known by any checker.
            // But if that result has no suggestions use the first one that does
            // provide suggestions for the misspelled word.
            if (!xRes.is() && bTmpResValid)
            {
                xRes = xTmpRes;
                nNumSuggestions = 0;
                if (xRes.is())
                    nNumSuggestions = xRes->getAlternatives().getLength();
            }
            sal_Int32 nTmpNumSuggestions = 0;
            if (xTmpRes.is() && bTmpResValid)
                nTmpNumSuggestions = xTmpRes->getAlternatives().getLength();
            if (xRes.is() && nNumSuggestions == 0 && nTmpNumSuggestions > 0)
            {
                xRes = xTmpRes;
                nNumSuggestions = nTmpNumSuggestions;
            }

            ++i;
        }
    }

    // if still no result instantiate new services and try those
    if ((!bTmpResValid || xTmpRes.is())
        &&  pEntry->nLastTriedSvcIndex < nLen - 1)
    {
        const OUString *pImplNames = pEntry->aSvcImplNames.getConstArray();
        Reference< XSpellChecker >  *pRef  = pEntry->aSvcRefs .getArray();

        const Reference< XComponentContext >& xContext(
            comphelper::getProcessComponentContext() );

        // build service initialization argument
        Sequence< Any > aArgs(2);
        aArgs.getArray()[0] <<= GetPropSet();

        sal_Int32 nNumSuggestions = -1;
        while (i < nLen  &&  (!bTmpResValid || xTmpRes.is()))
        {
            // create specific service via it's implementation name
            Reference< XSpellChecker > xSpell;
            try
            {
                xSpell.set( xContext->getServiceManager()->createInstanceWithArgumentsAndContext(
                                pImplNames[i], aArgs, xContext ),
                            UNO_QUERY );
            }
            catch (uno::Exception &)
            {
                SAL_WARN( "linguistic", "createInstanceWithArguments failed" );
            }
            pRef [i] = xSpell;

            Reference< XLinguServiceEventBroadcaster >
                    xBroadcaster( xSpell, UNO_QUERY );
            if (xBroadcaster.is())
                m_rMgr.AddLngSvcEvtBroadcaster( xBroadcaster );

            bTmpResValid = true;
            if (xSpell.is()  &&  xSpell->hasLocale( aLocale ))
            {
                bool bOK = GetCache().CheckWord( aChkWord, nLanguage );
                if (bOK)
                    xTmpRes = nullptr;
                else
                {
                    xTmpRes = xSpell->spell( aChkWord, aLocale, rProperties );

                    // Add correct words to the cache.
                    // But not those that are correct only because of
                    // the temporary supplied settings.
                    if (!xTmpRes.is()  &&  !rProperties.hasElements())
                        GetCache().AddWord( aChkWord, nLanguage );
                }
            }
            else
                bTmpResValid = false;

            // return first found result if the word is not known by any checker.
            // But if that result has no suggestions use the first one that does
            // provide suggestions for the misspelled word.
            if (!xRes.is() && bTmpResValid)
            {
                xRes = xTmpRes;
                nNumSuggestions = 0;
                if (xRes.is())
                    nNumSuggestions = xRes->getAlternatives().getLength();
            }
            sal_Int32 nTmpNumSuggestions = 0;
            if (xTmpRes.is() && bTmpResValid)
                nTmpNumSuggestions = xTmpRes->getAlternatives().getLength();
            if (xRes.is() && nNumSuggestions == 0 && nTmpNumSuggestions > 0)
            {
                xRes = xTmpRes;
                nNumSuggestions = nTmpNumSuggestions;
            }

            pEntry->nLastTriedSvcIndex = static_cast<sal_Int16>(i);
            ++i;
        }

        // if language is not supported by any of the services
        // remove it from the list.
        if (i == nLen)
        {
            if (!SvcListHasLanguage( *pEntry, nLanguage ))
                m_aSvcMap.erase( nLanguage );
        }
    }

    // if word is finally found to be correct
    // clear previously remembered alternatives
    if (bTmpResValid  &&  !xTmpRes.is())
        xRes = nullptr;

    // list of proposals found (to be checked against entries of
    // negative dictionaries)
    ProposalList aProposalList(aSingleQuote);
    sal_Int16 eFailureType = -1;    // no failure
    if (xRes.is())
    {
        aProposalList.Append( xRes->getAlternatives() );
        eFailureType = xRes->getFailureType();
    }
    Reference< XSearchableDictionaryList > xDList;
    if (GetDicList().is()  &&  IsUseDicList( rProperties, GetPropSet() ))
        xDList = GetDicList();

    // cross-check against results from user-dictionaries which have precedence!
    if (xDList.is())
    {
        Reference< XDictionaryEntry > xTmp( lcl_GetRulingDictionaryEntry( aChkWord, nLanguage ) );
        if (xTmp.is())
        {
            if (xTmp->isNegative())    // negative entry found
            {
                eFailureType = SpellFailure::IS_NEGATIVE_WORD;

                // replacement text to be added to suggestions, if not empty
                OUString aAddRplcTxt( xTmp->getReplacementText() );

                // replacement text must not be in negative dictionary itself
                if (!aAddRplcTxt.isEmpty() &&
                    !SearchDicList( xDList, aAddRplcTxt, nLanguage, false, true ).is())
                {
                    aProposalList.Prepend( aAddRplcTxt );
                }
            }
            else    // positive entry found
            {
                xRes = nullptr;
                eFailureType = -1;  // no failure
            }
        }
        else
        {
            setCharClass(LanguageTag(nLanguage));
            CapType ct = capitalType(aChkWord, m_oCharClass ? &*m_oCharClass : nullptr);
            if (ct == CapType::INITCAP || ct == CapType::ALLCAP)
            {
                Reference< XDictionaryEntry > xTmp2( lcl_GetRulingDictionaryEntry( makeLowerCase(aChkWord, m_oCharClass), nLanguage ) );
                if (xTmp2.is())
                {
                    if (xTmp2->isNegative())    // negative entry found
                    {
                        eFailureType = SpellFailure::IS_NEGATIVE_WORD;

                        // replacement text to be added to suggestions, if not empty
                        OUString aAddRplcTxt( xTmp2->getReplacementText() );

                        // replacement text must not be in negative dictionary itself
                        if (!aAddRplcTxt.isEmpty() &&
                            !SearchDicList( xDList, aAddRplcTxt, nLanguage, false, true ).is())
                        {
                            switch ( ct )
                            {
                                case CapType::INITCAP:
                                    aProposalList.Prepend( m_oCharClass->titlecase(aAddRplcTxt) );
                                    break;
                                case CapType::ALLCAP:
                                    aProposalList.Prepend( m_oCharClass->uppercase(aAddRplcTxt) );
                                    break;
                                default:
                                    /* can't happen because of if ct ==  above */
                                    break;
                            }
                        }
                    }
                    else    // positive entry found
                    {
                        xRes = nullptr;
                        eFailureType = -1;  // no failure
                    }
                }
            }
        }
    }

    if (eFailureType != -1)     // word misspelled or found in negative user-dictionary
    {
        // search suitable user-dictionaries for suggestions that are
        // similar to the misspelled word
        std::vector< OUString > aDicListProps;   // list of proposals from user-dictionaries
        SearchSimilarText( aChkWord, nLanguage, xDList, aDicListProps );
        aProposalList.Append( aDicListProps );
        std::vector< OUString > aProposals = aProposalList.GetVector();

        // remove entries listed in negative dictionaries
        // (we don't want to display suggestions that will be regarded as misspelled later on)
        if (xDList.is())
            SeqRemoveNegEntries( aProposals, xDList, nLanguage );

        uno::Reference< linguistic2::XSetSpellAlternatives > xSetAlt( xRes, uno::UNO_QUERY );
        if (xSetAlt.is())
        {
            xSetAlt->setAlternatives( comphelper::containerToSequence(aProposals) );
            xSetAlt->setFailureType( eFailureType );
        }
        else
        {
            if (xRes.is())
            {
                SAL_WARN( "linguistic", "XSetSpellAlternatives not implemented!" );
            }
            else if (!aProposals.empty())
            {
                // no xRes but Proposals found from the user-dictionaries.
                // Thus we need to create an xRes...
                xRes = new linguistic::SpellAlternatives( rWord, nLanguage,
                        comphelper::containerToSequence(aProposals) );
            }
        }
    }

    return xRes;
}

uno::Sequence< sal_Int16 > SAL_CALL SpellCheckerDispatcher::getLanguages(  )
{
    MutexGuard  aGuard( GetLinguMutex() );
    uno::Sequence< Locale > aTmp( getLocales() );
    uno::Sequence< sal_Int16 > aRes( LocaleSeqToLangSeq( aTmp ) );
    return aRes;
}


sal_Bool SAL_CALL SpellCheckerDispatcher::hasLanguage(
    sal_Int16 nLanguage )
{
    MutexGuard  aGuard( GetLinguMutex() );
    return hasLocale( LanguageTag::convertToLocale(LanguageType(static_cast<sal_uInt16>(nLanguage))));
}


sal_Bool SAL_CALL SpellCheckerDispatcher::isValid(
    const OUString& rWord,
    sal_Int16 nLanguage,
    const uno::Sequence< beans::PropertyValue >& rProperties )
{
    MutexGuard  aGuard( GetLinguMutex() );
    return isValid( rWord, LanguageTag::convertToLocale(LanguageType(static_cast<sal_uInt16>(nLanguage))), rProperties);
}


uno::Reference< linguistic2::XSpellAlternatives > SAL_CALL SpellCheckerDispatcher::spell(
    const OUString& rWord,
    sal_Int16 nLanguage,
    const uno::Sequence< beans::PropertyValue >& rProperties )
{
    MutexGuard  aGuard( GetLinguMutex() );
    return spell(rWord, LanguageTag::convertToLocale(LanguageType(static_cast<sal_uInt16>(nLanguage))), rProperties);
}


void SpellCheckerDispatcher::SetServiceList( const Locale &rLocale,
        const Sequence< OUString > &rSvcImplNames )
{
    MutexGuard  aGuard( GetLinguMutex() );

    if (m_pCache)
        m_pCache->Flush();    // new services may spell differently...

    LanguageType nLanguage = LinguLocaleToLanguage( rLocale );

    sal_Int32 nLen = rSvcImplNames.getLength();
    if (0 == nLen)
        // remove entry
        m_aSvcMap.erase( nLanguage );
    else
    {
        // modify/add entry
        LangSvcEntries_Spell *pEntry = m_aSvcMap[ nLanguage ].get();
        if (pEntry)
        {
            pEntry->Clear();
            pEntry->aSvcImplNames = rSvcImplNames;
            pEntry->aSvcRefs = Sequence< Reference < XSpellChecker > > ( nLen );
        }
        else
        {
            auto pTmpEntry = std::make_shared<LangSvcEntries_Spell>( rSvcImplNames );
            pTmpEntry->aSvcRefs = Sequence< Reference < XSpellChecker > >( nLen );
            m_aSvcMap[ nLanguage ] = std::move(pTmpEntry);
        }
    }
}


Sequence< OUString >
    SpellCheckerDispatcher::GetServiceList( const Locale &rLocale ) const
{
    MutexGuard  aGuard( GetLinguMutex() );

    Sequence< OUString > aRes;

    // search for entry with that language and use data from that
    LanguageType nLanguage = LinguLocaleToLanguage( rLocale );
    const SpellSvcByLangMap_t::const_iterator aIt( m_aSvcMap.find( nLanguage ) );
    const LangSvcEntries_Spell      *pEntry = aIt != m_aSvcMap.end() ? aIt->second.get() : nullptr;
    if (pEntry)
        aRes = pEntry->aSvcImplNames;

    return aRes;
}


void SpellCheckerDispatcher::FlushSpellCache()
{
    if (m_pCache)
        m_pCache->Flush();
}

void SpellCheckerDispatcher::setCharClass(const LanguageTag& rLanguageTag)
{
    if (m_oCharClass && m_oCharClass->getLanguageTag() == rLanguageTag)
        return;
    m_oCharClass.emplace( rLanguageTag );
}


OUString SpellCheckerDispatcher::makeLowerCase(const OUString& aTerm, const std::optional<CharClass> & pCC)
{
    if (pCC)
        return pCC->lowercase(aTerm);
    return aTerm;
}

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