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

#include <doc.hxx>
#include <docsh.hxx>
#include <view.hxx>
#include <drawdoc.hxx>
#include <ndarr.hxx>
#include <pam.hxx>
#include <swundo.hxx>
#include <UndoCore.hxx>
#include <editsh.hxx>
#include <unobaseclass.hxx>
#include <IDocumentDrawModelAccess.hxx>
#include <IDocumentRedlineAccess.hxx>
#include <IDocumentState.hxx>
#include <comphelper/lok.hxx>
#include <assert.h>

#include <sfx2/viewfrm.hxx>
#include <sfx2/bindings.hxx>

using namespace ::com::sun::star;

// the undo array should never grow beyond this limit:
#define UNDO_ACTION_LIMIT (USHRT_MAX - 1000)

namespace sw {

UndoManager::UndoManager(std::shared_ptr<SwNodes> const & xUndoNodes,
            IDocumentDrawModelAccess & rDrawModelAccess,
            IDocumentRedlineAccess & rRedlineAccess,
            IDocumentState & rState)
    :   m_rDrawModelAccess(rDrawModelAccess)
    ,   m_rRedlineAccess(rRedlineAccess)
    ,   m_rState(rState)
    ,   m_xUndoNodes(xUndoNodes)
    ,   m_bGroupUndo(true)
    ,   m_bDrawUndo(true)
    ,   m_bRepair(false)
    ,   m_bLockUndoNoModifiedPosition(false)
    ,   m_isAddWithIgnoreRepeat(false)
    ,   m_UndoSaveMark(MARK_INVALID)
    ,   m_pDocShell(nullptr)
    ,   m_pView(nullptr)
{
    assert(bool(m_xUndoNodes));
    // writer expects it to be disabled initially
    // Undo is enabled by SwEditShell constructor
    SdrUndoManager::EnableUndo(false);
}

SwNodes const& UndoManager::GetUndoNodes() const
{
    return *m_xUndoNodes;
}

SwNodes      & UndoManager::GetUndoNodes()
{
    return *m_xUndoNodes;
}

bool UndoManager::IsUndoNodes(SwNodes const& rNodes) const
{
    return & rNodes == m_xUndoNodes.get();
}

void UndoManager::SetDocShell(SwDocShell* pDocShell)
{
    m_pDocShell = pDocShell;
}

void UndoManager::SetView(SwView* pView)
{
    m_pView = pView;
}

size_t UndoManager::GetUndoActionCount(const bool bCurrentLevel) const
{
    size_t nRet = SdrUndoManager::GetUndoActionCount(bCurrentLevel);
    if (!comphelper::LibreOfficeKit::isActive() || !m_pView)
        return nRet;

    if (!nRet || !SdrUndoManager::GetUndoActionCount())
        return nRet;

    const SfxUndoAction* pAction = SdrUndoManager::GetUndoAction();
    if (!pAction)
        return nRet;

    if (!m_bRepair)
    {
        // If another view created the last undo action, prevent undoing it from this view.
        ViewShellId nViewShellId = m_pView->GetViewShellId();
        if (pAction->GetViewShellId() != nViewShellId)
            nRet = 0;
    }

    return nRet;
}

size_t UndoManager::GetRedoActionCount(const bool bCurrentLevel) const
{
    size_t nRet = SdrUndoManager::GetRedoActionCount(bCurrentLevel);
    if (!comphelper::LibreOfficeKit::isActive() || !m_pView)
        return nRet;

    if (!nRet || !SdrUndoManager::GetRedoActionCount())
        return nRet;

    const SfxUndoAction* pAction = SdrUndoManager::GetRedoAction();
    if (!pAction)
        return nRet;

    if (!m_bRepair)
    {
        // If another view created the first redo action, prevent redoing it from this view.
        ViewShellId nViewShellId = m_pView->GetViewShellId();
        if (pAction->GetViewShellId() != nViewShellId)
            nRet = 0;
    }

    return nRet;
}

void UndoManager::DoUndo(bool const bDoUndo)
{
    if(!isTextEditActive())
    {
        EnableUndo(bDoUndo);

        SwDrawModel*const pSdrModel = m_rDrawModelAccess.GetDrawModel();
        if( pSdrModel )
        {
            pSdrModel->EnableUndo(bDoUndo);
        }
    }
}

bool UndoManager::DoesUndo() const
{
    if(isTextEditActive())
    {
        return false;
    }
    else
    {
        return IsUndoEnabled();
    }
}

void UndoManager::DoGroupUndo(bool const bDoUndo)
{
    m_bGroupUndo = bDoUndo;
}

bool UndoManager::DoesGroupUndo() const
{
    return m_bGroupUndo;
}

void UndoManager::DoDrawUndo(bool const bDoUndo)
{
    m_bDrawUndo = bDoUndo;
}

bool UndoManager::DoesDrawUndo() const
{
    return m_bDrawUndo;
}

void UndoManager::DoRepair(bool bRepair)
{
    m_bRepair = bRepair;
}

bool UndoManager::DoesRepair() const
{
    return m_bRepair;
}

bool UndoManager::IsUndoNoResetModified() const
{
    return MARK_INVALID == m_UndoSaveMark;
}

void UndoManager::SetUndoNoResetModified()
{
    if (MARK_INVALID != m_UndoSaveMark)
    {
        RemoveMark(m_UndoSaveMark);
        m_UndoSaveMark = MARK_INVALID;
    }
}

void UndoManager::SetUndoNoModifiedPosition()
{
    if (!m_bLockUndoNoModifiedPosition)
    {
        m_UndoSaveMark = MarkTopUndoAction();
    }
}

void UndoManager::LockUndoNoModifiedPosition()
{
    m_bLockUndoNoModifiedPosition = true;
}

void UndoManager::UnLockUndoNoModifiedPosition()
{
    m_bLockUndoNoModifiedPosition = false;
}

SwUndo* UndoManager::GetLastUndo()
{
    if (!SdrUndoManager::GetUndoActionCount())
    {
        return nullptr;
    }
    SfxUndoAction *const pAction( SdrUndoManager::GetUndoAction() );
    return dynamic_cast<SwUndo*>(pAction);
}

void UndoManager::AppendUndo(std::unique_ptr<SwUndo> pUndo)
{
    AddUndoAction(std::move(pUndo));
}

void UndoManager::ClearRedo()
{
    return SdrUndoManager::ImplClearRedo_NoLock(TopLevel);
}

void UndoManager::DelAllUndoObj()
{
    ::sw::UndoGuard const undoGuard(*this);

    SdrUndoManager::ClearAllLevels();

    m_UndoSaveMark = MARK_INVALID;
}

SwUndoId
UndoManager::StartUndo(SwUndoId const i_eUndoId,
        SwRewriter const*const pRewriter)
{
    if (!IsUndoEnabled())
    {
        return SwUndoId::EMPTY;
    }

    SwUndoId const eUndoId( (i_eUndoId == SwUndoId::EMPTY) ? SwUndoId::START : i_eUndoId );

    assert(SwUndoId::END != eUndoId);
    OUString comment( (SwUndoId::START == eUndoId)
        ?   OUString("??")
        :   GetUndoComment(eUndoId) );
    if (pRewriter)
    {
        assert(SwUndoId::START != eUndoId);
        comment = pRewriter->Apply(comment);
    }

    ViewShellId nViewShellId(-1);
    if (m_pDocShell)
    {
        if (const SwView* pView = m_pDocShell->GetView())
            nViewShellId = pView->GetViewShellId();
    }
    SdrUndoManager::EnterListAction(comment, comment, static_cast<sal_uInt16>(eUndoId), nViewShellId);

    return eUndoId;
}

SwUndoId
UndoManager::EndUndo(SwUndoId eUndoId, SwRewriter const*const pRewriter)
{
    if (!IsUndoEnabled())
    {
        return SwUndoId::EMPTY;
    }

    if ((eUndoId == SwUndoId::EMPTY) || (SwUndoId::START == eUndoId))
           eUndoId = SwUndoId::END;
    OSL_ENSURE(!((SwUndoId::END == eUndoId) && pRewriter),
                "EndUndo(): no Undo ID, but rewriter given?");

    SfxUndoAction *const pLastUndo(
        (0 == SdrUndoManager::GetUndoActionCount())
            ? nullptr : SdrUndoManager::GetUndoAction() );

    int const nCount = LeaveListAction();

    if (nCount) // otherwise: empty list action not inserted!
    {
        assert(pLastUndo);
        assert(SwUndoId::START != eUndoId);
        auto pListAction = dynamic_cast<SfxListUndoAction*>(SdrUndoManager::GetUndoAction());
        assert(pListAction);
        if (SwUndoId::END != eUndoId)
        {
            OSL_ENSURE(static_cast<SwUndoId>(pListAction->GetId()) == eUndoId,
                    "EndUndo(): given ID different from StartUndo()");
            // comment set by caller of EndUndo
            OUString comment = GetUndoComment(eUndoId);
            if (pRewriter)
            {
                comment = pRewriter->Apply(comment);
            }
            pListAction->SetComment(comment);
        }
        else if (SwUndoId::START != static_cast<SwUndoId>(pListAction->GetId()))
        {
            // comment set by caller of StartUndo: nothing to do here
        }
        else if (pLastUndo)
        {
            // comment was not set at StartUndo or EndUndo:
            // take comment of last contained action
            // (note that this works recursively, i.e. the last contained
            // action may be a list action created by StartUndo/EndUndo)
            OUString const comment(pLastUndo->GetComment());
            pListAction->SetComment(comment);
        }
        else
        {
            OSL_ENSURE(false, "EndUndo(): no comment?");
        }
    }

    return eUndoId;
}

bool
UndoManager::GetLastUndoInfo(
        OUString *const o_pStr, SwUndoId *const o_pId, const SwView* pView) const
{
    // this is actually expected to work on the current level,
    // but that was really not obvious from the previous implementation...
    if (!SdrUndoManager::GetUndoActionCount())
    {
        return false;
    }

    SfxUndoAction *const pAction( SdrUndoManager::GetUndoAction() );

    if (comphelper::LibreOfficeKit::isActive() && !m_bRepair)
    {
        // If another view created the undo action, prevent undoing it from this view.
        ViewShellId nViewShellId = pView ? pView->GetViewShellId() : m_pDocShell->GetView()->GetViewShellId();
        if (pAction->GetViewShellId() != nViewShellId)
        {
            if (o_pId)
            {
                 *o_pId = SwUndoId::CONFLICT;
            }
            return false;
        }
    }

    if (o_pStr)
    {
        *o_pStr = pAction->GetComment();
    }
    if (o_pId)
    {
        if (auto pListAction = dynamic_cast<const SfxListUndoAction*>(pAction))
            *o_pId = static_cast<SwUndoId>(pListAction->GetId());
        else if (auto pSwAction = dynamic_cast<const SwUndo*>(pAction))
            *o_pId = pSwAction->GetId();
        else
            *o_pId = SwUndoId::EMPTY;
    }

    return true;
}

SwUndoComments_t UndoManager::GetUndoComments() const
{
    OSL_ENSURE(!SdrUndoManager::IsInListAction(),
            "GetUndoComments() called while in list action?");

    SwUndoComments_t ret;
    const size_t nUndoCount(SdrUndoManager::GetUndoActionCount(TopLevel));
    for (size_t n = 0; n < nUndoCount; ++n)
    {
        OUString const comment(
                SdrUndoManager::GetUndoActionComment(n, TopLevel));
        ret.push_back(comment);
    }

    return ret;
}

bool UndoManager::GetFirstRedoInfo(OUString *const o_pStr,
                                   SwUndoId *const o_pId,
                                   const SwView* pView) const
{
    if (!SdrUndoManager::GetRedoActionCount())
    {
        return false;
    }

    SfxUndoAction *const pAction( SdrUndoManager::GetRedoAction() );
    if ( pAction == nullptr )
    {
        return false;
    }

    if (comphelper::LibreOfficeKit::isActive() && !m_bRepair)
    {
        // If another view created the undo action, prevent redoing it from this view.
        ViewShellId nViewShellId = pView ? pView->GetViewShellId() : m_pDocShell->GetView()->GetViewShellId();
        if (pAction->GetViewShellId() != nViewShellId)
        {
            if (o_pId)
            {
                 *o_pId = SwUndoId::CONFLICT;
            }
            return false;
        }
    }

    if (o_pStr)
    {
        *o_pStr = pAction->GetComment();
    }
    if (o_pId)
    {
        if (auto pListAction = dynamic_cast<const SfxListUndoAction*>(pAction))
            *o_pId = static_cast<SwUndoId>(pListAction->GetId());
        else if (auto pSwAction = dynamic_cast<const SwUndo*>(pAction))
            *o_pId = pSwAction->GetId();
        else
            *o_pId = SwUndoId::EMPTY;
    }

    return true;
}

SwUndoComments_t UndoManager::GetRedoComments() const
{
    OSL_ENSURE(!SdrUndoManager::IsInListAction(),
            "GetRedoComments() called while in list action?");

    SwUndoComments_t ret;
    const size_t nRedoCount(SdrUndoManager::GetRedoActionCount(TopLevel));
    for (size_t n = 0; n < nRedoCount; ++n)
    {
        OUString const comment(
                SdrUndoManager::GetRedoActionComment(n, TopLevel));
        ret.push_back(comment);
    }

    return ret;
}

SwUndoId UndoManager::GetRepeatInfo(OUString *const o_pStr) const
{
    SwUndoId nRepeatId(SwUndoId::EMPTY);
    GetLastUndoInfo(o_pStr, & nRepeatId);
    if( SwUndoId::REPEAT_START <= nRepeatId && SwUndoId::REPEAT_END > nRepeatId )
    {
        return nRepeatId;
    }
    if (o_pStr) // not repeatable -> clear comment
    {
        o_pStr->clear();
    }
    return SwUndoId::EMPTY;
}

SwUndo * UndoManager::RemoveLastUndo()
{
    if (SdrUndoManager::GetRedoActionCount() ||
        SdrUndoManager::GetRedoActionCount(TopLevel))
    {
        OSL_ENSURE(false, "RemoveLastUndoAction(): there are Redo actions?");
        return nullptr;
    }
    if (!SdrUndoManager::GetUndoActionCount())
    {
        OSL_ENSURE(false, "RemoveLastUndoAction(): no Undo actions");
        return nullptr;
    }
    SfxUndoAction *const pLastUndo(GetUndoAction());
    SdrUndoManager::RemoveLastUndoAction();
    return dynamic_cast<SwUndo *>(pLastUndo);
}

// SfxUndoManager

void UndoManager::AddUndoAction(std::unique_ptr<SfxUndoAction> pAction, bool bTryMerge)
{
    SwUndo *const pUndo( dynamic_cast<SwUndo *>(pAction.get()) );
    if (pUndo)
    {
        if (RedlineFlags::NONE == pUndo->GetRedlineFlags())
        {
            pUndo->SetRedlineFlags( m_rRedlineAccess.GetRedlineFlags() );
        }
        if (m_isAddWithIgnoreRepeat)
        {
            pUndo->IgnoreRepeat();
        }
    }
    SdrUndoManager::AddUndoAction(std::move(pAction), bTryMerge);
    if (m_pDocShell)
    {
        SfxViewFrame* pViewFrame = SfxViewFrame::GetFirst( m_pDocShell );
        while ( pViewFrame )
        {
            pViewFrame->GetBindings().Invalidate( SID_UNDO );
            pViewFrame->GetBindings().Invalidate( SID_REDO );
            pViewFrame = SfxViewFrame::GetNext( *pViewFrame, m_pDocShell );
        }
    }

    // if the undo nodes array is too large, delete some actions
    while (UNDO_ACTION_LIMIT < GetUndoNodes().Count())
    {
        RemoveOldestUndoAction();
    }
}

namespace {

class CursorGuard
{
public:
    CursorGuard(SwEditShell & rShell, bool const bSave)
        : m_rShell(rShell)
        , m_bSaveCursor(bSave)
    {
        if (m_bSaveCursor)
        {
            m_rShell.Push(); // prevent modification of current cursor
        }
    }
    ~CursorGuard() COVERITY_NOEXCEPT_FALSE
    {
        if (m_bSaveCursor)
        {
            m_rShell.Pop(SwCursorShell::PopMode::DeleteCurrent);
        }
    }
private:
    SwEditShell & m_rShell;
    bool const m_bSaveCursor;
};

}

bool UndoManager::impl_DoUndoRedo(UndoOrRedoType undoOrRedo)
{
    SwDoc & rDoc(*GetUndoNodes().GetDoc());

    UnoActionContext c(& rDoc); // exception-safe StartAllAction/EndAllAction

    SwEditShell *const pEditShell( rDoc.GetEditShell() );

    OSL_ENSURE(pEditShell, "sw::UndoManager needs a SwEditShell!");
    if (!pEditShell)
    {
        throw uno::RuntimeException();
    }

    // in case the model has controllers locked, the Undo should not
    // change the view cursors!
    bool const bSaveCursors(pEditShell->CursorsLocked());
    CursorGuard aCursorGuard(*pEditShell, bSaveCursors);
    if (!bSaveCursors)
    {
        // (in case Undo was called via API) clear the cursors:
        pEditShell->KillPams();
        pEditShell->SetMark();
        pEditShell->ClearMark();
    }

    bool bRet(false);

    ::sw::UndoRedoContext context(rDoc, *pEditShell);

    // N.B. these may throw!
    if (UndoOrRedoType::Undo == undoOrRedo)
    {
        bRet = SdrUndoManager::UndoWithContext(context);
    }
    else
    {
        bRet = SdrUndoManager::RedoWithContext(context);
    }

    if (bRet)
    {
        // if we are at the "last save" position, the document is not modified
        if (SdrUndoManager::HasTopUndoActionMark(m_UndoSaveMark))
        {
            m_rState.ResetModified();
        }
        else
        {
            m_rState.SetModified();
        }
    }

    pEditShell->HandleUndoRedoContext(context);

    return bRet;
}

bool UndoManager::Undo()
{
    if(isTextEditActive())
    {
        return SdrUndoManager::Undo();
    }
    else
    {
        return impl_DoUndoRedo(UndoOrRedoType::Undo);
    }
}

bool UndoManager::Redo()
{
    if(isTextEditActive())
    {
        return SdrUndoManager::Redo();
    }
    else
    {
        return impl_DoUndoRedo(UndoOrRedoType::Redo);
    }
}

void UndoManager::EmptyActionsChanged()
{
    if (m_pDocShell)
    {
        m_pDocShell->Broadcast(SfxHint(SfxHintId::DocumentRepair));
    }
}

/** N.B.: this does _not_ call SdrUndoManager::Repeat because it is not
          possible to wrap a list action around it:
          calling EnterListAction here will cause SdrUndoManager::Repeat
          to repeat the list action!
 */
bool UndoManager::Repeat(::sw::RepeatContext & rContext,
                         sal_uInt16 const nRepeatCount)
{
    if (SdrUndoManager::IsInListAction())
    {
        OSL_ENSURE(false, "repeat in open list action???");
        return false;
    }
    if (!SdrUndoManager::GetUndoActionCount(TopLevel))
    {
        return false;
    }
    SfxUndoAction *const pRepeatAction(GetUndoAction());
    assert(pRepeatAction);
    if (!pRepeatAction->CanRepeat(rContext))
    {
        return false;
    }

    OUString const comment(pRepeatAction->GetComment());
    OUString const rcomment(pRepeatAction->GetRepeatComment(rContext));
    SwUndoId nId;
    if (auto const* const pSwAction = dynamic_cast<SwUndo*>(pRepeatAction))
        nId = pSwAction->GetId();
    else if (auto const* const pListAction = dynamic_cast<SfxListUndoAction*>(pRepeatAction))
        nId = static_cast<SwUndoId>(pListAction->GetId());
    else
        return false;
    if (DoesUndo())
    {
        ViewShellId nViewShellId(-1);
        if (m_pDocShell)
        {
            if (const SwView* pView = m_pDocShell->GetView())
                nViewShellId = pView->GetViewShellId();
        }
        EnterListAction(comment, rcomment, static_cast<sal_uInt16>(nId), nViewShellId);
    }

    SwPaM* pTmp = rContext.m_pCurrentPaM;
    for(SwPaM& rPaM : rContext.GetRepeatPaM().GetRingContainer())
    {    // iterate over ring
        rContext.m_pCurrentPaM = &rPaM;
        if (DoesUndo() && & rPaM != pTmp)
        {
            m_isAddWithIgnoreRepeat = true;
        }
        for (sal_uInt16 nRptCnt = nRepeatCount; nRptCnt > 0; --nRptCnt)
        {
            pRepeatAction->Repeat(rContext);
        }
        if (DoesUndo() && & rPaM != pTmp)
        {
            m_isAddWithIgnoreRepeat = false;
        }
        rContext.m_bDeleteRepeated = false; // reset for next PaM
    }
    rContext.m_pCurrentPaM = pTmp;

    if (DoesUndo())
    {
        LeaveListAction();
    }
    return true;
}

} // namespace sw

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