/* -*- 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 <osl/time.h>
#include <svx/svdmark.hxx>
#include <svx/svdobj.hxx>
#include <svx/svdpage.hxx>
#include <svx/svdpagv.hxx>
#include <svx/strings.hrc>
#include <svx/dialmgr.hxx>


#include <svl/SfxBroadcaster.hxx>
#include <svx/svdoedge.hxx>
#include <osl/diagnose.h>
#include <tools/debug.hxx>

#include <cassert>

void SdrMark::setTime()
{
    TimeValue aNow;
    osl_getSystemTime(&aNow);
    mnTimeStamp = sal_Int64(aNow.Seconds) * 1000000000 + aNow.Nanosec;
}

SdrMark::SdrMark(SdrObject* pNewObj, SdrPageView* pNewPageView)
:   mpSelectedSdrObject(pNewObj),
    mpPageView(pNewPageView),
    mbCon1(false),
    mbCon2(false),
    mnUser(0)
{
    if(mpSelectedSdrObject)
    {
        mpSelectedSdrObject->AddObjectUser( *this );
    }
    setTime();
}

SdrMark::SdrMark(const SdrMark& rMark)
:   ObjectUser(),
    mnTimeStamp(0),
    mpSelectedSdrObject(nullptr),
    mpPageView(nullptr),
    mbCon1(false),
    mbCon2(false),
    mnUser(0)
{
    *this = rMark;
}

SdrMark::~SdrMark()
{
    if (mpSelectedSdrObject)
    {
        mpSelectedSdrObject->RemoveObjectUser( *this );
    }
}

void SdrMark::ObjectInDestruction(const SdrObject& rObject)
{
    (void) rObject; // avoid warnings
    OSL_ENSURE(mpSelectedSdrObject && mpSelectedSdrObject == &rObject, "SdrMark::ObjectInDestruction: called from object different from hosted one (!)");
    OSL_ENSURE(mpSelectedSdrObject, "SdrMark::ObjectInDestruction: still selected SdrObject is deleted, deselect first (!)");
    mpSelectedSdrObject = nullptr;
}

void SdrMark::SetMarkedSdrObj(SdrObject* pNewObj)
{
    if(mpSelectedSdrObject)
    {
        mpSelectedSdrObject->RemoveObjectUser( *this );
    }

    mpSelectedSdrObject = pNewObj;

    if(mpSelectedSdrObject)
    {
        mpSelectedSdrObject->AddObjectUser( *this );
    }
}

SdrMark& SdrMark::operator=(const SdrMark& rMark)
{
    SetMarkedSdrObj(rMark.mpSelectedSdrObject);

    mnTimeStamp = rMark.mnTimeStamp;
    mpPageView = rMark.mpPageView;
    mbCon1 = rMark.mbCon1;
    mbCon2 = rMark.mbCon2;
    mnUser = rMark.mnUser;
    maPoints = rMark.maPoints;
    maGluePoints = rMark.maGluePoints;

    return *this;
}

static bool ImpSdrMarkListSorter(std::unique_ptr<SdrMark> const& lhs, std::unique_ptr<SdrMark> const& rhs)
{
    SdrObject* pObj1 = lhs->GetMarkedSdrObj();
    SdrObject* pObj2 = rhs->GetMarkedSdrObj();
    SdrObjList* pOL1 = pObj1 ? pObj1->getParentSdrObjListFromSdrObject() : nullptr;
    SdrObjList* pOL2 = pObj2 ? pObj2->getParentSdrObjListFromSdrObject() : nullptr;

    if (pOL1 == pOL2)
    {
        // AF: Note that I reverted a change from sal_uInt32 to sal_uLong (made
        // for 64bit compliance, #i78198#) because internally in SdrObject
        // both nOrdNum and mnNavigationPosition are stored as sal_uInt32.
        sal_uInt32 nObjOrd1(pObj1 ? pObj1->GetNavigationPosition() : 0);
        sal_uInt32 nObjOrd2(pObj2 ? pObj2->GetNavigationPosition() : 0);

        return nObjOrd1 < nObjOrd2;
    }
    else
    {
        return pOL1 < pOL2;
    }
}


void SdrMarkList::ForceSort() const
{
    if(!mbSorted)
    {
        const_cast<SdrMarkList*>(this)->ImpForceSort();
    }
}

void SdrMarkList::ImpForceSort()
{
    if(mbSorted)
        return;

    mbSorted = true;
    size_t nCount = maList.size();

    // remove invalid
    if(nCount > 0 )
    {
        std::erase_if(maList, [](std::unique_ptr<SdrMark>& rItem) { return rItem->GetMarkedSdrObj() == nullptr; });
        nCount = maList.size();
    }

    if(nCount <= 1)
        return;

    std::sort(maList.begin(), maList.end(), ImpSdrMarkListSorter);

    // remove duplicates
    if(maList.size() <= 1)
        return;

    SdrMark* pCurrent = maList.back().get();
    for (size_t count = maList.size() - 1; count; --count)
    {
        size_t i = count - 1;
        SdrMark* pCmp = maList[i].get();
        assert(pCurrent->GetMarkedSdrObj());
        if(pCurrent->GetMarkedSdrObj() == pCmp->GetMarkedSdrObj())
        {
            // Con1/Con2 Merging
            if(pCmp->IsCon1())
                pCurrent->SetCon1(true);

            if(pCmp->IsCon2())
                pCurrent->SetCon2(true);

            // delete pCmp
            maList.erase(maList.begin() + i);
        }
        else
        {
            pCurrent = pCmp;
        }
    }
}

void SdrMarkList::Clear()
{
    maList.clear();
    mbSorted = true; //we're empty, so can be considered sorted
    SetNameDirty();
}

SdrMarkList& SdrMarkList::operator=(const SdrMarkList& rLst)
{
    if (this != &rLst)
    {
        Clear();

        for(size_t i = 0; i < rLst.GetMarkCount(); ++i)
        {
            SdrMark* pMark = rLst.GetMark(i);
            maList.emplace_back(new SdrMark(*pMark));
        }

        maMarkName = rLst.maMarkName;
        mbNameOk = rLst.mbNameOk;
        maPointName = rLst.maPointName;
        mbPointNameOk = rLst.mbPointNameOk;
        maGluePointName = rLst.maGluePointName;
        mbSorted = rLst.mbSorted;
    }
    return *this;
}

SdrMark* SdrMarkList::GetMark(size_t nNum) const
{
    return (nNum < maList.size()) ? maList[nNum].get() : nullptr;
}

size_t SdrMarkList::FindObject(const SdrObject* pObj) const
{
    // Since relying on OrdNums is not allowed for the selection because objects in the
    // selection may not be inserted in a list if they are e.g. modified ATM, i changed
    // this loop to just look if the object pointer is in the selection.

    // Problem is that GetOrdNum() which is const, internally casts to non-const and
    // hardly sets the OrdNum member of the object (nOrdNum) to 0 (ZERO) if the object
    // is not inserted in an object list.
    // Since this may be by purpose and necessary somewhere else i decided that it is
    // less dangerous to change this method then changing SdrObject::GetOrdNum().
    if(pObj)
    {
        for(size_t a = 0; a < maList.size(); ++a)
        {
            if(maList[a]->GetMarkedSdrObj() == pObj)
            {
                return a;
            }
        }
    }

    return SAL_MAX_SIZE;
}

void SdrMarkList::InsertEntry(const SdrMark& rMark, bool bChkSort)
{
    SetNameDirty();
    const size_t nCount(maList.size());

    if(!bChkSort || !mbSorted || nCount == 0)
    {
        if(!bChkSort)
            mbSorted = false;

        maList.emplace_back(new SdrMark(rMark));
    }
    else
    {
        SdrMark* pLast = GetMark(nCount - 1);
        const SdrObject* pLastObj = pLast->GetMarkedSdrObj();
        const SdrObject* pNewObj = rMark.GetMarkedSdrObj();

        if(pLastObj == pNewObj)
        {
            // This one already exists.
            // Con1/Con2 Merging
            if(rMark.IsCon1())
                pLast->SetCon1(true);

            if(rMark.IsCon2())
                pLast->SetCon2(true);
        }
        else
        {
            maList.emplace_back(new SdrMark(rMark));

            // now check if the sort is ok
            const SdrObjList* pLastOL = pLastObj!=nullptr ? pLastObj->getParentSdrObjListFromSdrObject() : nullptr;
            const SdrObjList* pNewOL = pNewObj !=nullptr ? pNewObj->getParentSdrObjListFromSdrObject() : nullptr;

            if(pLastOL == pNewOL)
            {
                const sal_uInt32 nLastNum(pLastObj!=nullptr ? pLastObj->GetOrdNum() : 0);
                const sal_uInt32 nNewNum(pNewObj !=nullptr ? pNewObj ->GetOrdNum() : 0);

                if(nNewNum < nLastNum)
                {
                    // at some point, we have to sort
                    mbSorted = false;
                }
            }
            else
            {
                // at some point, we have to sort
                mbSorted = false;
            }
        }
    }
}

void SdrMarkList::DeleteMark(size_t nNum)
{
    SdrMark* pMark = GetMark(nNum);
    DBG_ASSERT(pMark!=nullptr,"DeleteMark: MarkEntry not found.");

    if(pMark)
    {
        maList.erase(maList.begin() + nNum);
        if (maList.empty())
            mbSorted = true; //we're empty, so can be considered sorted
        SetNameDirty();
    }
}

void SdrMarkList::ReplaceMark(const SdrMark& rNewMark, size_t nNum)
{
    SdrMark* pMark = GetMark(nNum);
    DBG_ASSERT(pMark!=nullptr,"ReplaceMark: MarkEntry not found.");

    if(pMark)
    {
        SetNameDirty();
        maList[nNum].reset(new SdrMark(rNewMark));
        mbSorted = false;
    }
}

void SdrMarkList::Merge(const SdrMarkList& rSrcList, bool bReverse)
{
    const size_t nCount(rSrcList.maList.size());

    if(rSrcList.mbSorted)
    {
        // merge without forcing a Sort in rSrcList
        bReverse = false;
    }

    if(!bReverse)
    {
        for(size_t i = 0; i < nCount; ++i)
        {
            SdrMark* pM = rSrcList.maList[i].get();
            InsertEntry(*pM);
        }
    }
    else
    {
        for(size_t i = nCount; i > 0;)
        {
            --i;
            SdrMark* pM = rSrcList.maList[i].get();
            InsertEntry(*pM);
        }
    }
}

bool SdrMarkList::DeletePageView(const SdrPageView& rPV)
{
    bool bChgd(false);

    for(auto it = maList.begin(); it != maList.end(); )
    {
        SdrMark* pMark = it->get();

        if(pMark->GetPageView()==&rPV)
        {
            it = maList.erase(it);
            SetNameDirty();
            bChgd = true;
        }
        else
            ++it;
    }

    return bChgd;
}

bool SdrMarkList::InsertPageView(const SdrPageView& rPV)
{
    bool bChgd(false);
    DeletePageView(rPV); // delete all of them, then append the entire page
    const SdrObjList* pOL = rPV.GetObjList();

    for (const rtl::Reference<SdrObject>& pObj : *pOL)
    {
        bool bDoIt(rPV.IsObjMarkable(pObj.get()));

        if(bDoIt)
        {
            maList.emplace_back(new SdrMark(pObj.get(), const_cast<SdrPageView*>(&rPV)));
            SetNameDirty();
            bChgd = true;
        }
    }

    return bChgd;
}

const OUString& SdrMarkList::GetMarkDescription() const
{
    const size_t nCount(GetMarkCount());

    if(mbNameOk && 1 == nCount)
    {
        // if it's a single selection, cache only text frame
        const SdrObject* pObj = GetMark(0)->GetMarkedSdrObj();
        const SdrTextObj* pTextObj = DynCastSdrTextObj( pObj );

        if(!pTextObj || !pTextObj->IsTextFrame())
        {
            const_cast<SdrMarkList*>(this)->mbNameOk = false;
        }
    }

    if(!mbNameOk)
    {
        SdrMark* pMark = GetMark(0);
        OUString aNam;

        if(!nCount)
        {
            const_cast<SdrMarkList*>(this)->maMarkName = SvxResId(STR_ObjNameNoObj);
        }
        else if(1 == nCount)
        {
            if(pMark->GetMarkedSdrObj())
            {
                aNam = pMark->GetMarkedSdrObj()->TakeObjNameSingul();
            }
        }
        else
        {
            if(pMark->GetMarkedSdrObj())
            {
                aNam = pMark->GetMarkedSdrObj()->TakeObjNamePlural();
                bool bEq(true);

                for(size_t i = 1; i < GetMarkCount() && bEq; ++i)
                {
                    SdrMark* pMark2 = GetMark(i);
                    OUString aStr1(pMark2->GetMarkedSdrObj()->TakeObjNamePlural());
                    bEq = aNam == aStr1;
                }

                if(!bEq)
                {
                    aNam = SvxResId(STR_ObjNamePlural);
                }
            }

            aNam = OUString::number( nCount ) + " " + aNam;
        }

        const_cast<SdrMarkList*>(this)->maMarkName = aNam;
        const_cast<SdrMarkList*>(this)->mbNameOk = true;
    }

    return maMarkName;
}

const OUString& SdrMarkList::GetPointMarkDescription(bool bGlue) const
{
    bool& rNameOk = const_cast<bool&>(bGlue ? mbGluePointNameOk : mbPointNameOk);
    OUString& rName = const_cast<OUString&>(bGlue ? maGluePointName : maPointName);
    const size_t nMarkCount(GetMarkCount());
    size_t nMarkPtCnt(0);
    size_t nMarkPtObjCnt(0);
    size_t n1stMarkNum(SAL_MAX_SIZE);

    for(size_t nMarkNum = 0; nMarkNum < nMarkCount; ++nMarkNum)
    {
        const SdrMark* pMark = GetMark(nMarkNum);
        const SdrUShortCont& rPts = bGlue ? pMark->GetMarkedGluePoints() : pMark->GetMarkedPoints();

        if (!rPts.empty())
        {
            if(n1stMarkNum == SAL_MAX_SIZE)
            {
                n1stMarkNum = nMarkNum;
            }

            nMarkPtCnt += rPts.size();
            nMarkPtObjCnt++;
        }

        if(nMarkPtObjCnt > 1 && rNameOk)
        {
            // preliminary decision
            return rName;
        }
    }

    if(rNameOk && 1 == nMarkPtObjCnt)
    {
        // if it's a single selection, cache only text frame
        const SdrObject* pObj = GetMark(0)->GetMarkedSdrObj();
        const SdrTextObj* pTextObj = DynCastSdrTextObj( pObj );

        if(!pTextObj || !pTextObj->IsTextFrame())
        {
            rNameOk = false;
        }
    }

    if(!nMarkPtObjCnt)
    {
        rName.clear();
        rNameOk = true;
    }
    else if(!rNameOk)
    {
        const SdrMark* pMark = GetMark(n1stMarkNum);
        OUString aNam;

        if(1 == nMarkPtObjCnt)
        {
            if(pMark->GetMarkedSdrObj())
            {
                aNam = pMark->GetMarkedSdrObj()->TakeObjNameSingul();
            }
        }
        else
        {
            if(pMark->GetMarkedSdrObj())
            {
                aNam = pMark->GetMarkedSdrObj()->TakeObjNamePlural();
            }

            bool bEq(true);

            for(size_t i = n1stMarkNum + 1; i < GetMarkCount() && bEq; ++i)
            {
                const SdrMark* pMark2 = GetMark(i);
                const SdrUShortCont& rPts = bGlue ? pMark2->GetMarkedGluePoints() : pMark2->GetMarkedPoints();

                if (!rPts.empty() && pMark2->GetMarkedSdrObj())
                {
                    OUString aStr1(pMark2->GetMarkedSdrObj()->TakeObjNamePlural());
                    bEq = aNam == aStr1;
                }
            }

            if(!bEq)
            {
                aNam = SvxResId(STR_ObjNamePlural);
            }

            aNam = OUString::number( nMarkPtObjCnt ) + " " + aNam;
        }

        OUString aStr1;

        if(1 == nMarkPtCnt)
        {
            aStr1 = SvxResId(bGlue ? STR_ViewMarkedGluePoint : STR_ViewMarkedPoint);
        }
        else
        {
            aStr1 = SvxResId(bGlue ? STR_ViewMarkedGluePoints : STR_ViewMarkedPoints);
            aStr1 = aStr1.replaceFirst("%2", OUString::number( nMarkPtCnt ));
        }

        aStr1 = aStr1.replaceFirst("%1", aNam);
        rName = aStr1;
        rNameOk = true;
    }

    return rName;
}

bool SdrMarkList::TakeBoundRect(SdrPageView const * pPV, tools::Rectangle& rRect) const
{
    bool bFnd(false);
    tools::Rectangle aR;

    for(size_t i = 0; i < GetMarkCount(); ++i)
    {
        SdrMark* pMark = GetMark(i);

        if(!pPV || pMark->GetPageView() == pPV)
        {
            if(pMark->GetMarkedSdrObj())
            {
                aR = pMark->GetMarkedSdrObj()->GetCurrentBoundRect();

                if(bFnd)
                {
                    rRect.Union(aR);
                }
                else
                {
                    rRect = aR;
                    bFnd = true;
                }
            }
        }
    }

    return bFnd;
}

bool SdrMarkList::TakeSnapRect(SdrPageView const * pPV, tools::Rectangle& rRect) const
{
    bool bFnd(false);

    for(size_t i = 0; i < GetMarkCount(); ++i)
    {
        SdrMark* pMark = GetMark(i);

        if(!pPV || pMark->GetPageView() == pPV)
        {
            if(pMark->GetMarkedSdrObj())
            {
                tools::Rectangle aR(pMark->GetMarkedSdrObj()->GetSnapRect());

                if(bFnd)
                {
                    rRect.Union(aR);
                }
                else
                {
                    rRect = aR;
                    bFnd = true;
                }
            }
        }
    }

    return bFnd;
}


namespace sdr
{
    ViewSelection::ViewSelection()
    :   mbEdgesOfMarkedNodesDirty(false)
    {
    }

    void ViewSelection::SetEdgesOfMarkedNodesDirty()
    {
        if(!mbEdgesOfMarkedNodesDirty)
        {
            mbEdgesOfMarkedNodesDirty = true;
            maEdgesOfMarkedNodes.Clear();
            maMarkedEdgesOfMarkedNodes.Clear();
            maAllMarkedObjects.clear();
        }
    }

    const SdrMarkList& ViewSelection::GetEdgesOfMarkedNodes() const
    {
        if(mbEdgesOfMarkedNodesDirty)
        {
            const_cast<ViewSelection*>(this)->ImpForceEdgesOfMarkedNodes();
        }

        return maEdgesOfMarkedNodes;
    }

    const SdrMarkList& ViewSelection::GetMarkedEdgesOfMarkedNodes() const
    {
        if(mbEdgesOfMarkedNodesDirty)
        {
            const_cast<ViewSelection*>(this)->ImpForceEdgesOfMarkedNodes();
        }

        return maMarkedEdgesOfMarkedNodes;
    }

    const std::vector<SdrObject*>& ViewSelection::GetAllMarkedObjects() const
    {
        if(mbEdgesOfMarkedNodesDirty)
            const_cast<ViewSelection*>(this)->ImpForceEdgesOfMarkedNodes();

        return maAllMarkedObjects;
    }

    void ViewSelection::ImplCollectCompleteSelection(SdrObject* pObj)
    {
        if(!pObj)
            return;

        bool bIsGroup(pObj->IsGroupObject());

        if(bIsGroup && DynCastE3dObject(pObj) != nullptr && DynCastE3dScene(pObj) == nullptr)
        {
            bIsGroup = false;
        }

        if(bIsGroup)
        {
            SdrObjList* pList = pObj->GetSubList();

            for (const rtl::Reference<SdrObject>& pObj2 : *pList)
                ImplCollectCompleteSelection(pObj2.get());
        }

        maAllMarkedObjects.push_back(pObj);
    }

    void ViewSelection::ImpForceEdgesOfMarkedNodes()
    {
        if(!mbEdgesOfMarkedNodesDirty)
            return;

        mbEdgesOfMarkedNodesDirty = false;
        maMarkedObjectList.ForceSort();
        maEdgesOfMarkedNodes.Clear();
        maMarkedEdgesOfMarkedNodes.Clear();
        maAllMarkedObjects.clear();

        // GetMarkCount after ForceSort
        const size_t nMarkCount(maMarkedObjectList.GetMarkCount());

        for(size_t a = 0; a < nMarkCount; ++a)
        {
            SdrObject* pCandidate = maMarkedObjectList.GetMark(a)->GetMarkedSdrObj();
            if(!pCandidate)
                continue;

            // build transitive hull
            ImplCollectCompleteSelection(pCandidate);

            // travel over broadcaster/listener to access edges connected to the selected object
            const SfxBroadcaster* pBC = pCandidate->GetBroadcaster();
            if(!pBC)
                continue;

            pBC->ForAllListeners(
                [this, &pCandidate, &a] (SfxListener* pLst)
                {
                    SdrEdgeObj* pEdge = dynamic_cast<SdrEdgeObj*>( pLst );

                    if(pEdge && pEdge->IsInserted() && pEdge->getSdrPageFromSdrObject() == pCandidate->getSdrPageFromSdrObject())
                    {
                        SdrMark aM(pEdge, maMarkedObjectList.GetMark(a)->GetPageView());

                        if(pEdge->GetConnectedNode(true) == pCandidate)
                        {
                            aM.SetCon1(true);
                        }

                        if(pEdge->GetConnectedNode(false) == pCandidate)
                        {
                            aM.SetCon2(true);
                        }

                        if(SAL_MAX_SIZE == maMarkedObjectList.FindObject(pEdge))
                        {
                            // check if it itself is selected
                            maEdgesOfMarkedNodes.InsertEntry(aM);
                        }
                        else
                        {
                            maMarkedEdgesOfMarkedNodes.InsertEntry(aM);
                        }
                    }
                    return false;
                });
        }
        maEdgesOfMarkedNodes.ForceSort();
        maMarkedEdgesOfMarkedNodes.ForceSort();
    }
} // end of namespace sdr

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