// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only

/*!
    \example imagescaling
    \meta tags {widgets, threads, network}
    \title Image Scaling
    \ingroup qtconcurrentexamples
    \examplecategory {Networking}
    \brief Demonstrates how to asynchronously download and scale images.

    This example shows how to use the QFuture, QPromise, and QFutureWatcher
    classes to download a collection of images from the network and scale them,
    without blocking the UI.

    \image imagescaling.webp

    The application consists of the following steps:

    \list 1
    \li Download images form the list of URLs specified by the user.
    \li Scale the images.
    \li Show the scaled images in a grid layout.
    \endlist

    Let's start with the download:

    \snippet imagescaling/imagescaling.cpp 8

    The \c download() method takes a list of URLs and returns a QFuture. The QFuture
    stores the byte array data received for each downloaded image. To store the data
    inside the QFuture, we create a QPromise object and report that it has started to
    indicate the start of the download:

    \snippet imagescaling/imagescaling.cpp 9
    \dots
    \snippet imagescaling/imagescaling.cpp 13

    The future associated with the promise is returned to the caller.

    Without going into details yet, let's note that the promise object is wrapped
    inside a QSharedPointer. This will be explained later.

    We use QNetworkAccessManager to send network requests and download data for each
    url:

    \snippet imagescaling/imagescaling.cpp 10

    And here starts the interesting part:

    \snippet imagescaling/imagescaling.cpp 11
    \dots

    Instead of connecting to QNetworkReply's signals using the QObject::connect()
    method, we use QtFuture::connect(). It works similar to QObject::connect(), but
    returns a QFuture object, that becomes available as soon as the
    QNetworkReply::finished() signal is emitted. This allows us to attach continuations
    and failure handlers, as it is done in the example.

    In the continuation attached via \l{QFuture::then}{.then()}, we check if the
    user has requested to cancel the download. If that's the case, we stop
    processing the request. By calling the \l QPromise::finish() method, we notify
    the user that processing has been finished.
    In case the network request has ended with an error, we throw an exception. The
    exception will be handled in the failure handler attached using the
    \l{QFuture::onFailed}{.onFailed()} method.
    Note that we have two failure handlers: the first one captures the network
    errors, the second one all other exceptions thrown during the execution. Both handlers
    save the exception inside the promise object (to be handled by the caller of the
    \c download() method) and report that the computation has finished. Also note that,
    for simplicity, in case of an error we interrupt all pending downloads.

    If the request has not been canceled and no error occurred, we read the data from
    the network reply and add it to the list of results of the promise object:

    \dots
    \snippet imagescaling/imagescaling.cpp 12
    \dots

    If the number of results stored inside the promise object is equal to the number
    of the \c {url}s to be downloaded, there are no more requests to process, so we also
    report that the promise has finished.

    As mentioned earlier, we've wrapped the promise inside a QSharedPointer.
    Since the promise object is shared between handlers connected to each network reply,
    we need to copy and use the promise object in multiple places simultaneously. Hence,
    a QSharedPointer is used.

    The \c download() method is called from the \c Images::process method. It is invoked
    when the user presses the \e {"Add URLs"} button:

    \dots
    \snippet imagescaling/imagescaling.cpp 1
    \dots

    After clearing the possible leftovers from previous download, we create a dialog
    so that the user can specify the URLs for the images to download. Based on the
    specified URL count, we initialize the layout where the images will be shown and
    start the download. The future returned by the \c download() method is saved, so that
    the user can cancel the download if needed:

    \snippet imagescaling/imagescaling.cpp 3
    \dots

    Next, we attach a continuation to handle the scaling step.
    More on that later:

    \snippet imagescaling/imagescaling.cpp 4
    \dots

    After that we attach \l {QFuture::}{onCanceled()} and \l {QFuture::}{onFailed()}
    handlers:

    \snippet imagescaling/imagescaling.cpp 5
    \dots

    The handler attached via the \l {QFuture::onCanceled}{.onCanceled()} method
    will be called if the user has pressed the \e "Cancel" button:

    \dots
    \snippet imagescaling/imagescaling.cpp 2
    \dots

    The \c cancel() method simply aborts all the pending requests:

    \snippet imagescaling/imagescaling.cpp 7

    The handlers attached via \l {QFuture::onFailed}{.onFailed()} method will be
    called in case an error occurred during one of the previous steps.
    For example, if a network error has been saved inside the promise during the
    download step, it will be propagated to the handler that takes
    \l QNetworkReply::NetworkError as argument.

    If the \c downloadFuture is not canceled, and didn't report any error, the
    scaling continuation is executed.

    Since the scaling may be computationally heavy, and we don't want to block
    the main thread, we use \l QtConcurrent::run(), to launch the scaling step
    in a new thread.

    \snippet imagescaling/imagescaling.cpp 16

    Since the scaling is launched in a separate thread, the user can potentially
    decide to close the application while the scaling operation is in progress.
    To handle such situations gracefully, we pass the \l QFuture returned by
    \l QtConcurrent::run() to the \l QFutureWatcher instance.

    The watcher's \l QFutureWatcher::finished signal is connected to the
    \c Images::scaleFinished slot:

    \snippet imagescaling/imagescaling.cpp 6

    This slot is responsible for showing the scaled images in the UI, and also
    for handling the errors that could potentially happen during scaling:

    \snippet imagescaling/imagescaling.cpp 15

    The error reporting is implemented by returning an optional from the
    \c Images::scaled() method:

    \snippet imagescaling/imagescaling.cpp 14

    The \c Images::OptionalImages type here is simply a typedef for \c std::optional:

    \snippet imagescaling/imagescaling.h 1

    \note We cannot handle the errors from the async scaling operation using
    the \l {QFuture::onFailed}{.onFailed()} handler, because the handler needs
    to be executed in the context of \c Images object in the UI thread.
    If the user closes the application while the async computation is done,
    the \c Images object will be destroyed, and accessing its members from the
    continuation will lead to a crash. Using \l QFutureWatcher and its signals
    allows us to avoid the problem, because the signals are disconnected when
    the \l QFutureWatcher is destroyed, so the related slots will never be
    executed in a destroyed context.

    The rest of the code is straightforward, you can check the example project for
    more details.

    \include examples-run.qdocinc
*/
