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

#include <AnnotationWin.hxx>

#include <PostItMgr.hxx>

#include <strings.hrc>

#include <uiobject.hxx>

#include <vcl/svapp.hxx>
#include <vcl/uitest/logger.hxx>
#include <vcl/uitest/eventdescription.hxx>

#include <svl/undo.hxx>
#include <svtools/svparser.hxx>
#include <unotools/localedatawrapper.hxx>
#include <unotools/syslocale.hxx>
#include <svl/languageoptions.hxx>
#include <osl/diagnose.h>

#include <editeng/eeitem.hxx>
#include <editeng/postitem.hxx>
#include <editeng/fhgtitem.hxx>
#include <editeng/langitem.hxx>
#include <editeng/editund2.hxx>

#include <editeng/editview.hxx>
#include <editeng/outliner.hxx>
#include <editeng/editeng.hxx>
#include <editeng/editobj.hxx>
#include <editeng/outlobj.hxx>

#include <comphelper/lok.hxx>
#include <comphelper/random.hxx>
#include <docufld.hxx>
#include <txtfld.hxx>
#include <ndtxt.hxx>
#include <view.hxx>
#include <viewopt.hxx>
#include <wrtsh.hxx>
#include <docsh.hxx>
#include <doc.hxx>
#include <IDocumentUndoRedo.hxx>
#if ENABLE_YRS
#include <IDocumentState.hxx>
#endif
#include <SwUndoField.hxx>
#include <edtwin.hxx>
#include "ShadowOverlayObject.hxx"
#include "AnchorOverlayObject.hxx"
#include "OverlayRanges.hxx"
#include "SidebarTxtControl.hxx"
#include "SidebarWinAcc.hxx"

#include <memory>

namespace{

void collectUIInformation( const OUString& aevent , const OUString& aID )
{
    EventDescription aDescription;
    aDescription.aID =  aID;
    aDescription.aParameters = {{"" ,  ""}};
    aDescription.aAction = aevent;
    aDescription.aParent = "MainWindow";
    aDescription.aKeyWord = "SwEditWinUIObject";
    UITestLogger::getInstance().logEvent(aDescription);
}

}

namespace SwPostItHelper {

void ImportHTML(Outliner& rOutliner, const OUString& rHtml)
{
    OString sHtmlContent(rHtml.toUtf8());
    SvMemoryStream aHTMLStream(const_cast<char*>(sHtmlContent.getStr()),
                               sHtmlContent.getLength(), StreamMode::READ);
    SvKeyValueIteratorRef xValues(new SvKeyValueIterator);
    // Insert newlines for divs, not normally done, so to keep things simple
    // only enable that for this case.
    xValues->Append(SvKeyValue("newline-on-div", "true"));
    xValues->Append(SvKeyValue("content-type", "text/html;charset=utf-8"));
    rOutliner.Read(aHTMLStream, "", EETextFormat::Html, xValues.get());
}

}

namespace sw::annotation {

// see AnnotationContents in sd for something similar
SwAnnotationWin::SwAnnotationWin( SwEditWin& rEditWin,
                                  SwPostItMgr& aMgr,
                                  SwAnnotationItem& rSidebarItem,
                                  SwFormatField* aField )
    : InterimItemWindow(&rEditWin, u"modules/swriter/ui/annotation.ui"_ustr, u"Annotation"_ustr)
    , mrMgr(aMgr)
    , mrView(rEditWin.GetView())
    , mnDeleteEventId(nullptr)
    , meSidebarPosition(sw::sidebarwindows::SidebarPosition::NONE)
    , mPageBorder(0)
    , mbAnchorRectChanged(false)
    , mbResolvedStateUpdated(false)
    , mbMouseOver(false)
    , mLayoutStatus(SwPostItHelper::INVISIBLE)
    , mbReadonly(false)
    , mbIsFollow(false)
    , mpSidebarItem(&rSidebarItem)
    , mpAnchorFrame(rSidebarItem.maLayoutInfo.mpAnchorFrame)
    , mpFormatField(aField)
    , mpField( static_cast<SwPostItField*>(aField->GetField()))
{
    set_id("Comment"+OUString::number(mpField->GetPostItId()));

    m_xContainer->connect_mouse_move(LINK(this, SwAnnotationWin, MouseMoveHdl));

    mpShadow = sidebarwindows::ShadowOverlayObject::CreateShadowOverlayObject( mrView );
    if ( mpShadow )
    {
        mpShadow->setVisible(false);
    }

#if !ENABLE_WASM_STRIP_ACCESSIBILITY
    if (rSidebarItem.maLayoutInfo.mpAnchorFrame)
    {
        mrMgr.ConnectSidebarWinToFrame( *(rSidebarItem.maLayoutInfo.mpAnchorFrame),
                                      mpSidebarItem->GetFormatField(),
                                      *this );
    }
#endif

    if (SupportsDoubleBuffering())
        // When double-buffering, allow parents to paint on our area. That's
        // necessary when parents paint the complete buffer.
        SetParentClipMode(ParentClipMode::NoClip);
}

SwAnnotationWin::~SwAnnotationWin()
{
    disposeOnce();
}

void SwAnnotationWin::dispose()
{
#if !ENABLE_WASM_STRIP_ACCESSIBILITY
    mrMgr.DisconnectSidebarWinFromFrame( *(mpSidebarItem->maLayoutInfo.mpAnchorFrame),
                                       *this );
#endif
    Disable();

    mxSidebarTextControlWin.reset();
    mxSidebarTextControl.reset();

    mxMetadataAuthor.reset();
    mxMetadataResolved.reset();
    mxMetadataDate.reset();
    mxVScrollbar.reset();

    mpAnchor.reset();
    mpShadow.reset();

    mpTextRangeOverlay.reset();

    mxMenuButton.reset();

    if (mnDeleteEventId)
        Application::RemoveUserEvent(mnDeleteEventId);

    mpOutliner.reset();
    mpOutlinerView.reset();

    InterimItemWindow::dispose();
}

void SwAnnotationWin::SetPostItText()
{
    //If the cursor was visible, then make it visible again after
    //changing text, e.g. fdo#33599
    vcl::Cursor *pCursor = GetOutlinerView()->GetEditView().GetCursor();
    bool bCursorVisible = pCursor && pCursor->IsVisible();

    //If the new text is the same as the old text, keep the same insertion
    //point .e.g. fdo#33599
    mpField = static_cast<SwPostItField*>(mpFormatField->GetField());
    OUString sNewText = mpField->GetPar2();
    bool bTextUnchanged = sNewText == mpOutliner->GetEditEngine().GetText();
    ESelection aOrigSelection(GetOutlinerView()->GetEditView().GetSelection());

    // get text from SwPostItField and insert into our textview
    mpOutliner->SetModifyHdl( Link<LinkParamNone*,void>() );
    mpOutliner->EnableUndo( false );
#if ENABLE_YRS
    auto const mode = mrView.GetDocShell()->GetDoc()->getIDocumentState().SetYrsMode(IYrsTransactionSupplier::Mode::Replay);
#endif
    if( mpField->GetTextObject() )
        mpOutliner->SetText( *mpField->GetTextObject() );
    else
    {
        mpOutliner->Clear();
        GetOutlinerView()->SetStyleSheet(SwResId(STR_POOLCOLL_COMMENT));
        GetOutlinerView()->InsertText(sNewText);
    }
#if ENABLE_YRS
    mrView.GetDocShell()->GetDoc()->getIDocumentState().SetYrsMode(mode);
#endif

    mpOutliner->ClearModifyFlag();
    mpOutliner->GetUndoManager().Clear();
    mpOutliner->EnableUndo( true );
    mpOutliner->SetModifyHdl( LINK( this, SwAnnotationWin, ModifyHdl ) );
    if (bTextUnchanged)
        GetOutlinerView()->GetEditView().SetSelection(aOrigSelection);
    if (bCursorVisible)
        GetOutlinerView()->ShowCursor();
    Invalidate();
}

void SwAnnotationWin::GeneratePostItName()
{
    if (mpField && mpField->GetName().isEmpty())
    {
        mpField->SetName(sw::mark::MarkBase::GenerateNewName(u"__Annotation__"));
    }
}

void SwAnnotationWin::SetResolved(bool resolved)
{
    bool oldState = IsResolved();
    static_cast<SwPostItField*>(mpFormatField->GetField())->SetResolved(resolved);
    if (SwWrtShell* pWrtShell = mrView.GetWrtShellPtr())
    {
        const SwViewOption* pVOpt = pWrtShell->GetViewOptions();
        mpSidebarItem->mbShow = !IsResolved() || (pVOpt->IsResolvedPostIts());
    }

    mpTextRangeOverlay.reset();

    if(IsResolved())
        mxMetadataResolved->show();
    else
        mxMetadataResolved->hide();

    if(IsResolved() != oldState)
    {
        mbResolvedStateUpdated = true;
#if ENABLE_YRS
        // for undo, before UpdateData()
        mrView.GetDocShell()->GetDoc()->getIDocumentState().YrsNotifySetResolved(
            GetOutlinerView()->GetEditView().GetYrsCommentId(),
            *static_cast<SwPostItField const*>(mpFormatField->GetField()));
#endif
    }
    UpdateData();
    Invalidate();
    collectUIInformation(u"SETRESOLVED"_ustr,get_id());
}

void SwAnnotationWin::ToggleResolved()
{
    SetResolved(!IsResolved());
}

void SwAnnotationWin::ToggleResolvedForThread()
{
    auto pTop = GetTopReplyNote();
    pTop->ToggleResolved();
    mrMgr.UpdateResolvedStatus(pTop);
    mrMgr.LayoutPostIts();
}

sal_uInt32 SwAnnotationWin::GetParaId()
{
    auto pField = static_cast<SwPostItField*>(mpFormatField->GetField());
    auto nParaId = pField->GetParaId();
    if (nParaId == 0)
    {
        // The parent annotation does not have a paraId. This happens when the annotation was just
        // created, and not imported. paraIds are regenerated upon export, thus this new paraId
        // is only generated so that children annotations can refer to it
        nParaId = CreateUniqueParaId();
        pField->SetParaId(nParaId);
    }
    return nParaId;
}

sal_uInt32 SwAnnotationWin::CreateUniqueParaId()
{
    return comphelper::rng::uniform_uint_distribution(0, std::numeric_limits<sal_uInt32>::max());
}

void SwAnnotationWin::DeleteThread()
{
    // Go to the top and delete each comment one by one
    SwAnnotationWin* topNote = GetTopReplyNote();
    for (SwAnnotationWin* current = topNote;;)
    {
        SwAnnotationWin* next = mrMgr.GetNextPostIt(KEY_PAGEDOWN, current);
        current->mnDeleteEventId = Application::PostUserEvent( LINK( current, SwAnnotationWin, DeleteHdl), nullptr, true );
        if (!next || next->GetTopReplyNote() != topNote)
            return;
        current = next;
    }
}

bool SwAnnotationWin::IsResolved() const
{
    return static_cast<SwPostItField*>(mpFormatField->GetField())->GetResolved();
}

bool SwAnnotationWin::IsThreadResolved()
{
    /// First Get the top note
    // then iterate downwards checking resolved status
    SwAnnotationWin* topNote = GetTopReplyNote();
    for (SwAnnotationWin* current = topNote;;)
    {
        if (!current->IsResolved())
            return false;
        current = mrMgr.GetNextPostIt(KEY_PAGEDOWN, current);
        if (!current || current->GetTopReplyNote() != topNote)
            return true;
    }
}

bool SwAnnotationWin::IsRootNote() const
{
    return static_cast<SwPostItField*>(mpFormatField->GetField())->GetParentPostItId() == 0;
}

void SwAnnotationWin::SetAsRoot()
{
    if (!IsRootNote())
    {
        SwPostItField* pPostIt = static_cast<SwPostItField*>(mpFormatField->GetField());
        pPostIt->SetParentId(0);
        pPostIt->SetParentPostItId(0);
        pPostIt->SetParentName(SwMarkName());
        mrMgr.MoveSubthreadToRoot(this);
        mpFormatField->Broadcast(SwFormatFieldHint(nullptr, SwFormatFieldHintWhich::CHANGED));
    }
}

void SwAnnotationWin::UpdateData()
{
    if ( mpOutliner->IsModified() || mbResolvedStateUpdated )
    {
        IDocumentUndoRedo & rUndoRedo(
            mrView.GetDocShell()->GetDoc()->GetIDocumentUndoRedo());
        std::unique_ptr<SwField> pOldField;
        if (rUndoRedo.DoesUndo())
        {
            pOldField = mpField->Copy();
        }
        mpField->SetPar2(mpOutliner->GetEditEngine().GetText());
        mpField->SetTextObject(mpOutliner->CreateParaObject());
        if (rUndoRedo.DoesUndo())
        {
            SwTextField *const pTextField = mpFormatField->GetTextField();
            SwPosition aPosition( pTextField->GetTextNode(), pTextField->GetStart() );
            rUndoRedo.AppendUndo(
                std::make_unique<SwUndoFieldFromDoc>(aPosition, *pOldField, *mpField, true));
#if ENABLE_YRS
            mrView.GetDocShell()->GetDoc()->getIDocumentState().YrsEndUndo();
#endif
        }
        // so we get a new layout of notes (anchor position is still the same and we would otherwise not get one)
        mrMgr.SetLayout();
        // #i98686# if we have several views, all notes should update their text
        if(mbResolvedStateUpdated)
            mpFormatField->Broadcast(SwFormatFieldHint( nullptr, SwFormatFieldHintWhich::RESOLVED));
        else
            mpFormatField->Broadcast(SwFormatFieldHint( nullptr, SwFormatFieldHintWhich::CHANGED));
        mrView.GetDocShell()->SetModified();

        UpdateMenu();
    }
    mpOutliner->ClearModifyFlag();
    mpOutliner->GetUndoManager().Clear();
    mbResolvedStateUpdated = false;
}

void SwAnnotationWin::Delete()
{
    collectUIInformation(u"DELETE"_ustr,get_id());
    SwWrtShell* pWrtShell = mrView.GetWrtShellPtr();
    if (!(pWrtShell && pWrtShell->GotoField(*mpFormatField)))
        return;

    if ( mrMgr.GetActiveSidebarWin() == this)
    {
        mrMgr.SetActiveSidebarWin(nullptr);
        // if the note is empty, the previous line will send a delete event, but we are already there
        if (mnDeleteEventId)
        {
            Application::RemoveUserEvent(mnDeleteEventId);
            mnDeleteEventId = nullptr;
        }
    }
    // we delete the field directly, the Mgr cleans up the PostIt by listening
    GrabFocusToDocument();
    pWrtShell->ClearMark();
    auto restoreGuard = mrMgr.ConfigureForCommentDelete();
    pWrtShell->DelRight();
}

void SwAnnotationWin::GotoPos()
{
    mrView.GetDocShell()->GetWrtShell()->GotoField(*mpFormatField);
}

sal_uInt32 SwAnnotationWin::MoveCaret()
{
    // if this is an answer, do not skip over all following ones, but insert directly behind the current one
    // but when just leaving a note, skip all following ones as well to continue typing
    return mrMgr.IsAnswer()
           ? 1
           : 1 + CountFollowing();
}

// counts how many SwPostItField we have right after the current one
sal_uInt32 SwAnnotationWin::CountFollowing()
{
    SwTextField* pTextField = mpFormatField->GetTextField();
    SwPosition aPosition( pTextField->GetTextNode(), pTextField->GetStart() );

    for (sal_Int32 n = 1;; ++n)
    {
        SwTextAttr * pTextAttr = pTextField->GetTextNode().GetTextAttrForCharAt(
                                            aPosition.GetContentIndex() + n,
                                            RES_TXTATR_ANNOTATION );
        if (!pTextAttr)
            return n - 1;
        const SwField* pField = pTextAttr->GetFormatField().GetField();
        if (!pField || pField->Which() != SwFieldIds::Postit)
            return n - 1;
    }
}

void SwAnnotationWin::InitAnswer(OutlinerParaObject const & rText)
{
    // If tiled annotations is off in lok case, skip adding additional reply text.
    if (comphelper::LibreOfficeKit::isActive() && !comphelper::LibreOfficeKit::isTiledAnnotations())
        return;

    //collect our old meta data
    SwAnnotationWin* pWin = mrMgr.GetNextPostIt(KEY_PAGEUP, this);
    if (!pWin)
        return;

    const SvtSysLocale aSysLocale;
    const LocaleDataWrapper& rLocalData = aSysLocale.GetLocaleData();
    SwRewriter aRewriter;
    aRewriter.AddRule(UndoArg1, pWin->GetAuthor());
    const OUString aText = aRewriter.Apply(SwResId(STR_REPLY))
            + " (" + rLocalData.getDate( pWin->GetDate())
            + ", " + rLocalData.getTime( pWin->GetTime(), false)
            + "): \"";
    GetOutlinerView()->InsertText(aText);

    // insert old, selected text or "..."
    // TODO: iterate over all paragraphs, not only first one to find out if it is empty
    if (rText.GetTextObject().HasText(0))
        GetOutlinerView()->GetEditView().InsertText(rText.GetTextObject());
    else
        GetOutlinerView()->InsertText(u"..."_ustr);
    GetOutlinerView()->InsertText(u"\"\n"_ustr);

    GetOutlinerView()->SetSelection(ESelection::All());
    SfxItemSet aAnswerSet( mrView.GetDocShell()->GetPool() );
    aAnswerSet.Put(SvxFontHeightItem(200,80,EE_CHAR_FONTHEIGHT));
    aAnswerSet.Put(SvxPostureItem(ITALIC_NORMAL,EE_CHAR_ITALIC));
    GetOutlinerView()->SetAttribs(aAnswerSet);
    GetOutlinerView()->SetSelection(ESelection::AtEnd());

    //remove all attributes and reset our standard ones
    GetOutlinerView()->GetEditView().RemoveAttribsKeepLanguages(true);
    // let's insert an undo step so the initial text can be easily deleted
    // but do not use UpdateData() directly, would set modified state again and reentrance into Mgr
    mpOutliner->SetModifyHdl( Link<LinkParamNone*,void>() );
    IDocumentUndoRedo & rUndoRedo(
        mrView.GetDocShell()->GetDoc()->GetIDocumentUndoRedo());
    std::unique_ptr<SwField> pOldField;
    if (rUndoRedo.DoesUndo())
    {
        pOldField = mpField->Copy();
    }
    mpField->SetPar2(mpOutliner->GetEditEngine().GetText());
    mpField->SetTextObject(mpOutliner->CreateParaObject());
    if (rUndoRedo.DoesUndo())
    {
        SwTextField *const pTextField = mpFormatField->GetTextField();
        SwPosition aPosition( pTextField->GetTextNode(), pTextField->GetStart() );
        rUndoRedo.AppendUndo(
            std::make_unique<SwUndoFieldFromDoc>(aPosition, *pOldField, *mpField, true));
#if ENABLE_YRS
// no! there is a StartUndo wrapping this        mrView.GetDocShell()->GetDoc()->getIDocumentState().YrsEndUndo();
#endif
    }
    mpOutliner->SetModifyHdl( LINK( this, SwAnnotationWin, ModifyHdl ) );
    mpOutliner->ClearModifyFlag();
    mpOutliner->GetUndoManager().Clear();
}

void SwAnnotationWin::UpdateText(const OUString& aText)
{
    mpOutliner->Clear();
    GetOutlinerView()->InsertText(aText);
    UpdateData();
}

void SwAnnotationWin::UpdateHTML(const OUString& rHtml)
{
    mpOutliner->Clear();
    SwPostItHelper::ImportHTML(*mpOutliner, rHtml);
    UpdateData();
}

OString SwAnnotationWin::GetSimpleHtml() const
{
    return GetOutlinerView()->GetEditView().GetSimpleHtml();
}

bool SwAnnotationWin::IsReadOnly() const
{
    return mbReadonly;
}

bool SwAnnotationWin::IsReadOnlyOrProtected() const
{
    return IsReadOnly() ||
           GetLayoutStatus() == SwPostItHelper::DELETED ||
           ( mpFormatField && mpFormatField->IsProtect() );
}

OUString SwAnnotationWin::GetAuthor() const
{
    return mpField->GetPar1();
}

Date SwAnnotationWin::GetDate() const
{
    return mpField->GetDate();
}

tools::Time SwAnnotationWin::GetTime() const
{
    return mpField->GetTime();
}

FactoryFunction SwAnnotationWin::GetUITestFactory() const
{
    return CommentUIObject::create;
}

} // end of namespace sw::annotation

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