/* -*- 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 <tools/urlobj.hxx>
#include <hintids.hxx>
#include <acmplwrd.hxx>
#include <doc.hxx>
#include <pagedesc.hxx>
#include <poolfmt.hxx>
#include <svl/listener.hxx>
#include <IDocumentStylePoolAccess.hxx>
#include <editeng/svxacorr.hxx>
#include <osl/diagnose.h>

#include <editeng/acorrcfg.hxx>
#include <sfx2/docfile.hxx>
#include <docsh.hxx>

#include <cassert>
#include <vector>

class SwAutoCompleteClient : public SvtListener
{
    SwAutoCompleteWord* m_pAutoCompleteWord;
    SwDoc*              m_pDoc;
#if OSL_DEBUG_LEVEL > 0
    static sal_uLong s_nSwAutoCompleteClientCount;
#endif
public:
    SwAutoCompleteClient(SwAutoCompleteWord& rToTell, SwDoc& rSwDoc);
    SwAutoCompleteClient(const SwAutoCompleteClient& rClient);
    virtual ~SwAutoCompleteClient() override;

    SwAutoCompleteClient& operator=(const SwAutoCompleteClient& rClient);

    const SwDoc& GetDoc() const {return *m_pDoc;}
#if OSL_DEBUG_LEVEL > 0
    static sal_uLong GetElementCount() {return s_nSwAutoCompleteClientCount;}
#endif
protected:
    virtual void Notify(const SfxHint&) override;
private:
    void DocumentDying();
};

class SwAutoCompleteWord_Impl
{
    std::vector<SwAutoCompleteClient>
                                m_aClientVector;
    SwAutoCompleteWord&         m_rAutoCompleteWord;
public:
    explicit SwAutoCompleteWord_Impl(SwAutoCompleteWord& rParent) :
        m_rAutoCompleteWord(rParent){}
    void AddDocument(SwDoc& rDoc);
    void RemoveDocument(const SwDoc& rDoc);
};

class SwAutoCompleteString
    : public editeng::IAutoCompleteString
{
#if OSL_DEBUG_LEVEL > 0
    static sal_uLong s_nSwAutoCompleteStringCount;
#endif
    std::vector<const SwDoc*> m_aSourceDocs;
    public:
        SwAutoCompleteString(const OUString& rStr, sal_Int32 nLen);

        virtual ~SwAutoCompleteString() override;
        void        AddDocument(const SwDoc& rDoc);
        //returns true if last document reference has been removed
        bool        RemoveDocument(const SwDoc& rDoc);
#if OSL_DEBUG_LEVEL > 0
    static sal_uLong GetElementCount() {return s_nSwAutoCompleteStringCount;}
#endif
};
#if OSL_DEBUG_LEVEL > 0
    sal_uLong SwAutoCompleteClient::s_nSwAutoCompleteClientCount = 0;
    sal_uLong SwAutoCompleteString::s_nSwAutoCompleteStringCount = 0;
#endif

SwAutoCompleteClient::SwAutoCompleteClient(SwAutoCompleteWord& rToTell, SwDoc& rSwDoc) :
        m_pAutoCompleteWord(&rToTell),
        m_pDoc(&rSwDoc)
{
    StartListening(m_pDoc->getIDocumentStylePoolAccess().GetPageDescFromPool(RES_POOLPAGE_STANDARD)->GetNotifier());
#if OSL_DEBUG_LEVEL > 0
    ++s_nSwAutoCompleteClientCount;
#endif
}

SwAutoCompleteClient::SwAutoCompleteClient(const SwAutoCompleteClient& rClient) :
    SvtListener(),
    m_pAutoCompleteWord(rClient.m_pAutoCompleteWord),
    m_pDoc(rClient.m_pDoc)
{
    StartListening(m_pDoc->getIDocumentStylePoolAccess().GetPageDescFromPool(RES_POOLPAGE_STANDARD)->GetNotifier());
#if OSL_DEBUG_LEVEL > 0
    ++s_nSwAutoCompleteClientCount;
#endif
}

SwAutoCompleteClient::~SwAutoCompleteClient()
{
#if OSL_DEBUG_LEVEL > 0
    --s_nSwAutoCompleteClientCount;
#else
    (void) this;
#endif
}

SwAutoCompleteClient& SwAutoCompleteClient::operator=(const SwAutoCompleteClient& rClient)
{
    m_pAutoCompleteWord = rClient.m_pAutoCompleteWord;
    m_pDoc = rClient.m_pDoc;
    CopyAllBroadcasters(rClient);
    return *this;
}

void SwAutoCompleteClient::DocumentDying()
{
    EndListeningAll();
    m_pAutoCompleteWord->DocumentDying(*m_pDoc);
}

void SwAutoCompleteClient::Notify(const SfxHint& rHint)
{
    switch(rHint.GetId())
    {
        case SfxHintId::Dying:
            DocumentDying();
            return;
        case SfxHintId::SwRemoveUnoObject:
            DocumentDying();
            return;
        case SfxHintId::SwObjectDying:
            DocumentDying();
            return;
        default:
            return;
    }
}

void SwAutoCompleteWord_Impl::AddDocument(SwDoc& rDoc)
{
    if (std::any_of(m_aClientVector.begin(), m_aClientVector.end(),
            [&rDoc](SwAutoCompleteClient& rClient) { return &rClient.GetDoc() == &rDoc; }))
        return;
    m_aClientVector.emplace_back(m_rAutoCompleteWord, rDoc);
}

void SwAutoCompleteWord_Impl::RemoveDocument(const SwDoc& rDoc)
{
    auto aIt = std::find_if(m_aClientVector.begin(), m_aClientVector.end(),
        [&rDoc](SwAutoCompleteClient& rClient) { return &rClient.GetDoc() == &rDoc; });
    if (aIt != m_aClientVector.end())
        m_aClientVector.erase(aIt);
}

SwAutoCompleteString::SwAutoCompleteString(
            const OUString& rStr, sal_Int32 const nLen)
    : editeng::IAutoCompleteString(rStr.copy(0, nLen))
{
#if OSL_DEBUG_LEVEL > 0
    ++s_nSwAutoCompleteStringCount;
#endif
}

SwAutoCompleteString::~SwAutoCompleteString()
{
#if OSL_DEBUG_LEVEL > 0
    --s_nSwAutoCompleteStringCount;
#else
    (void) this;
#endif
}

void SwAutoCompleteString::AddDocument(const SwDoc& rDoc)
{
    auto aIt = std::find(m_aSourceDocs.begin(), m_aSourceDocs.end(), &rDoc);
    if (aIt != m_aSourceDocs.end())
        return;
    m_aSourceDocs.push_back(&rDoc);
}

bool SwAutoCompleteString::RemoveDocument(const SwDoc& rDoc)
{
    auto aIt = std::find(m_aSourceDocs.begin(), m_aSourceDocs.end(), &rDoc);
    if (aIt != m_aSourceDocs.end())
    {
        m_aSourceDocs.erase(aIt);
        return m_aSourceDocs.empty();
    }
    return false;
}

SwAutoCompleteWord::SwAutoCompleteWord(
    editeng::SortedAutoCompleteStrings::size_type nWords, sal_uInt16 nMWrdLen ):
    m_pImpl(new SwAutoCompleteWord_Impl(*this)),
    m_nMaxCount( nWords ),
    m_nMinWordLen( nMWrdLen ),
    m_bLockWordList( false )
{
}

SwAutoCompleteWord::~SwAutoCompleteWord()
{
    m_WordList.DeleteAndDestroyAll(); // so the assertion below works
#if OSL_DEBUG_LEVEL > 0
    sal_uLong nStrings = SwAutoCompleteString::GetElementCount();
    sal_uLong nClients = SwAutoCompleteClient::GetElementCount();
    OSL_ENSURE(!nStrings && !nClients, "AutoComplete: clients or string count mismatch");
#endif
}

bool SwAutoCompleteWord::InsertWord( const OUString& rWord, SwDoc& rDoc )
{
    SwDocShell* pDocShell = rDoc.GetDocShell();
    SfxMedium* pMedium = pDocShell ? pDocShell->GetMedium() : nullptr;
    // strings from help module should not be added
    if( pMedium )
    {
        const INetURLObject& rURL = pMedium->GetURLObject();
        if ( rURL.GetProtocol() == INetProtocol::VndSunStarHelp )
            return false;
    }

    OUString aNewWord = rWord.replaceAll(OUStringChar(CH_TXTATR_INWORD), "")
                             .replaceAll(OUStringChar(CH_TXTATR_BREAKWORD), "");

    m_pImpl->AddDocument(rDoc);
    bool bRet = false;
    sal_Int32 nWrdLen = aNewWord.getLength();
    while( nWrdLen && '.' == aNewWord[ nWrdLen-1 ])
        --nWrdLen;

    if( !m_bLockWordList && nWrdLen >= m_nMinWordLen )
    {
        SwAutoCompleteString* pNew = new SwAutoCompleteString( aNewWord, nWrdLen );
        pNew->AddDocument(rDoc);
        std::pair<editeng::SortedAutoCompleteStrings::const_iterator, bool>
            aInsPair = m_WordList.insert(pNew);

        m_LookupTree.insert( aNewWord.subView(0, nWrdLen) );

        if (aInsPair.second)
        {
            bRet = true;
            if (m_aLRUList.size() >= m_nMaxCount)
            {
                // the last one needs to be removed
                // so that there is space for the first one
                SwAutoCompleteString* pDel = m_aLRUList.back();
                m_aLRUList.pop_back();
                m_WordList.erase(pDel);
                delete pDel;
            }
            m_aLRUList.push_front(pNew);
        }
        else
        {
            delete pNew;
            // then move "up"
            pNew = static_cast<SwAutoCompleteString*>(*aInsPair.first);

            // add the document to the already inserted string
            pNew->AddDocument(rDoc);

            // move pNew to the front of the LRU list
            SwAutoCompleteStringPtrDeque::iterator it = std::find( m_aLRUList.begin(), m_aLRUList.end(), pNew );
            OSL_ENSURE( m_aLRUList.end() != it, "String not found" );
            if ( m_aLRUList.begin() != it && m_aLRUList.end() != it )
            {
                m_aLRUList.erase( it );
                m_aLRUList.push_front( pNew );
            }
        }
    }
    return bRet;
}

void SwAutoCompleteWord::SetMaxCount(
    editeng::SortedAutoCompleteStrings::size_type nNewMax )
{
    if( nNewMax < m_nMaxCount && m_aLRUList.size() > nNewMax )
    {
        // remove the trailing ones
        SwAutoCompleteStringPtrDeque::size_type nLRUIndex = nNewMax-1;
        while (nNewMax < m_WordList.size() && nLRUIndex < m_aLRUList.size())
        {
            auto it = m_WordList.find(m_aLRUList[nLRUIndex++]);
            OSL_ENSURE( m_WordList.end() != it, "String not found" );
            editeng::IAutoCompleteString *const pDel = *it;
            m_WordList.erase(it);
            delete pDel;
        }
        m_aLRUList.erase( m_aLRUList.begin() + nNewMax - 1, m_aLRUList.end() );
    }
    m_nMaxCount = nNewMax;
}

void SwAutoCompleteWord::SetMinWordLen( sal_uInt16 n )
{
    // Do you really want to remove all words that are less than the minWrdLen?
    if( n < m_nMinWordLen )
    {
        size_t nPos = 0;
        while (nPos < m_WordList.size())
        {
            if (m_WordList[ nPos ]->GetAutoCompleteString().getLength() < n)
            {
                SwAutoCompleteString *const pDel =
                    dynamic_cast<SwAutoCompleteString*>(m_WordList[nPos]);
                m_WordList.erase_at(nPos);

                SwAutoCompleteStringPtrDeque::iterator it = std::find( m_aLRUList.begin(), m_aLRUList.end(), pDel );
                OSL_ENSURE( m_aLRUList.end() != it, "String not found" );
                m_aLRUList.erase( it );
                delete pDel;
                continue;
            }
            ++nPos;
        }
    }

    m_nMinWordLen = n;
}

/** Return all words matching a given prefix
 *
 *  @param aMatch the prefix to search for
 *  @param rWords the words found matching
 */
bool SwAutoCompleteWord::GetWordsMatching(std::u16string_view aMatch, std::vector<OUString>& rWords) const
{
    assert(rWords.empty());
    m_LookupTree.findSuggestions(aMatch, rWords);
    return !rWords.empty();
}

void SwAutoCompleteWord::CheckChangedList(
        const editeng::SortedAutoCompleteStrings& rNewLst)
{
    size_t nMyLen = m_WordList.size(), nNewLen = rNewLst.size();
    size_t nMyPos = 0, nNewPos = 0;

    for( ; nMyPos < nMyLen && nNewPos < nNewLen; ++nMyPos, ++nNewPos )
    {
        const editeng::IAutoCompleteString * pStr = rNewLst[ nNewPos ];
        while (m_WordList[nMyPos] != pStr)
        {
            SwAutoCompleteString *const pDel =
                dynamic_cast<SwAutoCompleteString*>(m_WordList[nMyPos]);
            m_WordList.erase_at(nMyPos);
            SwAutoCompleteStringPtrDeque::iterator it = std::find( m_aLRUList.begin(), m_aLRUList.end(), pDel );
            OSL_ENSURE( m_aLRUList.end() != it, "String not found" );
            m_aLRUList.erase( it );
            delete pDel;
            if( nMyPos >= --nMyLen )
                break;
        }
    }
    // remove the elements at the end of the array
    if( nMyPos >= nMyLen )
        return;

    // clear LRU array first then delete the string object
    for( ; nNewPos < nMyLen; ++nNewPos )
    {
        SwAutoCompleteString *const pDel =
            dynamic_cast<SwAutoCompleteString*>(m_WordList[nNewPos]);
        SwAutoCompleteStringPtrDeque::iterator it = std::find( m_aLRUList.begin(), m_aLRUList.end(), pDel );
        OSL_ENSURE( m_aLRUList.end() != it, "String not found" );
        m_aLRUList.erase( it );
        delete pDel;
    }
    // remove from array
    m_WordList.erase(m_WordList.begin() + nMyPos,
                     m_WordList.begin() + nMyLen);
}

void SwAutoCompleteWord::DocumentDying(const SwDoc& rDoc)
{
    m_pImpl->RemoveDocument(rDoc);

    SvxAutoCorrect* pACorr = SvxAutoCorrCfg::Get().GetAutoCorrect();
    const bool bDelete = !pACorr->GetSwFlags().bAutoCmpltKeepList;
    for (size_t nPos = m_WordList.size(); nPos; nPos--)
    {
        SwAutoCompleteString *const pCurrent = dynamic_cast<SwAutoCompleteString*>(m_WordList[nPos - 1]);
        if(pCurrent && pCurrent->RemoveDocument(rDoc) && bDelete)
        {
            m_WordList.erase_at(nPos - 1);
            SwAutoCompleteStringPtrDeque::iterator it = std::find( m_aLRUList.begin(), m_aLRUList.end(), pCurrent );
            OSL_ENSURE( m_aLRUList.end() != it, "word not found in LRU list" );
            m_aLRUList.erase( it );
            delete pCurrent;
        }
    }
}

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