/* -*- 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 <svx/dialmgr.hxx>
#include <svx/strings.hrc>
#include <bitmaps.hlst>
#include <docrecovery.hxx>

#include <comphelper/propertyvalue.hxx>
#include <comphelper/sequenceashashmap.hxx>
#include <comphelper/string.hxx>
#include <svtools/imagemgr.hxx>
#include <sfx2/filedlghelper.hxx>
#include <tools/urlobj.hxx>
#include <utility>
#include <vcl/weld.hxx>
#include <vcl/svapp.hxx>

#include <com/sun/star/util/URL.hpp>
#include <com/sun/star/util/XURLTransformer.hpp>
#include <com/sun/star/frame/theAutoRecovery.hpp>
#include <com/sun/star/ui/dialogs/XFolderPicker2.hpp>
#include <com/sun/star/ui/dialogs/ExecutableDialogResults.hpp>
#include <com/sun/star/util/URLTransformer.hpp>
#include <osl/diagnose.h>
#include <osl/file.hxx>
#include <unotools/pathoptions.hxx>

namespace svx::DocRecovery
{

using namespace ::osl;

#define COLUMN_STANDARDIMAGE -1
#define COLUMN_DISPLAYNAME 0
#define COLUMN_STATUSIMAGE 1
#define COLUMN_STATUSTEXT 2

RecoveryCore::RecoveryCore(css::uno::Reference< css::uno::XComponentContext > xContext,
                                 bool                                            bUsedForSaving)
    : m_xContext        (std::move( xContext    ))
    , m_pListener       ( nullptr            )
    , m_bListenForSaving(bUsedForSaving)
{
    impl_startListening();
}


RecoveryCore::~RecoveryCore()
{
    impl_stopListening();
}


const css::uno::Reference< css::uno::XComponentContext >& RecoveryCore::getComponentContext() const
{
    return m_xContext;
}


TURLList& RecoveryCore::getURLListAccess()
{
    return m_lURLs;
}


bool RecoveryCore::isBrokenTempEntry(const TURLInfo& rInfo)
{
    if (rInfo.TempURL.isEmpty())
        return false;

    // Note: If the original files was recovery ... but a temp file
    // exists ... an error inside the temp file exists!
    if (
        (rInfo.RecoveryState != E_RECOVERY_FAILED            ) &&
        (rInfo.RecoveryState != E_ORIGINAL_DOCUMENT_RECOVERED)
       )
       return false;

    return true;
}


void RecoveryCore::saveBrokenTempEntries(const OUString& rPath)
{
    if (rPath.isEmpty())
        return;

    if (!m_xRealCore.is())
        return;

    // prepare all needed parameters for the following dispatch() request.
    css::util::URL aCopyURL = impl_getParsedURL(RECOVERY_CMD_DO_ENTRY_BACKUP);
    css::uno::Sequence< css::beans::PropertyValue > lCopyArgs(3);
    auto plCopyArgs = lCopyArgs.getArray();
    plCopyArgs[0].Name    = PROP_DISPATCHASYNCHRON;
    plCopyArgs[0].Value <<= false;
    plCopyArgs[1].Name    = PROP_SAVEPATH;
    plCopyArgs[1].Value <<= rPath;
    plCopyArgs[2].Name    = PROP_ENTRYID;
    // lCopyArgs[2].Value will be changed during next loop...

    // work on a copied list only...
    // Reason: We will get notifications from the core for every
    // changed or removed element. And that will change our m_lURLs list.
    // That's not a good idea, if we use a stl iterator inbetween .-)
    TURLList lURLs = m_lURLs;
    for (const TURLInfo& rInfo : lURLs)
    {
        if (!RecoveryCore::isBrokenTempEntry(rInfo))
            continue;

        plCopyArgs[2].Value <<= rInfo.ID;
        m_xRealCore->dispatch(aCopyURL, lCopyArgs);
    }
}


void RecoveryCore::saveAllTempEntries(const OUString& rPath)
{
    if (rPath.isEmpty())
        return;

    if (!m_xRealCore.is())
        return;

    // prepare all needed parameters for the following dispatch() request.
    css::util::URL aCopyURL = impl_getParsedURL(RECOVERY_CMD_DO_ENTRY_BACKUP);
    css::uno::Sequence< css::beans::PropertyValue > lCopyArgs(3);
    auto plCopyArgs = lCopyArgs.getArray();
    plCopyArgs[0].Name    = PROP_DISPATCHASYNCHRON;
    plCopyArgs[0].Value <<= false;
    plCopyArgs[1].Name    = PROP_SAVEPATH;
    plCopyArgs[1].Value <<= rPath;
    plCopyArgs[2].Name    = PROP_ENTRYID;
    // lCopyArgs[2].Value will be changed during next loop ...

    // work on a copied list only ...
    // Reason: We will get notifications from the core for every
    // changed or removed element. And that will change our m_lURLs list.
    // That's not a good idea, if we use a stl iterator inbetween .-)
    TURLList lURLs = m_lURLs;
    for (const TURLInfo& rInfo : lURLs)
    {
        if (rInfo.TempURL.isEmpty())
            continue;

        plCopyArgs[2].Value <<= rInfo.ID;
        m_xRealCore->dispatch(aCopyURL, lCopyArgs);
    }
}


void RecoveryCore::forgetBrokenTempEntries()
{
    if (!m_xRealCore.is())
        return;

    css::util::URL aRemoveURL = impl_getParsedURL(RECOVERY_CMD_DO_ENTRY_CLEANUP);
    css::uno::Sequence< css::beans::PropertyValue > lRemoveArgs(2);
    auto plRemoveArgs = lRemoveArgs.getArray();
    plRemoveArgs[0].Name    = PROP_DISPATCHASYNCHRON;
    plRemoveArgs[0].Value <<= false;
    plRemoveArgs[1].Name    = PROP_ENTRYID;
    // lRemoveArgs[1].Value will be changed during next loop ...

    // work on a copied list only ...
    // Reason: We will get notifications from the core for every
    // changed or removed element. And that will change our m_lURLs list.
    // That's not a good idea, if we use a stl iterator inbetween .-)
    TURLList lURLs = m_lURLs;
    for (const TURLInfo& rInfo : lURLs)
    {
        if (!RecoveryCore::isBrokenTempEntry(rInfo))
            continue;

        plRemoveArgs[1].Value <<= rInfo.ID;
        m_xRealCore->dispatch(aRemoveURL, lRemoveArgs);
    }
}

// should only be called with valid m_xRealCore
void RecoveryCore::forgetAllRecoveryEntriesMarkedForDiscard()
{
    assert(m_xRealCore);

    // potential to move in a separate function
    css::util::URL aRemoveURL = impl_getParsedURL(RECOVERY_CMD_DO_ENTRY_CLEANUP);
    css::uno::Sequence<css::beans::PropertyValue> lRemoveArgs(2);
    auto plRemoveArgs = lRemoveArgs.getArray();
    plRemoveArgs[0].Name = PROP_DISPATCHASYNCHRON;
    plRemoveArgs[0].Value <<= false;
    plRemoveArgs[1].Name = PROP_ENTRYID;

    // work on a copied list only ...
    // Reason: We will get notifications from the core for every
    // changed or removed element. And that will change our m_lURLs list.
    // That's not a good idea, if we use a stl iterator inbetween .-)
    TURLList lURLs = m_lURLs;
    for (const TURLInfo& rInfo : lURLs)
    {
        if (!rInfo.ShouldDiscard)
            continue;

        plRemoveArgs[1].Value <<= rInfo.ID;
        m_xRealCore->dispatch(aRemoveURL, lRemoveArgs);
    }
}

void RecoveryCore::forgetAllRecoveryEntries()
{
    if (!m_xRealCore.is())
        return;

    css::util::URL aRemoveURL = impl_getParsedURL(RECOVERY_CMD_DO_ENTRY_CLEANUP);
    css::uno::Sequence< css::beans::PropertyValue > lRemoveArgs(2);
    auto plRemoveArgs = lRemoveArgs.getArray();
    plRemoveArgs[0].Name    = PROP_DISPATCHASYNCHRON;
    plRemoveArgs[0].Value <<= false;
    plRemoveArgs[1].Name    = PROP_ENTRYID;
    // lRemoveArgs[1].Value will be changed during next loop ...

    // work on a copied list only ...
    // Reason: We will get notifications from the core for every
    // changed or removed element. And that will change our m_lURLs list.
    // That's not a good idea, if we use a stl iterator inbetween .-)
    TURLList lURLs = m_lURLs;
    for (const TURLInfo& rInfo : lURLs)
    {
        plRemoveArgs[1].Value <<= rInfo.ID;
        m_xRealCore->dispatch(aRemoveURL, lRemoveArgs);
    }
}


void RecoveryCore::forgetBrokenRecoveryEntries()
{
    if (!m_xRealCore.is())
        return;

    css::util::URL aRemoveURL = impl_getParsedURL(RECOVERY_CMD_DO_ENTRY_CLEANUP);
    css::uno::Sequence< css::beans::PropertyValue > lRemoveArgs(2);
    auto plRemoveArgs = lRemoveArgs.getArray();
    plRemoveArgs[0].Name    = PROP_DISPATCHASYNCHRON;
    plRemoveArgs[0].Value <<= false;
    plRemoveArgs[1].Name    = PROP_ENTRYID;
    // lRemoveArgs[1].Value will be changed during next loop ...

    // work on a copied list only ...
    // Reason: We will get notifications from the core for every
    // changed or removed element. And that will change our m_lURLs list.
    // That's not a good idea, if we use a stl iterator inbetween .-)
    TURLList lURLs = m_lURLs;
    for (const TURLInfo& rInfo : lURLs)
    {
        if (!RecoveryCore::isBrokenTempEntry(rInfo))
            continue;

        plRemoveArgs[1].Value <<= rInfo.ID;
        m_xRealCore->dispatch(aRemoveURL, lRemoveArgs);
    }
}


void RecoveryCore::setProgressHandler(const css::uno::Reference< css::task::XStatusIndicator >& xProgress)
{
    m_xProgress = xProgress;
}


void RecoveryCore::setUpdateListener(IRecoveryUpdateListener* pListener)
{
    m_pListener = pListener;
}


void RecoveryCore::doEmergencySavePrepare()
{
    if (!m_xRealCore.is())
        return;

    css::util::URL aURL = impl_getParsedURL(RECOVERY_CMD_DO_PREPARE_EMERGENCY_SAVE);

    css::uno::Sequence< css::beans::PropertyValue > lArgs{ comphelper::makePropertyValue(
        PROP_DISPATCHASYNCHRON, false) };

    m_xRealCore->dispatch(aURL, lArgs);
}


void RecoveryCore::doEmergencySave()
{
    if (!m_xRealCore.is())
        return;

    css::util::URL aURL = impl_getParsedURL(RECOVERY_CMD_DO_EMERGENCY_SAVE);

    css::uno::Sequence< css::beans::PropertyValue > lArgs{
        comphelper::makePropertyValue(PROP_STATUSINDICATOR, m_xProgress),
        comphelper::makePropertyValue(PROP_DISPATCHASYNCHRON, true)
    };

    m_xRealCore->dispatch(aURL, lArgs);
}


void RecoveryCore::doRecovery()
{
    if (!m_xRealCore.is())
        return;

    forgetAllRecoveryEntriesMarkedForDiscard();

    css::util::URL aURL = impl_getParsedURL(RECOVERY_CMD_DO_RECOVERY);

    css::uno::Sequence< css::beans::PropertyValue > lArgs{
        comphelper::makePropertyValue(PROP_STATUSINDICATOR, m_xProgress),
        comphelper::makePropertyValue(PROP_DISPATCHASYNCHRON, true)
    };

    m_xRealCore->dispatch(aURL, lArgs);
}


ERecoveryState RecoveryCore::mapDocState2RecoverState(EDocStates eDocState)
{
    // ???
    ERecoveryState eRecState = E_NOT_RECOVERED_YET;

    /* Attention:
        Some of the following states can occur at the
        same time. So we have to check for the "worst case" first!

        DAMAGED -> INCOMPLETE -> HANDLED
     */

    // running ...
    if (
        (eDocState & EDocStates::TryLoadBackup  ) ||
        (eDocState & EDocStates::TryLoadOriginal)
       )
        eRecState = E_RECOVERY_IS_IN_PROGRESS;
    // red
    else if (eDocState & EDocStates::Damaged)
        eRecState = E_RECOVERY_FAILED;
    // yellow
    else if (eDocState & EDocStates::Incomplete)
        eRecState = E_ORIGINAL_DOCUMENT_RECOVERED;
    // green
    else if (eDocState & EDocStates::Succeeded)
        eRecState = E_SUCCESSFULLY_RECOVERED;

    return eRecState;
}


void SAL_CALL RecoveryCore::statusChanged(const css::frame::FeatureStateEvent& aEvent)
{
    // a) special notification about start/stop async dispatch!
    //    FeatureDescriptor = "start" || "stop"
    if (aEvent.FeatureDescriptor == RECOVERY_OPERATIONSTATE_START)
    {
        return;
    }

    if (aEvent.FeatureDescriptor == RECOVERY_OPERATIONSTATE_STOP)
    {
        if (m_pListener)
            m_pListener->end();
        return;
    }

    // b) normal notification about changed items
    //    FeatureDescriptor = "Update"
    //    State             = List of information [seq< NamedValue >]
    if (aEvent.FeatureDescriptor != RECOVERY_OPERATIONSTATE_UPDATE)
        return;

    ::comphelper::SequenceAsHashMap lInfo(aEvent.State);
    TURLInfo                        aNew;

    aNew.ID          = lInfo.getUnpackedValueOrDefault(STATEPROP_ID         , sal_Int32(0)     );
    aNew.DocState    = static_cast<EDocStates>(lInfo.getUnpackedValueOrDefault(STATEPROP_STATE      , sal_Int32(0)     ));
    aNew.OrgURL      = lInfo.getUnpackedValueOrDefault(STATEPROP_ORGURL     , OUString());
    aNew.TempURL     = lInfo.getUnpackedValueOrDefault(STATEPROP_TEMPURL    , OUString());
    aNew.FactoryURL  = lInfo.getUnpackedValueOrDefault(STATEPROP_FACTORYURL , OUString());
    aNew.TemplateURL = lInfo.getUnpackedValueOrDefault(STATEPROP_TEMPLATEURL, OUString());
    aNew.DisplayName = lInfo.getUnpackedValueOrDefault(STATEPROP_TITLE      , OUString());
    aNew.Module      = lInfo.getUnpackedValueOrDefault(STATEPROP_MODULE     , OUString());

    if (aNew.OrgURL.isEmpty()) {
        // If there is no file URL, the window title is used for the display name.
        // Remove any unwanted elements such as " - LibreOffice Writer".
        sal_Int32 i = aNew.DisplayName.indexOf(" - ");
        if (i > 0)
            aNew.DisplayName = aNew.DisplayName.copy(0, i);
    } else {
        // If there is a file URL, parse out the filename part as the display name.
        INetURLObject aOrgURL(aNew.OrgURL);
        aNew.DisplayName = aOrgURL.getName(INetURLObject::LAST_SEGMENT, true,
                                           INetURLObject::DecodeMechanism::WithCharset);
    }

    // search for already existing items and update her nState value ...
    for (TURLInfo& aOld : m_lURLs)
    {
        if (aOld.ID == aNew.ID)
        {
            // change existing
            aOld.DocState      = aNew.DocState;
            aOld.RecoveryState = RecoveryCore::mapDocState2RecoverState(aOld.DocState);
            if (m_pListener)
            {
                m_pListener->updateItems();
                m_pListener->stepNext(&aOld);
            }
            return;
        }
    }

    // append as new one
    // TODO think about matching Module name to a corresponding icon
    OUString sURL = aNew.OrgURL;
    if (sURL.isEmpty())
        sURL = aNew.FactoryURL;
    if (sURL.isEmpty())
        sURL = aNew.TempURL;
    if (sURL.isEmpty())
        sURL = aNew.TemplateURL;
    INetURLObject aURL(sURL);
    aNew.StandardImageId = SvFileInformationManager::GetFileImageId(aURL);

    /* set the right UI state for this item to NOT_RECOVERED_YET... because nDocState shows the state of
       the last emergency save operation before and is interesting for the used recovery core service only...
       for now! But if there is a further notification for this item (see lines above!) we must
       map the doc state to an UI state. */
    aNew.RecoveryState = E_NOT_RECOVERED_YET;

    m_lURLs.push_back(aNew);

    if (m_pListener)
        m_pListener->updateItems();
}


void SAL_CALL RecoveryCore::disposing(const css::lang::EventObject& /*aEvent*/)
{
    m_xRealCore.clear();
}


void RecoveryCore::impl_startListening()
{
    // listening already initialized ?
    if (m_xRealCore.is())
        return;
    m_xRealCore = css::frame::theAutoRecovery::get(m_xContext);

    css::util::URL aURL;
    if (m_bListenForSaving)
        aURL.Complete = RECOVERY_CMD_DO_EMERGENCY_SAVE;
    else
        aURL.Complete = RECOVERY_CMD_DO_RECOVERY;
    css::uno::Reference< css::util::XURLTransformer > xParser(css::util::URLTransformer::create(m_xContext));
    xParser->parseStrict(aURL);

    /* Note: addStatusListener() call us synchronous back ... so we
             will get the complete list of currently open documents! */
    m_xRealCore->addStatusListener(static_cast< css::frame::XStatusListener* >(this), aURL);
}


void RecoveryCore::impl_stopListening()
{
    // Ignore it, if this instance doesn't listen currently
    if (!m_xRealCore.is())
        return;

    css::util::URL aURL;
    if (m_bListenForSaving)
        aURL.Complete = RECOVERY_CMD_DO_EMERGENCY_SAVE;
    else
        aURL.Complete = RECOVERY_CMD_DO_RECOVERY;
    css::uno::Reference< css::util::XURLTransformer > xParser(css::util::URLTransformer::create(m_xContext));
    xParser->parseStrict(aURL);

    m_xRealCore->removeStatusListener(static_cast< css::frame::XStatusListener* >(this), aURL);
    m_xRealCore.clear();
}


css::util::URL RecoveryCore::impl_getParsedURL(const OUString& sURL)
{
    css::util::URL aURL;
    aURL.Complete = sURL;

    css::uno::Reference< css::util::XURLTransformer > xParser(css::util::URLTransformer::create(m_xContext));
    xParser->parseStrict(aURL);

    return aURL;
}

PluginProgress::PluginProgress(weld::ProgressBar* pProgressBar)
    : m_pProgressBar(pProgressBar)
    , m_nRange(100)
{
}

PluginProgress::~PluginProgress()
{
}

void SAL_CALL PluginProgress::dispose()
{
    m_pProgressBar = nullptr;
}

void SAL_CALL PluginProgress::addEventListener(const css::uno::Reference< css::lang::XEventListener >& )
{
}

void SAL_CALL PluginProgress::removeEventListener( const css::uno::Reference< css::lang::XEventListener >& )
{
}

void SAL_CALL PluginProgress::start(const OUString&, sal_Int32 nRange)
{
    m_nRange = nRange;
    if (m_pProgressBar)
        m_pProgressBar->set_percentage(0);
}

void SAL_CALL PluginProgress::end()
{
    if (m_pProgressBar)
        m_pProgressBar->set_percentage(m_nRange);
}

void SAL_CALL PluginProgress::setText(const OUString& rText)
{
    if (m_pProgressBar)
        m_pProgressBar->set_text(rText);
}

void SAL_CALL PluginProgress::setValue(sal_Int32 nValue)
{
    if (m_pProgressBar)
        m_pProgressBar->set_percentage((nValue * 100) / m_nRange);
}

void SAL_CALL PluginProgress::reset()
{
    if (m_pProgressBar)
        m_pProgressBar->set_percentage(0);
}

SaveDialog::SaveDialog(weld::Window* pParent, RecoveryCore* pCore)
    : GenericDialogController(pParent, u"svx/ui/docrecoverysavedialog.ui"_ustr, u"DocRecoverySaveDialog"_ustr)
    , m_pCore(pCore)
    , m_xFileListLB(m_xBuilder->weld_tree_view(u"filelist"_ustr))
    , m_xOkBtn(m_xBuilder->weld_button(u"ok"_ustr))
{
    m_xFileListLB->set_size_request(-1, m_xFileListLB->get_height_rows(10));

    // Prepare the office for the following crash save step.
    // E.g. hide all open windows so the user can't influence our
    // operation .-)
    m_pCore->doEmergencySavePrepare();

    m_xOkBtn->connect_clicked(LINK(this, SaveDialog, OKButtonHdl));

    // fill listbox with current open documents

    TURLList&                rURLs = m_pCore->getURLListAccess();

    for (const TURLInfo& rInfo : rURLs)
    {
        m_xFileListLB->append(u""_ustr, rInfo.DisplayName, rInfo.StandardImageId);
    }
}

SaveDialog::~SaveDialog()
{
}

IMPL_LINK_NOARG(SaveDialog, OKButtonHdl, weld::Button&, void)
{
    // start crash-save with progress
    std::unique_ptr<SaveProgressDialog> xProgress(new SaveProgressDialog(m_xDialog.get(), m_pCore));
    short nResult = xProgress->run();
    xProgress.reset();

    // if "CANCEL" => return "CANCEL"
    // if "OK"     => request a restart always!
    if (nResult == DLG_RET_OK)
        nResult = DLG_RET_OK_AUTOLAUNCH;

    m_xDialog->response(nResult);
}

SaveProgressDialog::SaveProgressDialog(weld::Window* pParent, RecoveryCore* pCore)
    : GenericDialogController(pParent, u"svx/ui/docrecoveryprogressdialog.ui"_ustr, u"DocRecoveryProgressDialog"_ustr)
    , m_pCore(pCore)
    , m_xProgressBar(m_xBuilder->weld_progress_bar(u"progress"_ustr))
{
    m_xProgressBar->set_size_request(m_xProgressBar->get_approximate_digit_width() * 50, -1);
    m_xProgress = new PluginProgress(m_xProgressBar.get());
}

SaveProgressDialog::~SaveProgressDialog()
{
    if (m_xProgress)
        m_xProgress->dispose();
}

short SaveProgressDialog::run()
{
    ::SolarMutexGuard aLock;

    m_pCore->setProgressHandler(m_xProgress);
    m_pCore->setUpdateListener(this);
    m_pCore->doEmergencySave();
    short nRet = DialogController::run();
    m_pCore->setUpdateListener(nullptr);
    return nRet;
}

void SaveProgressDialog::updateItems()
{
}

void SaveProgressDialog::stepNext(TURLInfo* )
{
    /* TODO

       if m_pCore would have a member m_mCurrentItem, you could see,
       who is current, who is next ... You can show this information
       in progress report FixText
    */
}

void SaveProgressDialog::end()
{
    m_xDialog->response(DLG_RET_OK);
}

static short impl_askUserForWizardCancel(weld::Widget* pParent, TranslateId pRes)
{
    std::unique_ptr<weld::MessageDialog> xQuery(Application::CreateMessageDialog(pParent,
                                                VclMessageType::Question, VclButtonsType::YesNo, SvxResId(pRes)));
    if (xQuery->run() == RET_YES)
        return DLG_RET_OK;
    else
        return DLG_RET_CANCEL;
}

RecoveryDialog::RecoveryDialog(weld::Window* pParent, RecoveryCore* pCore)
    : GenericDialogController(pParent, u"svx/ui/docrecoveryrecoverdialog.ui"_ustr, u"DocRecoveryRecoverDialog"_ustr)
    , m_aTitleRecoveryInProgress(SvxResId(RID_SVXSTR_RECOVERY_INPROGRESS))
    , m_aRecoveryOnlyFinish (SvxResId(RID_SVXSTR_RECOVERYONLY_FINISH))
    , m_aRecoveryOnlyFinishDescr(SvxResId(RID_SVXSTR_RECOVERYONLY_FINISH_DESCR))
    , m_pCore(pCore)
    , m_eRecoveryState(RecoveryDialog::E_RECOVERY_PREPARED)
    , m_bWaitForCore(false)
    , m_bWasRecoveryStarted(false)
//    , m_aColumnOffset(0)
    , m_aToggleCount(0)
    , m_aSuccessRecovStr(SvxResId(RID_SVXSTR_SUCCESSRECOV))
    , m_aOrigDocRecovStr(SvxResId(RID_SVXSTR_ORIGDOCRECOV))
    , m_aRecovFailedStr(SvxResId(RID_SVXSTR_RECOVFAILED))
    , m_aRecovInProgrStr(SvxResId(RID_SVXSTR_RECOVINPROGR))
    , m_aNotRecovYetStr(SvxResId(RID_SVXSTR_NOTRECOVYET))
    , m_aWillBeDiscStr(SvxResId(RID_SVXSTR_WILLDISCARD))
    , m_xDescrFT(m_xBuilder->weld_label(u"desc"_ustr))
    , m_xProgressBar(m_xBuilder->weld_progress_bar(u"progress"_ustr))
    , m_xFileListLB(m_xBuilder->weld_tree_view(u"filelist"_ustr))
    , m_xNextBtn(m_xBuilder->weld_button(u"next"_ustr))
    , m_xCancelBtn(m_xBuilder->weld_button(u"cancel"_ustr))
{
    const auto nWidth = m_xFileListLB->get_approximate_digit_width() * 80;
    m_xFileListLB->set_size_request(nWidth, m_xFileListLB->get_height_rows(10));
    m_xProgressBar->set_size_request(m_xProgressBar->get_approximate_digit_width() * 50, -1);
    m_xProgress = new PluginProgress(m_xProgressBar.get());

    std::vector<int> aWidths;
    aWidths.push_back(60 * nWidth / 100);
    aWidths.push_back(5 * nWidth / 100);
    m_xFileListLB->set_column_fixed_widths(aWidths);
    m_xFileListLB->enable_toggle_buttons(weld::ColumnToggleType::Check);

    m_xNextBtn->set_sensitive(true);
    m_xNextBtn->connect_clicked( LINK( this, RecoveryDialog, NextButtonHdl ) );
    m_xCancelBtn->connect_clicked( LINK( this, RecoveryDialog, CancelButtonHdl ) );

    // fill list box first time
    TURLList& rURLList = m_pCore->getURLListAccess();
    for (size_t i = 0, nCount = rURLList.size(); i < nCount; ++i)
    {
        const TURLInfo& rInfo = rURLList[i];
        m_xFileListLB->append();
        m_xFileListLB->set_toggle(i, TRISTATE_TRUE);
        m_xFileListLB->set_id(i, weld::toId(&rInfo));
        m_xFileListLB->set_image(i, rInfo.StandardImageId, COLUMN_STANDARDIMAGE);
        m_xFileListLB->set_text(i, rInfo.DisplayName, COLUMN_DISPLAYNAME);
        m_xFileListLB->set_image(i, impl_getStatusImage(rInfo), COLUMN_STATUSIMAGE);
        m_xFileListLB->set_text(i, impl_getStatusString(rInfo), COLUMN_STATUSTEXT);
        m_aToggleCount++;
    }

    // mark first item
    if (m_xFileListLB->n_children())
        m_xFileListLB->set_cursor(0);

    m_xFileListLB->connect_toggled(LINK(this, RecoveryDialog, ToggleRowHdl));
}

RecoveryDialog::~RecoveryDialog()
{
    if (m_xProgress)
        m_xProgress->dispose();
}

bool RecoveryDialog::allSuccessfullyRecovered()
{
    const int c = m_xFileListLB->n_children();
    for (int i = 0; i < c; ++i)
    {
        TURLInfo* pInfo = weld::fromId<TURLInfo*>(m_xFileListLB->get_id(i));
        if (!pInfo)
            continue;

        if (pInfo->RecoveryState != E_SUCCESSFULLY_RECOVERED)
            return false;
    }
    return true;
}

short RecoveryDialog::execute()
{
    ::SolarMutexGuard aSolarLock;

    switch (m_eRecoveryState)
    {
        case RecoveryDialog::E_RECOVERY_IN_PROGRESS :
             {
                // user decided to start recovery ...
                m_bWasRecoveryStarted = true;
                // do it asynchronous (to allow repaints)
                // and wait for this asynchronous operation.
                m_xDescrFT->set_label( m_aTitleRecoveryInProgress );
                m_xNextBtn->set_sensitive(false);
                m_xCancelBtn->set_sensitive(false);
                m_pCore->setProgressHandler(m_xProgress);
                m_pCore->setUpdateListener(this);
                m_pCore->doRecovery();

                m_bWaitForCore = true;
                while(m_bWaitForCore && !Application::IsQuit())
                    Application::Yield();

                m_pCore->setUpdateListener(nullptr);

                // Skip FINISH button if everything was successfully recovered
                if (allSuccessfullyRecovered())
                    m_eRecoveryState = RecoveryDialog::E_RECOVERY_DONE;
                else
                    m_eRecoveryState = RecoveryDialog::E_RECOVERY_CORE_DONE;
                return execute();
             }

        case RecoveryDialog::E_RECOVERY_CORE_DONE :
             {
                 // the core finished it's task.
                 // let the user decide the next step.
                 m_xDescrFT->set_label(m_aRecoveryOnlyFinishDescr);
                 m_xNextBtn->set_label(m_aRecoveryOnlyFinish);
                 m_xNextBtn->set_sensitive(true);
                 m_xCancelBtn->set_sensitive(false);
                 return 0;
             }

        case RecoveryDialog::E_RECOVERY_DONE :
             {
                 // All documents were recovered.
                 // User decided to step to the "next" wizard page.
                 // Do it ... but check first, if there exist some
                 // failed recovery documents. They must be saved to
                 // a user selected directory.
                 short                 nRet                  = DLG_RET_UNKNOWN;
                 BrokenRecoveryDialog aBrokenRecoveryDialog(m_xDialog.get(), m_pCore, !m_bWasRecoveryStarted);
                 OUString sSaveDir = aBrokenRecoveryDialog.getSaveDirURL(); // get the default dir
                 if (aBrokenRecoveryDialog.isExecutionNeeded())
                 {
                     nRet = aBrokenRecoveryDialog.run();
                     sSaveDir = aBrokenRecoveryDialog.getSaveDirURL();
                 }

                 switch(nRet)
                 {
                     // no broken temp files exists
                     // step to the next wizard page
                     case DLG_RET_UNKNOWN :
                          {
                              m_eRecoveryState = RecoveryDialog::E_RECOVERY_HANDLED;
                              return DLG_RET_OK;
                          }

                     // user decided to save the broken temp files
                     // do and forget it
                     // step to the next wizard page
                     case DLG_RET_OK :
                          {
                              m_pCore->saveBrokenTempEntries(sSaveDir);
                              m_pCore->forgetBrokenTempEntries();
                              m_eRecoveryState = RecoveryDialog::E_RECOVERY_HANDLED;
                              return DLG_RET_OK;
                          }

                     // user decided to ignore broken temp files.
                     // Ask it again ... may be this decision was wrong.
                     // Results:
                     //     IGNORE => remove broken temp files
                     //            => step to the next wizard page
                     //     CANCEL => step back to the recovery page
                     case DLG_RET_CANCEL :
                          {
                              // TODO ask user ...
                              m_pCore->forgetBrokenTempEntries();
                              m_eRecoveryState = RecoveryDialog::E_RECOVERY_HANDLED;
                              return DLG_RET_OK;
                          }
                 }

                 m_eRecoveryState = RecoveryDialog::E_RECOVERY_HANDLED;
                 return DLG_RET_OK;
             }

        case RecoveryDialog::E_RECOVERY_CANCELED :
             {
                 // "YES" => break recovery
                 // But there exist different states, where "cancel" can be called.
                 // Handle it different.
                 if (m_bWasRecoveryStarted)
                     m_eRecoveryState = RecoveryDialog::E_RECOVERY_CANCELED_AFTERWARDS;
                 else
                     m_eRecoveryState = RecoveryDialog::E_RECOVERY_CANCELED_BEFORE;
                 return execute();
             }

        case RecoveryDialog::E_RECOVERY_CANCELED_BEFORE :
        case RecoveryDialog::E_RECOVERY_CANCELED_AFTERWARDS :
             {
                 // We have to check if there exists some temp. files.
                 // They should be saved to a user defined location.
                 // If no temp files exists or user decided to ignore it ...
                 // we have to remove all recovery/session data anyway!
                 short nRet = DLG_RET_UNKNOWN;
                 BrokenRecoveryDialog aBrokenRecoveryDialog(m_xDialog.get(), m_pCore, !m_bWasRecoveryStarted);
                 OUString sSaveDir = aBrokenRecoveryDialog.getSaveDirURL(); // get the default save location

                 // dialog itself checks if there is a need to copy files for this mode.
                 // It uses the information m_bWasRecoveryStarted doing so.
                 if (aBrokenRecoveryDialog.isExecutionNeeded())
                 {
                     nRet     = aBrokenRecoveryDialog.run();
                     sSaveDir = aBrokenRecoveryDialog.getSaveDirURL();
                 }

                 // Possible states:
                 // a) nRet == DLG_RET_UNKNOWN
                 //         dialog was not shown ...
                 //         because there exists no temp file for copy.
                 //         => remove all recovery data
                 // b) nRet == DLG_RET_OK
                 //         dialog was shown ...
                 //         user decided to save temp files
                 //         => save all OR broken temp files (depends from the time, where cancel was called)
                 //         => remove all recovery data
                 // c) nRet == DLG_RET_CANCEL
                 //         dialog was shown ...
                 //         user decided to ignore temp files
                 //         => remove all recovery data
                 // => a)/c) are the same ... b) has one additional operation

                 // b)
                 if (nRet == DLG_RET_OK)
                 {
                     if (m_bWasRecoveryStarted)
                         m_pCore->saveBrokenTempEntries(sSaveDir);
                     else
                         m_pCore->saveAllTempEntries(sSaveDir);
                 }

                 // a,b,c)
                 if (m_bWasRecoveryStarted)
                    m_pCore->forgetBrokenRecoveryEntries();
                 else
                    m_pCore->forgetAllRecoveryEntries();
                 m_eRecoveryState = RecoveryDialog::E_RECOVERY_HANDLED;

                 // THERE IS NO WAY BACK. see impl_askUserForWizardCancel()!
                 return DLG_RET_CANCEL;
             }
    }

    // should never be reached .-)
    OSL_FAIL("Should never be reached!");
    return DLG_RET_OK;
}

void RecoveryDialog::updateItems()
{
    int c = m_xFileListLB->n_children();
    for (int i = 0; i < c; ++i)
    {
        TURLInfo* pInfo = weld::fromId<TURLInfo*>(m_xFileListLB->get_id(i));
        if ( !pInfo )
            continue;

        m_xFileListLB->set_image(i, impl_getStatusImage(*pInfo), COLUMN_STATUSIMAGE);
        OUString sStatus = impl_getStatusString( *pInfo );
        if (!sStatus.isEmpty())
            m_xFileListLB->set_text(i, sStatus, COLUMN_STATUSTEXT);
    }
}

void RecoveryDialog::stepNext(TURLInfo* pItem)
{
    int c = m_xFileListLB->n_children();
    for (int i=0; i < c; ++i)
    {
        TURLInfo* pInfo = weld::fromId<TURLInfo*>(m_xFileListLB->get_id(i));
        if (pInfo->ID != pItem->ID)
            continue;

        m_xFileListLB->set_cursor(i);
        m_xFileListLB->scroll_to_row(i);
        break;
    }
}

void RecoveryDialog::end()
{
    m_bWaitForCore = false;
}

IMPL_LINK_NOARG(RecoveryDialog, NextButtonHdl, weld::Button&, void)
{
    switch (m_eRecoveryState)
    {
        case RecoveryDialog::E_RECOVERY_PREPARED:
            m_eRecoveryState = RecoveryDialog::E_RECOVERY_IN_PROGRESS;
            execute();
        break;
        case RecoveryDialog::E_RECOVERY_CORE_DONE:
            m_eRecoveryState = RecoveryDialog::E_RECOVERY_DONE;
            execute();
        break;
    }

    if (m_eRecoveryState == RecoveryDialog::E_RECOVERY_HANDLED)
    {
        m_xDialog->response(DLG_RET_OK);
    }
}

IMPL_LINK_NOARG(RecoveryDialog, CancelButtonHdl, weld::Button&, void)
{
    switch (m_eRecoveryState)
    {
        case RecoveryDialog::E_RECOVERY_PREPARED:
            if (impl_askUserForWizardCancel(m_xDialog.get(), RID_SVXSTR_QUERY_EXIT_RECOVERY) != DLG_RET_CANCEL)
            {
                m_eRecoveryState = RecoveryDialog::E_RECOVERY_CANCELED;
                execute();
            }
            break;
        case RecoveryDialog::E_RECOVERY_CORE_DONE:
            m_eRecoveryState = RecoveryDialog::E_RECOVERY_CANCELED;
            execute();
            break;
    }

    if (m_eRecoveryState == RecoveryDialog::E_RECOVERY_HANDLED)
    {
        m_xDialog->response(RET_CANCEL);
    }
}

IMPL_LINK_NOARG(RecoveryDialog, ToggleRowHdl, const weld::TreeView::iter_col&, void)
{
    int aIndex = m_xFileListLB->get_selected_index();
    TriState eState = m_xFileListLB->get_toggle(aIndex);

    if (m_bWasRecoveryStarted)
    {
        switch (eState)
        {
            case TRISTATE_FALSE:
                 eState = TRISTATE_TRUE;
                break;
            case TRISTATE_TRUE:
                eState = TRISTATE_FALSE;
                break;
            default:
                // should never happen
                assert(false);
                break;
        }

        // revert toggle
        m_xFileListLB->set_toggle(aIndex, eState);
    }
    else
    {
        impl_updateItemDescription(aIndex, eState);

        switch (eState)
        {
            case TRISTATE_FALSE:
                m_aToggleCount--;
                break;
            case TRISTATE_TRUE:
                m_aToggleCount++;
                break;
            default:
                // should never happen
                assert(false);
                break;
        }

        m_xNextBtn->set_sensitive(m_aToggleCount != 0);
    }
}

OUString RecoveryDialog::impl_getStatusString( const TURLInfo& rInfo ) const
{
    OUString sStatus;
    switch ( rInfo.RecoveryState )
    {
        case E_SUCCESSFULLY_RECOVERED :
            sStatus = m_aSuccessRecovStr;
            break;
        case E_ORIGINAL_DOCUMENT_RECOVERED :
            sStatus = m_aOrigDocRecovStr;
            break;
        case E_RECOVERY_FAILED :
            sStatus = m_aRecovFailedStr;
            break;
        case E_RECOVERY_IS_IN_PROGRESS :
            sStatus = m_aRecovInProgrStr;
            break;
        case E_NOT_RECOVERED_YET :
            sStatus = m_aNotRecovYetStr;
            break;
        case E_WILL_BE_DISCARDED:
            sStatus = m_aWillBeDiscStr;
            break;
        default:
            break;
    }
    return sStatus;
}

OUString RecoveryDialog::impl_getStatusImage( const TURLInfo& rInfo )
{
    OUString sStatus;
    switch ( rInfo.RecoveryState )
    {
        case E_SUCCESSFULLY_RECOVERED :
            sStatus = RID_SVXBMP_GREENCHECK;
            break;
        case E_ORIGINAL_DOCUMENT_RECOVERED :
            sStatus = RID_SVXBMP_YELLOWCHECK;
            break;
        case E_RECOVERY_FAILED :
            sStatus = RID_SVXBMP_REDCROSS;
            break;
        default:
            break;
    }
    return sStatus;
}

void RecoveryDialog::impl_updateItemDescription(int row, const TriState& rState)
{
    TURLInfo* pInfo = reinterpret_cast<TURLInfo*>(m_xFileListLB->get_id(row).toInt64());
    if (!pInfo)
        return;

    switch (rState)
    {
        case TRISTATE_FALSE:
            pInfo->RecoveryState = ERecoveryState::E_WILL_BE_DISCARDED;
            pInfo->ShouldDiscard = true;
            break;
        case TRISTATE_TRUE:
            pInfo->RecoveryState = ERecoveryState::E_NOT_RECOVERED_YET;
            pInfo->ShouldDiscard = false;
            break;
        default:
            // should never happen
            assert(false);
            break;
    }

    OUString sStatus = impl_getStatusString(*pInfo);
    if (!sStatus.isEmpty())
        m_xFileListLB->set_text(row, sStatus, COLUMN_STATUSTEXT);
}

BrokenRecoveryDialog::BrokenRecoveryDialog(weld::Window* pParent,
                                           RecoveryCore* pCore,
                                           bool bBeforeRecovery)
    : GenericDialogController(pParent, u"svx/ui/docrecoverybrokendialog.ui"_ustr, u"DocRecoveryBrokenDialog"_ustr)
    , m_pCore(pCore)
    , m_bBeforeRecovery(bBeforeRecovery)
    , m_bExecutionNeeded(false)
    , m_xFileListLB(m_xBuilder->weld_tree_view(u"filelist"_ustr))
    , m_xSaveDirED(m_xBuilder->weld_entry(u"savedir"_ustr))
    , m_xSaveDirBtn(m_xBuilder->weld_button(u"change"_ustr))
    , m_xOkBtn(m_xBuilder->weld_button(u"ok"_ustr))
    , m_xCancelBtn(m_xBuilder->weld_button(u"cancel"_ustr))
{
    m_xSaveDirBtn->connect_clicked( LINK( this, BrokenRecoveryDialog, SaveButtonHdl ) );
    m_xOkBtn->connect_clicked( LINK( this, BrokenRecoveryDialog, OkButtonHdl ) );
    m_xCancelBtn->connect_clicked( LINK( this, BrokenRecoveryDialog, CancelButtonHdl ) );

    m_sSavePath = SvtPathOptions().GetWorkPath();
    INetURLObject aObj( m_sSavePath );
    OUString sPath;
    osl::FileBase::getSystemPathFromFileURL(aObj.GetMainURL( INetURLObject::DecodeMechanism::NONE ), sPath);
    m_xSaveDirED->set_text(sPath);

    impl_refresh();
}

BrokenRecoveryDialog::~BrokenRecoveryDialog()
{
}

void BrokenRecoveryDialog::impl_refresh()
{
    m_bExecutionNeeded = false;
    TURLList& rURLList = m_pCore->getURLListAccess();
    for (const TURLInfo& rInfo : rURLList)
    {
        if (m_bBeforeRecovery)
        {
            // "Cancel" before recovery ->
            // search for any temp files!
            if (rInfo.TempURL.isEmpty())
                continue;
        }
        else
        {
            // "Cancel" after recovery ->
            // search for broken temp files
            if (!RecoveryCore::isBrokenTempEntry(rInfo))
                continue;
        }

        m_bExecutionNeeded = true;

        m_xFileListLB->append(weld::toId(&rInfo), rInfo.DisplayName, rInfo.StandardImageId);
    }
    m_sSavePath.clear();
    m_xOkBtn->grab_focus();
}

bool BrokenRecoveryDialog::isExecutionNeeded() const
{
    return m_bExecutionNeeded;
}

const OUString& BrokenRecoveryDialog::getSaveDirURL() const
{
    return m_sSavePath;
}

IMPL_LINK_NOARG(BrokenRecoveryDialog, OkButtonHdl, weld::Button&, void)
{
    OUString sPhysicalPath = comphelper::string::strip(m_xSaveDirED->get_text(), ' ');
    OUString sURL;
    osl::FileBase::getFileURLFromSystemPath( sPhysicalPath, sURL );
    m_sSavePath = sURL;
    while (m_sSavePath.isEmpty())
        impl_askForSavePath();

    m_xDialog->response(DLG_RET_OK);
}

IMPL_LINK_NOARG(BrokenRecoveryDialog, CancelButtonHdl, weld::Button&, void)
{
    m_xDialog->response(RET_CANCEL);
}

IMPL_LINK_NOARG(BrokenRecoveryDialog, SaveButtonHdl, weld::Button&, void)
{
    impl_askForSavePath();
}

void BrokenRecoveryDialog::impl_askForSavePath()
{
    css::uno::Reference< css::ui::dialogs::XFolderPicker2 > xFolderPicker =
        sfx2::createFolderPicker(m_pCore->getComponentContext(), m_xDialog.get());

    INetURLObject aURL(m_sSavePath, INetProtocol::File);
    xFolderPicker->setDisplayDirectory(aURL.GetMainURL(INetURLObject::DecodeMechanism::NONE));
    short nRet = xFolderPicker->execute();
    if (nRet == css::ui::dialogs::ExecutableDialogResults::OK)
    {
        m_sSavePath = xFolderPicker->getDirectory();
        OUString sPath;
        osl::FileBase::getSystemPathFromFileURL(m_sSavePath, sPath);
        m_xSaveDirED->set_text(sPath);
    }
}

}

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